diff --git a/docs/src/qsa-api/endpoints/projects.md b/docs/src/qsa-api/endpoints/projects.md index 99d5ad62..d7c81025 100644 --- a/docs/src/qsa-api/endpoints/projects.md +++ b/docs/src/qsa-api/endpoints/projects.md @@ -175,3 +175,28 @@ $ curl "http://localhost:5000/api/projects/my_project/styles" \ } }' ```` + +## Cache + +| Method | URL | Description | +|---------|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| GET | `/api/projects/{project}/cache` | Return metadata about the cache | +| POST | `/api/projects/{project}/cache/reset` | Clear cached data and reset cache configuration | + +Example: + +```` shell +$ curl "http://localhost:5000/api/projects/my_project/cache" +{ + "valid":true, + "storage":"filesystem" +} +```` + +
+Reset cache + +When a QGIS project is created manually without QSA, the cache is not +initialized. This method allows to create the MapProxy configuration file +accordingly. +
diff --git a/qsa-api/qsa_api/api/projects.py b/qsa-api/qsa_api/api/projects.py index c563d514..d0f456e2 100644 --- a/qsa-api/qsa_api/api/projects.py +++ b/qsa-api/qsa_api/api/projects.py @@ -436,3 +436,39 @@ def project_del_layer(name, layer_name): except Exception as e: logger().exception(str(e)) return {"error": "internal server error"}, 415 + + +@projects.get("//cache") +def project_cache(name): + log_request() + try: + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + if project.exists(): + cache_infos, err = project.cache_metadata() + if err: + return {"error": err}, 415 + return jsonify(cache_infos), 201 + else: + return {"error": "Project does not exist"}, 415 + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 + + +@projects.post("//cache/reset") +def project_cache_reset(name): + log_request() + try: + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + if project.exists(): + rc, err = project.cache_reset() + if err: + return {"error": err}, 415 + return jsonify(rc), 201 + else: + return {"error": "Project does not exist"}, 415 + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 diff --git a/qsa-api/qsa_api/mapproxy/mapproxy.py b/qsa-api/qsa_api/mapproxy/mapproxy.py index 927f6236..72e7533a 100644 --- a/qsa-api/qsa_api/mapproxy/mapproxy.py +++ b/qsa-api/qsa_api/mapproxy/mapproxy.py @@ -30,7 +30,12 @@ def write(self) -> None: with open(self._mapproxy_project, "w") as file: yaml.safe_dump(self.cfg, file, sort_keys=False) - def read(self) -> bool: + def read(self) -> (bool, str): + # if a QGIS project is created manually without QSA, the MapProxy + # configuration file may not be created at this point. + if not self._mapproxy_project.exists(): + self.create() + try: with open(self._mapproxy_project, "r") as file: self.cfg = yaml.safe_load(file) @@ -48,6 +53,21 @@ def read(self) -> bool: return True, "" + def metadata(self) -> dict: + md = {} + + md["storage"] = "" + md["valid"] = False + + if self._mapproxy_project.exists(): + md["valid"] = True + + md["storage"] = "filesystem" + if config().mapproxy_cache_s3_bucket: + md["storage"] = "s3" + + return md + def clear_cache(self, layer_name: str) -> None: if config().mapproxy_cache_s3_bucket: bucket_name = config().mapproxy_cache_s3_bucket diff --git a/qsa-api/qsa_api/project.py b/qsa-api/qsa_api/project.py index 6e590a32..9179b852 100644 --- a/qsa-api/qsa_api/project.py +++ b/qsa-api/qsa_api/project.py @@ -129,8 +129,44 @@ def metadata(self) -> dict: if StorageBackend.type() == StorageBackend.POSTGRESQL: m["schema"] = self.schema + + m["cache"] = "disabled" + if self._mapproxy_enabled: + m["cache"] = "mapproxy" + return m + def cache_metadata(self) -> (dict, str): + if self._mapproxy_enabled: + return QSAMapProxy(self.name).metadata(), "" + return {}, "Cache is disabled" + + def cache_reset(self) -> (bool, str): + if self._mapproxy_enabled: + mp = QSAMapProxy(self.name) + rc, err = mp.read() + if not rc: + return False, err + + p = QgsProject() + p.read(self._qgis_project_uri) + + for layer in p.mapLayers().values(): + t = layer.type() + bbox = QSAProject._layer_bbox(layer) + epsg_code = QSAProject._layer_epsg_code(layer) + + mp.remove_layer(layer.name()) + mp.add_layer( + layer.name(), bbox, epsg_code, t == Qgis.LayerType.Raster, None + ) + + mp.write() + + return True, "" + + return False, "Cache is disabled" + def style_default(self, geometry: str) -> bool: con = sqlite3.connect(self.sqlite_db.as_posix()) cur = con.cursor() @@ -245,7 +281,7 @@ def layer_update_style( def layer_exists(self, name: str) -> bool: return bool(self.layer(name)) - def remove_layer(self, name: str) -> None: + def remove_layer(self, name: str) -> bool: # remove layer in qgis project project = QgsProject() project.read(self._qgis_project_uri, Qgis.ProjectReadFlag.DontResolveLayers) @@ -260,7 +296,11 @@ def remove_layer(self, name: str) -> None: # remove layer in mapproxy config if self._mapproxy_enabled: mp = QSAMapProxy(self.name) - mp.read() + rc, err = mp.read() + if not rc: + self.debug(err) + return False + mp.remove_layer(name) mp.write() @@ -319,8 +359,13 @@ def remove(self) -> None: for layer in self.layers: self.remove_layer(layer) + # remove mapproxy config file + if self._mapproxy_enabled: + mp = QSAMapProxy(self.name) + mp.remove() + # remove qsa projects dir - shutil.rmtree(self._qgis_project_dir) + shutil.rmtree(self._qgis_project_dir, ignore_errors=True) # remove remove qgis prohect in db if necessary if StorageBackend.type() == StorageBackend.POSTGRESQL: @@ -412,20 +457,10 @@ def add_layer( if self._mapproxy_enabled: self.debug("Update MapProxy configuration file") - bbox = list( - map( - float, - lyr.extent() - .asWktCoordinates() - .replace(",", "") - .split(" "), - ) - ) - - authid_items = lyr.crs().authid().split(":") - if len(authid_items) < 2: + bbox = QSAProject._layer_bbox(lyr) + epsg_code = QSAProject._layer_epsg_code(lyr) + if epsg_code < 0: return False, f"Invalid CRS {lyr.crs().authid()}" - epsg_code = int(authid_items[1]) self.debug(f"EPSG code {epsg_code}") @@ -704,6 +739,25 @@ def _layer_provider(layer_type: Qgis.LayerType, datasource: str) -> str: provider = "gdal" return provider + @staticmethod + def _layer_epsg_code(lyr) -> int: + authid_items = lyr.crs().authid().split(":") + if len(authid_items) < 2: + return -1 + return int(authid_items[1]) + + @staticmethod + def _layer_bbox(lyr) -> list: + return list( + map( + float, + lyr.extent() + .asWktCoordinates() + .replace(",", "") + .split(" "), + ) + ) + @property def _qgis_project_uri(self) -> str: if StorageBackend.type() == StorageBackend.POSTGRESQL: