diff --git a/services/datasets/metadata.py b/services/datasets/metadata.py new file mode 100644 index 0000000..c01802a --- /dev/null +++ b/services/datasets/metadata.py @@ -0,0 +1,29 @@ +from datetime import datetime +from sqlalchemy import text +from database.connection import sync_engine +from utils.logger_config import log_activity + +def update_job_status(table_name: str, status: str, job_id: str = None): + print("update status") + query = text(""" + UPDATE backend.author_metadata + SET process = :status, + updated_at = :updated_at + WHERE table_title = :table_name + """) + + params = { + "status": status, + "updated_at": datetime.utcnow(), + "table_name": table_name + } + + with sync_engine.begin() as conn: + conn.execute(query, params) + + print(f"[DB] Metadata '{table_name}' updated to status '{status}'") + + + + + diff --git a/services/datasets/publish_geonetwork.py b/services/datasets/publish_geonetwork.py new file mode 100644 index 0000000..92f0327 --- /dev/null +++ b/services/datasets/publish_geonetwork.py @@ -0,0 +1,639 @@ +from fastapi import HTTPException +import requests +from sqlalchemy import text +from core.config import GEONETWORK_PASS, GEONETWORK_URL, GEONETWORK_USER +from database.connection import sync_engine as engine +from datetime import datetime +from uuid import uuid4 +import re + +def escape_url_params(url: str) -> str: + """ + Escape karakter berbahaya di dalam URL agar valid dalam XML. + Khususnya mengganti '&' menjadi '&' kecuali jika sudah '&'. + """ + # Ganti semua & yang bukan bagian dari & + url = re.sub(r'&(?!amp;)', '&', url) + return url + + +def fix_xml_urls(xml: str) -> str: + """ + Temukan semua ... dalam XML dan escape URL-nya. + """ + def replacer(match): + original = match.group(1).strip() + fixed = escape_url_params(original) + return f"{fixed}" + + # Replace semua ... + xml_fixed = re.sub( + r"(.*?)", + replacer, + xml, + flags=re.DOTALL + ) + + return xml_fixed + + + +def get_extent(table_name: str): + + sql = f""" + SELECT + ST_XMin(extent), ST_YMin(extent), + ST_XMax(extent), ST_YMax(extent) + FROM ( + SELECT ST_Extent(geom) AS extent + FROM public.{table_name} + ) AS box; + """ + + conn = engine.connect() + try: + row = conn.execute(text(sql)).fetchone() + finally: + conn.close() + + if not row or row[0] is None: + return None + + # return { + # "xmin": float(row[0]), + # "ymin": float(row[1]), + # "xmax": float(row[2]), + # "ymax": float(row[3]) + # } + + return { + "xmin": 110.1372, # west + "ymin": -9.3029, # south + "xmax": 114.5287, # east + "ymax": -5.4819 # north + } + + +# def get_author_metadata(table_name: str): + +# sql = """ +# SELECT table_title, dataset_title, dataset_abstract, keywords, date_created, +# organization_name, contact_person_name, +# contact_email, contact_phone, geom_type +# FROM backend.author_metadata +# WHERE table_title = :table +# LIMIT 1 +# """ + +# conn = engine.connect() +# try: +# row = conn.execute(text(sql), {"table": table_name}).fetchone() +# finally: +# conn.close() + +# if not row: +# raise Exception(f"Tidak ada metadata untuk tabel: {table_name}") + +# # FIX: SQLAlchemy Row → dict +# return dict(row._mapping) + +def get_author_metadata(table_name: str): + + sql = """ + SELECT am.table_title, am.dataset_title, am.dataset_abstract, am.keywords, am.date_created, + am.organization_name, am.contact_person_name, am.created_at, + am.contact_email, am.contact_phone, am.geom_type, + u.organization_id, + o.address AS organization_address, + o.email AS organization_email, + o.phone_number AS organization_phone + FROM backend.author_metadata AS am + LEFT JOIN backend.users u ON am.user_id = u.id + LEFT JOIN backend.organizations o ON u.organization_id = o.id + WHERE am.table_title = :table + LIMIT 1 + """ + + conn = engine.connect() + try: + row = conn.execute(text(sql), {"table": table_name}).fetchone() + finally: + conn.close() + + if not row: + raise Exception(f"Tidak ada metadata untuk tabel: {table_name}") + + return dict(row._mapping) + + +def map_geom_type(gtype): + + if gtype is None: + return "surface" + + # Jika LIST → ambil elemen pertama + if isinstance(gtype, list): + if len(gtype) > 0: + gtype = gtype[0] + else: + return "surface" + + # Setelah pasti string + gtype = str(gtype).lower() + + if "polygon" in gtype or "multi" in gtype: + return "surface" + if "line" in gtype: + return "curve" + if "point" in gtype: + return "point" + + return "surface" + + +def generate_metadata_xml(table_name, meta, extent, geoserver_links): + + keywords_xml = "".join([ + f""" + {kw.strip()} + """ for kw in meta["keywords"].split(",") + ]) + + geom_type_code = map_geom_type(meta["geom_type"]) + print('type', geom_type_code) + uuid = str(uuid4()) + + return f""" + + + {uuid} + + + + + + + + + + + + + + {meta['contact_person_name']} + + + {meta['organization_name']} + + + + + + + {meta['organization_phone']} + + + {meta['organization_phone']} + + + + + + + {meta['organization_address']} + + + Surabaya + + + Jawa Timur + + + Indonesia + + + {meta['organization_email']} + + + + + 08.00-16.00 + + + + + + + + + + {datetime.utcnow().isoformat()}+07:00 + + + ISO 19115:2003/19139 + + + 1.0 + + + + + + + + + + 38 + + + + + + + + + + + 4326 + + + EPSG + + + + + + + + + + + {meta['dataset_title']} + + + + + {meta['created_at'].isoformat()}+07:00 + + + + + + + + {meta['date_created'].year} + + + + + {meta['contact_person_name']} + + + {meta['organization_name']} + + + + + + + {meta['organization_phone']} + + + {meta['organization_phone']} + + + + + + + {meta['organization_address']} + + + Surabaya + + + Indonesia + + + {meta['organization_email']} + + + + + 08.00-16.00 + + + + + + + + + + Timezone: UTC+7 (Asia/Jakarta) + + + + + {meta['dataset_abstract']} + + + {meta['dataset_abstract']} + + + + + + + + Dinas Tenaga Kerja dan Transmigrasi Provinsi Jawa Timur + + + Dinas Tenaga Kerja dan Transmigrasi Provinsi Jawa Timur + + + + + + + + {meta['organization_phone']} + + + {meta['organization_phone']} + + + + + + + {meta['organization_address']} + + + Surabaya + + + Jawa Timur + + + Indonesia + + + {meta['organization_email']} + + + + + + + + + + + + + + + + + + + + {keywords_xml} + + + + + + + + + + + + Penggunaan data harus mencantumkan sumber: {meta['organization_name']}. + + + + + + + + + + + + 25000 + + + + + + + + + + + + + + + + {extent['xmin']} + {extent['xmax']} + {extent['ymin']} + {extent['ymax']} + + + + + + + + + + true + + + + + + {meta['dataset_title']} + + + + + {meta['created_at'].isoformat()}+07:00 + + + + + + + + {meta['date_created'].year} + + + + + + + + + + + + + {geoserver_links["wms_url"]} + + + DB:POSTGIS + + + {meta["dataset_title"]} + + + {meta["dataset_title"]} + + + + + + + + https://geoserver.jatimprov.go.id/wms?service=WMS&version=1.1.0&request=GetMap&layers=satupeta:fngsl_trw_1_4_2020_2023&bbox=110.89527893066406,-8.78022289276123,116.27019500732422,-5.042964935302734&width=768&height=534&srs=EPSG:4326&styles=&format=application/openlayers + + + + WWW:LINK-1.0-http--link + + + {meta["dataset_title"]} + + + {meta["dataset_title"]} + + + + + + + {geoserver_links["wms_url"]} + + + OGC:WMS + + + {meta["dataset_title"]} + + + + + + + + {geoserver_links["wfs_url"]} + + + OGC:WFS + + + {meta["dataset_title"]} + + + + + + + + + + + + + + + + + + + + Data dihasilkan dari digitasi peta dasar skala 1:25000 menggunakan QGIS. + + + + + + +""" + + +def upload_metadata_to_geonetwork(xml_metadata: str): + session = requests.Session() + session.auth = (GEONETWORK_USER, GEONETWORK_PASS) + + # 1. Get XSRF token + try: + info_url = f"{GEONETWORK_URL}/srv/eng/info?type=me" + session.get(info_url) + except requests.exceptions.RequestException as e: + raise HTTPException(status_code=503, detail=f"Failed to connect to GeoNetwork: {e}") + + xsrf_token = session.cookies.get('XSRF-TOKEN') + if not xsrf_token: + raise HTTPException(status_code=500, detail="Could not retrieve XSRF-TOKEN from GeoNetwork.") + + headers = { + 'X-XSRF-TOKEN': xsrf_token, + 'Accept': 'application/json' + } + + GN_API_RECORDS_URL = f"{GEONETWORK_URL}/srv/api/records" + + # 2. GeoNetwork requires a multipart/form-data upload + files = { + 'file': ('metadata.xml', xml_metadata, 'application/xml') + } + + response = session.post( + GN_API_RECORDS_URL, + files=files, + headers=headers, + cookies=session.cookies.get_dict() + ) + + # print("response", response.json()) + return response.json() + + + +def publish_metadata(table_name: str, geoserver_links: dict): + + extent = get_extent(table_name) + meta = get_author_metadata(table_name) + xml = generate_metadata_xml( + table_name=meta["dataset_title"], + meta=meta, + extent=extent, + geoserver_links=geoserver_links + ) + + xml_clean = fix_xml_urls(xml) + response = upload_metadata_to_geonetwork(xml_clean) + + uuid = response.get("uuid") + print(f"[GeoNetwork] Metadata uploaded. UUID = {uuid}") + + return uuid + + diff --git a/services/datasets/publish_geoserver.py b/services/datasets/publish_geoserver.py new file mode 100644 index 0000000..abe9704 --- /dev/null +++ b/services/datasets/publish_geoserver.py @@ -0,0 +1,251 @@ +import requests +import json +import os +from core.config import GEOSERVER_URL, GEOSERVER_USER, GEOSERVER_PASS, GEOSERVER_WORKSPACE + +# DATASTORE = "postgis" #per OPD +DATASTORE = "server_lokal" +# SLD_DIR = "./styles" + +# BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# SLD_DIR = os.path.join(BASE_DIR, "styles") + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +MAIN_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) +SLD_DIR = os.path.join(MAIN_DIR, "style_temp") + + +def publish_layer_to_geoserver(table: str, job_id: str): + print(f"[GeoServer] Publish layer + upload SLD: {table}") + + # ========================== + # 1. Publish Feature Type + # ========================== + # ft_url = f"{GEOSERVER_URL}/rest/workspaces/{GEOSERVER_WORKSPACE}/datastores/{DATASTORE}/featuretypes" + ft_url = f"{GEOSERVER_URL}/rest/workspaces/{GEOSERVER_WORKSPACE}/datastores/{DATASTORE}/featuretypes?computeDefault=true" + + payload = { + "featureType": { + "name": table, + "nativeName": table, + "enabled": True + } + } + + requests.post( + ft_url, + auth=(GEOSERVER_USER, GEOSERVER_PASS), + headers={"Content-Type": "application/json"}, + data=json.dumps(payload) + ) + + print(f"[GeoServer] FeatureType published for: {table}") + + # ========================================== + # 2. Upload SLD file to GeoServer + # ========================================== + + sld_file = f"{SLD_DIR}/{job_id}.sld" + style_name = table # style name sama dengan table + + if not os.path.exists(sld_file): + print(f"[WARNING] SLD file tidak ditemukan: {sld_file}") + else: + print(f"[GeoServer] Upload SLD {sld_file}") + style_url = f"{GEOSERVER_URL}/rest/styles" + + with open(sld_file, "rb") as sld: + requests.post( + f"{style_url}?name={style_name}&workspace={GEOSERVER_WORKSPACE}", + auth=(GEOSERVER_USER, GEOSERVER_PASS), + headers={"Content-Type": "application/vnd.ogc.sld+xml"}, + data=sld.read() + ) + + print(f"[GeoServer] SLD uploaded: {style_name}") + + # ========================================== + # 3. Apply SLD to the layer + # ========================================== + + layer_url = f"{GEOSERVER_URL}/rest/layers/{GEOSERVER_WORKSPACE}:{table}" + + payload = { + "layer": { + "defaultStyle": { + "name": style_name, + "workspace": GEOSERVER_WORKSPACE + }, + "enabled": True + } + } + + requests.put( + layer_url, + auth=(GEOSERVER_USER, GEOSERVER_PASS), + headers={"Content-Type": "application/json"}, + data=json.dumps(payload) + ) + + print(f"[GeoServer] SLD applied as default style for {table}") + + # ========================================== + # 4. Delete SLD file from local folder + # ========================================== + + os.remove(sld_file) + print(f"[CLEANUP] SLD file removed: {sld_file}") + + # ============================================== + # 5. Reload GeoServer (optional but recommended) + # ============================================== + requests.post( + f"{GEOSERVER_URL}/rest/reload", + auth=(GEOSERVER_USER, GEOSERVER_PASS) + ) + + # ==================================================== + # 7. Generate GeoServer WMS/WFS link untuk GeoNetwork + # ==================================================== + + wms_link = ( + f"{GEOSERVER_URL}/{GEOSERVER_WORKSPACE}/wms?" + f"service=WMS&request=GetMap&layers={GEOSERVER_WORKSPACE}:{table}" + ) + wfs_link = ( + f"{GEOSERVER_URL}/{GEOSERVER_WORKSPACE}/wfs?" + f"service=WFS&request=GetFeature&typeName={GEOSERVER_WORKSPACE}:{table}" + ) + print(f"[GeoServer] WMS URL: {wms_link}") + print(f"[GeoServer] WFS URL: {wfs_link}") + print(f"[GeoServer] Reload completed. Layer {table} ready.") + return { + "table": table, + "style": style_name, + "wms_url": wms_link, + "wfs_url": wfs_link + } + + + + + +# use default style +# def publish_layer_to_geoserver(table: str): + +# print(f"[GeoServer] Publish layer: {table}") + +# # ========== 1. Publish Feature Type ========== +# ft_url = f"{GEOSERVER_URL}/rest/workspaces/{WORKSPACE}/datastores/{DATASTORE}/featuretypes" + +# payload = { +# "featureType": { +# "name": table, +# "nativeName": table, +# "enabled": True +# } +# } + +# requests.post( +# ft_url, +# auth=(GEOSERVER_USER, GEOSERVER_PASS), +# headers={"Content-Type": "application/json"}, +# data=json.dumps(payload) +# ) + +# # =================================================== +# # 2. Tentukan SLD file (prioritas table.sld → fallback default) +# # =================================================== +# table_sld = SLD_DIR / f"{table}.sld" +# default_sld = SLD_DIR / "default_style.sld" + +# if table_sld.exists(): +# chosen_sld = table_sld +# delete_after = True +# style_name = table # pakai nama style sama dengan layer +# print(f"[SLD] Menggunakan SLD khusus: {chosen_sld}") +# else: +# chosen_sld = default_sld +# delete_after = False +# style_name = "default_style" +# print(f"[SLD] Menggunakan default SLD: {chosen_sld}") + +# # ========================================== +# # 3. Upload SLD +# # ========================================== +# style_url = f"{GEOSERVER_URL}/rest/styles" + +# with open(chosen_sld, "rb") as sld: +# requests.post( +# f"{style_url}?name={style_name}&workspace={WORKSPACE}", +# auth=(GEOSERVER_USER, GEOSERVER_PASS), +# headers={"Content-Type": "application/vnd.ogc.sld+xml"}, +# data=sld.read() +# ) + +# print(f"[GeoServer] SLD uploaded: {style_name}") + +# # ========================================== +# # 4. Apply SLD ke layer +# # ========================================== +# layer_url = f"{GEOSERVER_URL}/rest/layers/{WORKSPACE}:{table}" + +# payload = { +# "layer": { +# "defaultStyle": { +# "name": style_name, +# "workspace": WORKSPACE +# }, +# "enabled": True +# } +# } + +# requests.put( +# layer_url, +# auth=(GEOSERVER_USER, GEOSERVER_PASS), +# headers={"Content-Type": "application/json"}, +# data=json.dumps(payload) +# ) + +# print(f"[GeoServer] Style '{style_name}' applied to layer '{table}'") + +# # ========================================== +# # 5. Delete table.sld jika ada +# # ========================================== +# if delete_after: +# table_sld.unlink() +# print(f"[CLEANUP] File SLD '{table}.sld' dihapus") + +# # ==================================================== +# # 6. Reload GeoServer (opsional tapi aman) +# # ==================================================== +# requests.post( +# f"{GEOSERVER_URL}/rest/reload", +# auth=(GEOSERVER_USER, GEOSERVER_PASS) +# ) + +# # ==================================================== +# # 7. Generate GeoServer WMS/WFS link untuk GeoNetwork +# # ==================================================== + +# wms_link = ( +# f"{GEOSERVER_URL}/{WORKSPACE}/wms?" +# f"service=WMS&request=GetMap&layers={WORKSPACE}:{table}" +# ) + +# wfs_link = ( +# f"{GEOSERVER_URL}/{WORKSPACE}/wfs?" +# f"service=WFS&request=GetFeature&typeName={WORKSPACE}:{table}" +# ) + +# print(f"[GeoServer] WMS URL: {wms_link}") +# print(f"[GeoServer] WFS URL: {wfs_link}") + +# return { +# "table": table, +# "style": style_name, +# "wms_url": wms_link, +# "wfs_url": wfs_link +# } + +