55from zope .component import adapter
66from zope .interface import implementer
77from zope .interface import Interface
8+ from plone import api
9+ from datetime import datetime
10+ from redturtle .rsync .scripts .rsync import logger
811
912import json
1013import requests
14+ import re
15+ import uuid
1116
1217
1318class TimeoutHTTPAdapter (HTTPAdapter ):
@@ -37,6 +42,15 @@ class RsyncAdapterBase:
3742 def __init__ (self , context , request ):
3843 self .context = context
3944 self .request = request
45+ self .options = None
46+ self .logdata = []
47+ self .n_updated = 0
48+ self .n_created = 0
49+ self .n_items = 0
50+ self .n_todelete = 0
51+ self .sync_uids = set ()
52+ self .start = datetime .now ()
53+ self .end = None
4054
4155 def requests_retry_session (
4256 self ,
@@ -63,12 +77,95 @@ def requests_retry_session(
6377 session .mount ("https://" , http_adapter )
6478 return session
6579
66- def log_item_title (self , start , options ):
80+ def log_item_title (self , start ):
6781 """
6882 Return the title of the log item for the rsync command.
6983 """
7084 return f"Report sync { start .strftime ('%d-%m-%Y %H:%M:%S' )} "
7185
86+ def autolink (self , text ):
87+ """
88+ Fix links in the text.
89+ """
90+ return re .sub (
91+ r"(https?://\S+|/\S+)" ,
92+ r'<a href="\1">\1</a>' ,
93+ text ,
94+ re .MULTILINE | re .DOTALL ,
95+ )
96+
97+ def get_frontend_url (self , item ):
98+ frontend_domain = api .portal .get_registry_record (
99+ name = "volto.frontend_domain" , default = ""
100+ )
101+ if not frontend_domain or frontend_domain == "https://" :
102+ frontend_domain = "http://localhost:3000"
103+ if frontend_domain .endswith ("/" ):
104+ frontend_domain = frontend_domain [:- 1 ]
105+ portal_url = api .portal .get ().portal_url ()
106+
107+ return item .absolute_url ().replace (portal_url , frontend_domain )
108+
109+ def log_info (self , msg , type = "info" , force_sys_log = False ):
110+ """
111+ append a message to the logdata list and print it.
112+
113+ """
114+ style = ""
115+ if type == "error" :
116+ style = "padding:5px;background-color:red;color:#fff"
117+ if type == "warning" :
118+ style = "padding:5px;background-color:#ff9d00;color:#fff"
119+ msg = f"[{ datetime .now ().strftime ('%d-%m-%Y %H:%M:%S' )} ] { msg } "
120+ self .logdata .append (f'<p style="{ style } ">{ self .autolink (msg )} </p>' )
121+
122+ # print the message on standard output
123+ if type == "error" :
124+ logger .error (msg )
125+ elif type == "warning" :
126+ logger .warning (msg )
127+ else :
128+ if self .options .verbose or force_sys_log :
129+ logger .info (msg )
130+
131+ def get_log_container (self ):
132+ logpath = getattr (self .options , "logpath" , None )
133+ if not logpath :
134+ logger .warning ("No logpath specified, skipping log write into database." )
135+ return
136+ logcontainer = api .content .get (logpath )
137+ if not logcontainer :
138+ logger .warning (
139+ f'Log container not found with path "{ logpath } ", skipping log write into database.'
140+ )
141+ return
142+ return logcontainer
143+
144+ def write_log (self ):
145+ """
146+ Write the log into the database.
147+ """
148+ logcontainer = self .get_log_container ()
149+ if not logcontainer :
150+ return
151+ description = f"{ self .n_items } elementi trovati, { self .n_created } creati, { self .n_updated } aggiornati, { self .n_todelete } da eliminare"
152+ blockid = str (uuid .uuid4 ())
153+ api .content .create (
154+ logcontainer ,
155+ "Document" ,
156+ title = self .log_item_title (start = self .start ),
157+ description = description ,
158+ blocks = {
159+ blockid : {
160+ "@type" : "html" ,
161+ "html" : "\n " .join (self .logdata ),
162+ }
163+ },
164+ blocks_layout = {
165+ "items" : [blockid ],
166+ },
167+ )
168+
72169 def set_args (self , parser ):
73170 """
74171 Set some additional arguments for the rsync command.
@@ -82,33 +179,144 @@ def set_args(self, parser):
82179 """
83180 return
84181
85- def get_data (self , options ):
182+ def get_data (self ):
183+ """ """
184+ try :
185+ data = self .do_get_data ()
186+ except Exception as e :
187+ logger .exception (e )
188+ msg = f"Error in data generation: { e } "
189+ self .log_info (msg = msg , type = "error" )
190+ return
191+ if not data :
192+ msg = "No data to sync."
193+ self .log_info (msg = msg , type = "warning" )
194+ return
195+ return data
196+
197+ def convert_source_data (self , data ):
198+ """
199+ If needed, convert the source data to a format that can be used by the rsync command.
200+ """
201+ return data , None
202+
203+ def find_item_from_row (self , row ):
204+ """
205+ Find the item in the context from the given row of data.
206+ This method should be implemented by subclasses to find the specific type of content item.
207+ """
208+ try :
209+ return self .do_find_item_from_row (row = row )
210+ except Exception as e :
211+ msg = f"[Error] Unable to find item from row { row } : { e } "
212+ self .log_info (msg = msg , type = "error" )
213+ return None
214+
215+ def create_item (self , row ):
216+ """
217+ Create the item.
218+ """
219+ try :
220+ res = self .do_create_item (row = row )
221+ except Exception as e :
222+ msg = f"[Error] Unable to create item { row } : { e } "
223+ self .log_info (msg = msg , type = "error" )
224+ return
225+ if not res :
226+ msg = f"[Error] item { row } not created."
227+ self .log_info (msg = msg , type = "error" )
228+ return
229+
230+ # adapter could create a list of items (maybe also children or related items)
231+ if isinstance (res , list ):
232+ self .n_created += len (res )
233+ for item in res :
234+ msg = f"[CREATED] { '/' .join (item .getPhysicalPath ())} "
235+ self .log_info (msg = msg )
236+ else :
237+ self .n_created += 1
238+ msg = f"[CREATED] { '/' .join (res .getPhysicalPath ())} "
239+ self .log_info (msg = msg )
240+ return res
241+
242+ def update_item (self , item , row ):
243+ """
244+ Handle update of the item.
245+ """
246+ try :
247+ res = self .do_update_item (item = item , row = row )
248+ except Exception as e :
249+ msg = f"[Error] Unable to update item { self .get_frontend_url (item )} : { e } "
250+ self .log_info (msg = msg , type = "error" )
251+ return
252+
253+ if not res :
254+ msg = f"[SKIPPED] { self .get_frontend_url (item )} "
255+ self .log_info (msg = msg )
256+ return
257+
258+ if isinstance (res , list ):
259+ self .n_updated += len (res )
260+ for updated in res :
261+ msg = f"[UPDATED] { updated .absolute_url ()} "
262+ self .log_info (msg = msg )
263+ self .sync_uids .add (updated .UID ())
264+ updated .reindexObject ()
265+ else :
266+ self .n_updated += 1
267+ msg = f"[UPDATED] { self .get_frontend_url (item )} "
268+ self .log_info (msg = msg )
269+ self .sync_uids .add (item .UID ())
270+ item .reindexObject ()
271+
272+ def delete_items (self , data ):
273+ """
274+ See if there are items to delete.
275+ """
276+ res = self .do_delete_items (data = data )
277+ if not res :
278+ return
279+ if isinstance (res , list ):
280+ self .n_todelete += len (res )
281+ for item in res :
282+ msg = f"[DELETED] { item } "
283+ self .log_info (msg = msg )
284+ else :
285+ self .n_todelete += 1
286+ msg = f"[DELETED] { res } "
287+ self .log_info (msg = msg )
288+
289+ def do_get_data (self ):
86290 """
87291 Convert the data to be used for the rsync command.
88292 Return:
89293 - data: the data to be used for the rsync command
90294 - error: an error message if there was an error, None otherwise
91295 """
92- error = None
93296 data = None
94297 # first, read source data
95- if getattr (options , "source_path" , None ):
96- file_path = Path (options .source_path )
298+ if getattr (self . options , "source_path" , None ):
299+ file_path = Path (self . options .source_path )
97300 if file_path .exists () and file_path .is_file ():
98301 with open (file_path , "r" ) as f :
99302 try :
100303 data = json .load (f )
101304 except json .JSONDecodeError :
102305 data = f .read ()
103306 else :
104- error = f"Source file not found in: { file_path } "
105- return data , error
106- elif getattr (options , "source_url" , None ):
307+ self .log_info (
308+ msg = f"Source file not found in: { file_path } " , type = "warning"
309+ )
310+ return
311+ elif getattr (self .options , "source_url" , None ):
107312 http = self .requests_retry_session (retries = 7 , timeout = 30.0 )
108- response = http .get (options .source_url )
313+ response = http .get (self . options .source_url )
109314 if response .status_code != 200 :
110- error = f"Error getting data from { options .source_url } : { response .status_code } "
111- return data , error
315+ self .log_info (
316+ msg = f"Error getting data from { self .options .source_url } : { response .status_code } " ,
317+ type = "warning" ,
318+ )
319+ return
112320 if "application/json" in response .headers .get ("Content-Type" , "" ):
113321 try :
114322 data = response .json ()
@@ -117,44 +325,27 @@ def get_data(self, options):
117325 else :
118326 data = response .content
119327
120- if data :
121- data , error = self .convert_source_data (data )
122- return data , error
328+ return self .convert_source_data (data )
123329
124- def convert_source_data (self , data ):
125- """
126- If needed, convert the source data to a format that can be used by the rsync command.
127- """
128- return data , None
330+ def do_find_item_from_row (self , row ):
331+ raise NotImplementedError ()
129332
130- def find_item_from_row (self , row ):
333+ def do_update_item (self , item , row ):
131334 """
132- Find the item in the context from the given row of data.
133- This method should be implemented by subclasses to find the specific type of content item.
335+ Update the item from the given row of data.
336+ This method should be implemented by subclasses to update the specific type of content item.
134337 """
135338 raise NotImplementedError ()
136339
137- def create_item (self , row , options ):
340+ def do_create_item (self , row ):
138341 """
139342 Create a new content item from the given row of data.
140343 This method should be implemented by subclasses to create the specific type of content item.
141344 """
142345 raise NotImplementedError ()
143346
144- def update_item (self , item , row ):
347+ def do_delete_items (self , data ):
145348 """
146- Update an existing content item from the given row of data.
147- This method should be implemented by subclasses to update the specific type of content item.
349+ Delete items
148350 """
149351 raise NotImplementedError ()
150-
151- def delete_items (self , data , sync_uids ):
152- """
153- params:
154- - data: the data to be used for the rsync command
155- - sync_uids: the uids of the items thata has been updated
156-
157- Delete items if needed.
158- This method should be implemented by subclasses to delete the specific type of content item.
159- """
160- return
0 commit comments