11import re
22import logging
3- import json
43from functools import cache
54from io import BytesIO
65from typing import Any
76from geonetwork import GnApi
87from geoservercloud .services import RestService # type: ignore
8+ from geoservercloud import GeoServerCloudSync # type: ignore
9+ from geoservercloud .exceptions import DatastoreMissing , WorkspaceMissing # type: ignore
910from maelstro .metadata import Meta
1011from maelstro .config import app_config as config
1112from maelstro .common .types import GsLayer
1213from maelstro .common .models import CopyPreview , InfoRecord , SuccessRecord
1314from maelstro .common .exceptions import ParamError
1415from .georchestra import GeorchestraHandler
15- from .operations import raise_for_status
1616
1717
1818logger = logging .getLogger ()
@@ -90,35 +90,29 @@ def copy_preview(
9090 )
9191 dst_gs_url = dst_gs_info ["url" ]
9292
93- geoservers = self .meta . get_gs_layers ( config . get_gs_sources ())
94- for server_url , layer_names in geoservers . items ():
95- styles : set [ str ] = set ()
96- for layer_name in layer_names :
97-
93+ if self . include_layers or self .include_styles :
94+ geoservers = self . meta . get_gs_layers ( config . get_gs_sources ())
95+ for server_url , layer_names in geoservers . items ():
96+ styles : set [ str ] = set ()
97+ layers : set [ str ] = set ()
9898 gs_src = self .geo_hnd .get_gs_service (server_url , True )
99- layers = {}
10099 for layer_name in layer_names :
101- resp = gs_src .rest_client .get (f"/rest/layers/{ layer_name } .json" )
102- raise_for_status (resp )
103- layers [layer_name ] = resp .json ()
100+ layer , status = gs_src .get_layer (None , layer_name )
104101
105- for layer in layers .values ():
106- styles .update (self .get_styles_from_layer (layer ).keys ())
102+ if status == 200 :
103+ layers .add (str (layer_name ))
104+ styles .update (layer .all_style_names )
107105
108- if layer_names or styles :
109- # only output servers where some layers or styles have been identified
110- preview ["geoserver_resources" ].append (
111- {
112- "src" : server_url ,
113- "dst" : dst_gs_url ,
114- "layers" : (
115- [str (layer_name ) for layer_name in layer_names ]
116- if self .include_layers
117- else []
118- ),
119- "styles" : list (styles ) if self .include_styles else [],
120- }
121- )
106+ if layers or styles :
107+ # only output servers where some layers or styles have been identified
108+ preview ["geoserver_resources" ].append (
109+ {
110+ "src" : server_url ,
111+ "dst" : dst_gs_url ,
112+ "layers" : list (layers ) if self .include_layers else [],
113+ "styles" : list (styles ) if self .include_styles else [],
114+ }
115+ )
122116
123117 return CopyPreview (** preview ) # type: ignore
124118
@@ -141,7 +135,17 @@ def copy_dataset(
141135 return []
142136
143137 if self .include_layers or self .include_styles :
144- self .copy_layers ()
138+ server_layers = self .meta .get_gs_layers (config .get_gs_sources ())
139+ for gs_url , layer_names in server_layers .items ():
140+ if not layer_names :
141+ continue
142+ sync_service = self .geo_hnd .get_gs_sync_service (gs_url , self .dst_name )
143+
144+ # styles must be copied first
145+ if self .include_styles :
146+ self .copy_styles (sync_service , layer_names )
147+ if self .include_layers :
148+ self .copy_layers (sync_service , layer_names )
145149
146150 if self .include_meta :
147151 xsl_transformations = config .get_transformation_pair (
@@ -176,231 +180,53 @@ def copy_dataset(
176180 return str (results ["msg" ])
177181 return "copy_successful"
178182
179- def copy_layers (self ) -> None :
180- server_layers = self .meta .get_gs_layers (config .get_gs_sources ())
181- for gs_url , layer_names in server_layers .items ():
182- if layer_names :
183- gs_src = self .geo_hnd .get_gs_service (gs_url , True )
184- layers = {}
185- for layer_name in layer_names :
186- resp = gs_src .rest_client .get (f"/rest/layers/{ layer_name } .json" )
187- raise_for_status (resp )
188- layers [layer_name ] = resp .json ()
189-
190- stores = {}
191- workspaces = {}
192- styles = {}
193-
194- # fill in workspaces used in styles
195- if self .include_styles :
196- for layer_data in layers .values ():
197- styles .update (self .get_styles_from_layer (layer_data ))
198-
199- for style in styles .values ():
200- try :
201- workspaces .update (self .get_workspaces_from_style (style ))
202- except KeyError :
203- # skip styles without a workspace
204- pass
205-
206- # fill in workspaces and datastores used in layers
207- if self .include_layers :
208- stores .update (self .get_stores_from_layers (gs_src , layers ))
209-
210- for store in stores .values ():
211- workspaces .update (self .get_workspaces_from_store (gs_src , store ))
212-
213- self .check_workspaces (gs_src , workspaces )
214- self .check_datastores (gs_src , stores )
215-
216- # styles must be copieed first
217- if self .include_styles :
218- with self .geo_hnd .log_handler .logger_context ("Style" ):
219- for style in styles .values ():
220- self .copy_style (gs_src , style )
221- self .geo_hnd .log_handler .log_info (
222- SuccessRecord (
223- message = "Styles copied successfully" ,
224- detail = {"styles" : list (styles .keys ())},
225- )
226- )
227-
228- # styles must be available when cloning layers
229- if self .include_layers :
230- with self .geo_hnd .log_handler .logger_context ("Layer" ):
231- for layer_name , layer_data in layers .items ():
232- self .copy_layer (gs_src , layer_name , layer_data )
233- self .geo_hnd .log_handler .log_info (
234- SuccessRecord (
235- message = "Layers copied successfully" ,
236- detail = {"layers" : list (layers .keys ())},
237- )
238- )
239-
240- def get_styles_from_layer (self , layer_data : dict [str , Any ]) -> dict [str , Any ]:
241- default_style = layer_data ["layer" ]["defaultStyle" ]
242- additional_styles = layer_data ["layer" ].get ("styles" , {}).get ("style" , [])
243- if isinstance (additional_styles , dict ):
244- # in case of a single element in the list, this may be provided by the API
245- # as a dict, it must be converted to a list of dicts
246- additional_styles = [additional_styles ]
247- all_styles = {
248- style ["name" ]: style for style in [default_style ] + additional_styles
249- }
250- return all_styles
251-
252- def get_workspaces_from_style (self , style : dict [str , Any ]) -> dict [str , Any ]:
253- return {
254- # workspace references in style data have no href or other details
255- style ["workspace" ]: None
256- }
257-
258- def get_stores_from_layers (
259- self , gs_src : RestService , layers : dict [GsLayer , Any ]
260- ) -> dict [str , Any ]:
261- stores = {}
262- resources = {
263- layer_data ["layer" ]["resource" ]["name" ]: layer_data ["layer" ]["resource" ]
264- for layer_data in layers .values ()
265- }
266- for res in resources .values ():
267- resource_class = res ["@class" ]
268- resource_route = res ["href" ].replace (gs_src .url , "" )
269- resource_resp = gs_src .rest_client .get (resource_route )
270- raise_for_status (resource_resp )
271- resource_info = resource_resp .json ()
272- store = resource_info [resource_class ]["store" ]
273- stores [store ["name" ]] = store
274- return stores
275-
276- def get_workspaces_from_store (
277- self , gs_src : RestService , store : dict [str , Any ]
278- ) -> dict [str , Any ]:
279- store_class = store ["@class" ]
280- store_route = store ["href" ].replace (gs_src .url , "" )
281- store_resp = gs_src .rest_client .get (store_route )
282- raise_for_status (store_resp )
283- store_info = store_resp .json ()
284- workspace = store_info [store_class ]["workspace" ]
285- return {workspace ["name" ]: workspace }
286-
287- def check_workspaces (self , gs_src : RestService , workspaces : dict [str , Any ]) -> None :
288- for workspace_name , workspace in workspaces .items ():
289- if workspace is None :
290- workspace_route = f"/rest/workspaces/{ workspace_name } "
291- else :
292- workspace_route = workspace ["href" ].replace (gs_src .url , "" )
293- has_workspace = self .gs_dst .rest_client .get (workspace_route )
294- if has_workspace .status_code == 404 :
295- raise ParamError (
296- context = "dst" ,
297- key = workspace_route ,
298- err = f"Workspace { workspace_name } not found on destination Geoserver { self .dst_name } " ,
299- operations = self .geo_hnd .log_handler .get_json_responses (),
300- )
301- raise_for_status (has_workspace )
302-
303- def check_datastores (self , gs_src : RestService , datastores : dict [str , Any ]) -> None :
304- for store_name , store in datastores .items ():
305- store_route = store ["href" ].replace (gs_src .url , "" )
306- has_datastore = self .gs_dst .rest_client .get (store_route )
307- if has_datastore .status_code == 404 :
308- raise ParamError (
309- context = "dst" ,
310- key = store_route ,
311- err = f"Datastore { store_name } not found on destination Geoserver { self .dst_name } " ,
312- operations = self .geo_hnd .log_handler .get_json_responses (),
313- )
314- raise_for_status (has_datastore )
315-
316- def copy_layer (
317- self ,
318- gs_src : RestService ,
319- layer_name : GsLayer ,
320- layer_data : dict [str , Any ],
321- ) -> None :
322- resource_route = layer_data ["layer" ]["resource" ]["href" ].replace (gs_src .url , "" )
323-
324- layer_string = json .dumps (layer_data )
325- layer_data = json .loads (layer_string .replace (gs_src .url , self .gs_dst .url ))
326-
327- has_resource = self .gs_dst .rest_client .get (resource_route )
328- has_layer = self .gs_dst .rest_client .get (f"/rest/layers/{ layer_name } " )
329-
330- xml_resource_route = resource_route .replace (".json" , ".xml" )
331- resource_post_route = re .sub (
332- r"/[^/]*\.xml$" ,
333- "" ,
334- xml_resource_route ,
183+ def copy_styles (self , sync_service : GeoServerCloudSync , layer_names : set [GsLayer ]) -> None :
184+ # styles must be copied first
185+ styles = set (
186+ style_name
187+ for l in layer_names
188+ for layer , status in [sync_service .src_instance .get_layer (None , l )]
189+ for style_name in layer .all_style_names
190+ if status == 200
335191 )
336- resource = gs_src .rest_client .get (xml_resource_route )
337-
338- if has_resource .status_code == 200 :
339- if has_layer .status_code != 200 :
340- resp = self .gs_dst .rest_client .delete (resource_route )
341- raise_for_status (resp )
342- resp = self .gs_dst .rest_client .post (
343- resource_post_route ,
344- data = resource .content ,
345- headers = {"content-type" : "application/xml" },
192+ with self .geo_hnd .log_handler .logger_context ("Style" ):
193+ for style in styles :
194+ try :
195+ resp , status = sync_service .copy_style (style )
196+ if status != 200 :
197+ raise ParamError
198+ except (DatastoreMissing , WorkspaceMissing ) as err :
199+ raise ParamError (
200+ context = "dst" ,
201+ key = f"Style { style } " ,
202+ err = f"{ err .detail .message } on destination Geoserver { self .dst_name } " ,
203+ operations = self .geo_hnd .log_handler .get_json_responses (),
204+ ) from err
205+ self .geo_hnd .log_handler .log_info (
206+ SuccessRecord (
207+ message = "Styles copied successfully" ,
208+ detail = {"styles" : list (styles )},
346209 )
347- else :
348- resp = self .gs_dst .rest_client .put (
349- xml_resource_route ,
350- data = resource .content ,
351- headers = {"content-type" : "application/xml" },
352- )
353- else :
354- resp = self .gs_dst .rest_client .post (
355- resource_post_route ,
356- data = resource .content ,
357- headers = {"content-type" : "application/xml" },
358210 )
359- if resp .status_code == 404 :
360- raise ParamError (
361- context = "dst" ,
362- key = resource_post_route ,
363- err = "Route not found. Check Workspace and datastore" ,
364- )
365- raise_for_status (resp )
366-
367- resp = self .gs_dst .rest_client .put (
368- f"/rest/layers/{ layer_name } " , json = layer_data
369- )
370- raise_for_status (resp )
371-
372- def copy_style (
373- self ,
374- gs_src : RestService ,
375- style : dict [str , Any ],
376- ) -> None :
377- if gs_src .url in style ["href" ]:
378- style_route = style ["href" ].replace (gs_src .url , "" )
379- resp = gs_src .rest_client .get (style_route )
380- raise_for_status (resp )
381- style_info = resp .json ()
382- style_format = style_info ["style" ]["format" ]
383- style_def_route = style_route .replace (".json" , f".{ style_format } " )
384- style_def = gs_src .rest_client .get (style_def_route )
385211
386- dst_style = self .gs_dst .rest_client .get (style_route )
387- if dst_style .status_code == 200 :
388- dst_style = self .gs_dst .rest_client .put (style_route , json = style_info )
389- raise_for_status (dst_style )
390- else :
391- style_post_route = re .sub (
392- r"/styles/.*\.json" ,
393- "/styles" ,
394- style_route ,
395- )
396- dst_style = self .gs_dst .rest_client .post (
397- style_post_route , json = style_info
212+ def copy_layers (self , sync_service : GeoServerCloudSync , layer_names : set [GsLayer ]) -> None :
213+ # styles must be available when cloning layers
214+ with self .geo_hnd .log_handler .logger_context ("Layer" ):
215+ for layer_name in layer_names :
216+ try :
217+ resp , status = sync_service .copy_layer (None , layer_name )
218+ if status != 200 :
219+ raise ParamError
220+ except (DatastoreMissing , WorkspaceMissing ) as err :
221+ raise ParamError (
222+ context = "dst" ,
223+ key = f"Layer { layer_name } " ,
224+ err = f"{ err .detail .message } on destination Geoserver { self .dst_name } " ,
225+ operations = self .geo_hnd .log_handler .get_json_responses (),
226+ ) from err
227+ self .geo_hnd .log_handler .log_info (
228+ SuccessRecord (
229+ message = "Layers copied successfully" ,
230+ detail = {"layers" : list (layer_names )},
398231 )
399- # raise_for_status(dst_style)
400-
401- dst_style_def = self .gs_dst .rest_client .put (
402- style_def_route ,
403- data = style_def .content ,
404- headers = {"content-type" : style_def .headers ["content-type" ]},
405232 )
406- raise_for_status (dst_style_def )
0 commit comments