@@ -154,11 +154,13 @@ def handle_post_copy_action(self, files: dict) -> int:
154154 for file_name , attributes in files .items ():
155155 # Build up file path below site root
156156 file_path = f"{ attributes ['directory' ]} /{ file_name } "
157- # Get the file id and delete
158- file_id = self .get_file_id_from_path (file_path )
159- delete_url = f"https://graph.microsoft.com/v1.0/sites/{ self .site_id } /drive/items/{ file_id } "
157+ # Get the file url and delete
158+ file_url = self .get_file_url_from_path (file_path )
159+ if not file_url :
160+ self .logger .error (f"Failed to get file URL for { file_path } " )
161+ return 1
160162 response = requests .delete (
161- delete_url ,
163+ file_url ,
162164 headers = {
163165 "Authorization" : "Bearer " + self .credentials ["access_token" ],
164166 },
@@ -177,7 +179,11 @@ def handle_post_copy_action(self, files: dict) -> int:
177179 for file_name , attributes in files .items ():
178180 # Build up file path below site root
179181 file_path = f"{ attributes ['directory' ]} /{ file_name } "
180- file_id = self .get_file_id_from_path (file_path )
182+ file_url = self .get_file_url_from_path (file_path )
183+ if not file_url :
184+ self .logger .error (f"Failed to get file URL for { file_path } " )
185+ return 1
186+
181187 new_file = f"{ file_name .split ('/' )[- 1 ]} "
182188
183189 # getting the archiving path
@@ -199,19 +205,57 @@ def handle_post_copy_action(self, files: dict) -> int:
199205 self .spec ["postCopyAction" ]["sub" ],
200206 file_name ,
201207 )
202- update_url = f"https://graph.microsoft.com/v1.0/sites/{ self .site_id } /drive/items/{ file_id } "
208+ patch_body = {
209+ "parentReference" : {"id" : f"{ destination_id } " },
210+ "name" : f"{ new_file } " ,
211+ }
212+ patch_headers = {
213+ "Authorization" : "Bearer " + self .credentials ["access_token" ],
214+ "Content-Type" : "application/json" ,
215+ }
203216 response = requests .patch (
204- update_url ,
205- headers = {
206- "Authorization" : "Bearer " + self .credentials ["access_token" ],
207- "Content-Type" : "application/json" ,
208- },
217+ file_url ,
218+ headers = patch_headers ,
209219 timeout = 60 ,
210- json = {
211- "parentReference" : {"id" : f"{ destination_id } " },
212- "name" : f"{ new_file } " ,
213- },
220+ json = patch_body ,
214221 )
222+ if response .status_code == 409 :
223+ # Target already exists - delete it and retry (unix-style overwrite)
224+ self .logger .info (
225+ f"Destination file { new_file } already exists, overwriting"
226+ )
227+ conflict_url = self .get_file_url_from_path (
228+ f"{ destination_path } /{ new_file } "
229+ )
230+ if not conflict_url :
231+ self .logger .error (
232+ f"Failed to get file URL for { destination_path } /{ new_file } "
233+ )
234+ return 1
235+ response = requests .delete (
236+ conflict_url ,
237+ headers = {
238+ "Authorization" : (
239+ "Bearer " + self .credentials ["access_token" ]
240+ ),
241+ },
242+ timeout = 60 ,
243+ )
244+ # Check the response was a success
245+ if response .status_code != 204 :
246+ self .logger .error (
247+ f"Failed to delete conflicting file: { new_file } "
248+ )
249+ self .logger .error (f"Got return code: { response .status_code } " )
250+ self .logger .error (response .json ())
251+ return 1
252+
253+ response = requests .patch (
254+ file_url ,
255+ headers = patch_headers ,
256+ timeout = 60 ,
257+ json = patch_body ,
258+ )
215259 if response .status_code != 200 :
216260 self .logger .error (f"Failed to move file: { file_name } " )
217261 self .logger .error (f"Got return code: { response .status_code } " )
@@ -233,13 +277,35 @@ def create_or_get_folder(self, destination_path: str) -> str | None:
233277 for folder in folders :
234278 # build the path depending on if parent exists
235279 current_path = f"{ current_parent } /{ folder } " if current_parent else folder
236- # get folder id from current path
237- folder_id = self .get_file_id_from_path (current_path )
280+ # get folder url from current path
281+ folder_url = self .get_file_url_from_path (current_path )
238282
239- # if folder doesn't exist, create it; else, update parent details
240- if folder_id is None :
283+ # if folder doesn't exist, create it and get the actual item ID back
284+ if folder_url is None :
241285 self .logger .info (f"Folder { folder } does not exist, creating" )
242286 folder_id = self .create_folder (parent_id , folder )
287+ else :
288+ # get_file_url_from_path returns a path-based URL, not a drive item ID;
289+ # resolve it to the actual item ID so it can be used in parentReference.id
290+ response = requests .get (
291+ folder_url ,
292+ headers = {
293+ "Authorization" : "Bearer " + self .credentials ["access_token" ],
294+ },
295+ timeout = 60 ,
296+ )
297+ if response .status_code == 404 :
298+ self .logger .info (f"Folder { folder } does not exist, creating" )
299+ folder_id = self .create_folder (parent_id , folder )
300+ elif response .status_code == 200 :
301+ folder_id = response .json ()["id" ]
302+ else :
303+ self .logger .error (f"Failed to resolve folder: { current_path } " )
304+ self .logger .error (response .json ())
305+ raise RemoteTransferError (
306+ f"Failed to resolve folder: { current_path } "
307+ )
308+
243309 # updating parent info for the next folder in sequence
244310 current_parent = current_path
245311 parent_id = folder_id
@@ -365,6 +431,10 @@ def push_files_from_worker(
365431 for file in files :
366432 # Strip the directory from the file
367433 file_name = file .split ("/" )[- 1 ]
434+ file_url = self .get_file_url_from_path (
435+ self .spec ["directory" ] + "/" + file_name
436+ )
437+
368438 # Handle any rename that might be specified in the spec
369439 if "rename" in self .spec :
370440 rename_regex = self .spec ["rename" ]["pattern" ]
@@ -391,7 +461,8 @@ def push_files_from_worker(
391461 continue
392462
393463 # Otherwise do a normal upload
394- upload_url = f"https://graph.microsoft.com/v1.0/sites/{ self .site_id } /drive/root:/{ file_name } :/content"
464+ upload_url = f"{ file_url } :/content"
465+ self .logger .info (f"Using upload url: { upload_url } " )
395466 with open (file , "rb" ) as f :
396467 max_retries = 5
397468 retry_delay = 1
@@ -450,18 +521,20 @@ def _do_upload_session(self, file: str, file_name: str) -> int:
450521 # 3. Trigger the appropriate URI endpoint to upload the file
451522
452523 # Determine if the file already exists
453- file_id = self .get_file_id_from_path (file_name )
454- if file_id is None :
524+ file_url = self .get_file_url_from_path (file_name )
525+ if file_url is None :
455526 self .logger .info (f"File { file_name } does not already exist." )
456- # Get the parent item id , using the dirname of the file
457- parent_folder_id = self .get_file_id_from_path (path .dirname (file_name ))
527+ # Get the parent item url , using the dirname of the file
528+ parent_folder_url = self .get_file_url_from_path (path .dirname (file_name ))
458529 file_name_basename = path .basename (file_name )
459530
460- upload_session_url = f"https://graph.microsoft.com/v1.0/sites/{ self .site_id } /drive/items/{ parent_folder_id } :/{ file_name_basename } :/createUploadSession"
531+ upload_session_url = (
532+ f"{ parent_folder_url } :/{ file_name_basename } :/createUploadSession"
533+ )
461534
462535 else :
463536 self .logger .info (f"File { file_name } already exists. Replacing file." )
464- upload_session_url = f"https://graph.microsoft.com/v1.0/sites/ { self . site_id } /drive/items/ { file_id } /createUploadSession"
537+ upload_session_url = f"{ file_url } : /createUploadSession"
465538
466539 response = requests .post (
467540 upload_session_url ,
@@ -531,10 +604,11 @@ def _do_upload_session(self, file: str, file_name: str) -> int:
531604
532605 # If it's a 201, then we are done with the upload
533606 if response .status_code == 201 or (
534- response .status_code == 200 and file_id is not None
607+ response .status_code == 200 and file_url is not None
535608 ):
536- file_id = response .json ()["id" ]
537- self .logger .info (f"Successfully uploaded file. File ID: { file_id } " )
609+ self .logger .info (
610+ f"Successfully uploaded file. File ID: { response .json ()['id' ]} "
611+ )
538612 else :
539613 self .logger .info (f"Uploaded chunk { i + 1 } of { num_chunks } " )
540614
@@ -564,11 +638,11 @@ def pull_files_to_worker(self, files: dict, local_staging_directory: str) -> int
564638 file_path = f"{ attributes ['directory' ]} /{ file_name } "
565639
566640 try :
567- # Get the item id based on source path
568- file_id = self .get_file_id_from_path (file_path )
569- # Download file using item id
641+ # Get the item url based on source path
642+ file_url = self .get_file_url_from_path (file_path )
643+ # Download file using item url
570644 self .logger .info (f"Downloading file: { file_name } " )
571- download_url = f"https://graph.microsoft.com/v1.0/sites/ { self . site_id } /drive/items/ { file_id } /content"
645+ download_url = f"{ file_url } : /content"
572646 response = requests .get (
573647 download_url ,
574648 headers = {
@@ -611,14 +685,58 @@ def create_flag_files(self) -> int:
611685 def tidy (self ) -> None :
612686 """Nothing to tidy."""
613687
614- def get_file_id_from_path (self , file_path : str ) -> str | None :
688+ def get_file_url_from_path (self , file_path : str ) -> str | None :
615689 """Returns the id for a sharepoint drive item from the path."""
616690 if file_path == "" : # We are dealing with the root folder
617691 item_url = (
618692 f"https://graph.microsoft.com/v1.0/sites/{ self .site_id } /drive/root"
619693 )
620694 else :
621- item_url = f"https://graph.microsoft.com/v1.0/sites/{ self .site_id } /drive/root:/{ file_path } "
695+ parts = file_path .split ("/" )
696+ file_name = parts [- 1 ]
697+ # Remove the filename from the path
698+ o365_file_path = "/" .join (parts [:- 1 ])
699+
700+ if o365_file_path .startswith ("/" ):
701+ path_parts = o365_file_path .split ("/" )
702+ # Get the first part of the path, which is the document library name
703+ library_name = path_parts [1 ]
704+ # file path then needs to be the rest of the path
705+ o365_file_path = "/" .join (path_parts [2 :])
706+
707+ # If the path starts with a / then it's a document library, we need to get the id of the document library
708+ # Do a GET request to /sites/{siteId}/drives to get the document libraries
709+ response = requests .get (
710+ f"https://graph.microsoft.com/v1.0/sites/{ self .site_id } /drives" ,
711+ headers = {
712+ "Authorization" : "Bearer " + self .credentials ["access_token" ],
713+ },
714+ timeout = 60 ,
715+ )
716+ if response .status_code != 200 :
717+ self .logger .error ("Failed to get document libraries" )
718+ self .logger .error (response .json ())
719+ raise RemoteTransferError ("Failed to get document libraries" )
720+
721+ document_libraries = response .json ()["value" ]
722+
723+ for document_library in document_libraries :
724+ if document_library ["name" ] == library_name :
725+ item_path = (
726+ f"{ o365_file_path } /{ file_name } "
727+ if o365_file_path
728+ else file_name
729+ )
730+ return f"https://graph.microsoft.com/v1.0/sites/{ self .site_id } /drives/{ document_library ['id' ]} /root:/{ item_path } "
731+
732+ self .logger .error (
733+ f"Failed to find document library with name { library_name } "
734+ )
735+ raise RemoteTransferError (
736+ f"Failed to find Document Library named { library_name } "
737+ )
738+
739+ return f"https://graph.microsoft.com/v1.0/sites/{ self .site_id } /drive/root:/{ re .sub (r'/+' , '/' , file_path )} "
622740
623741 response = requests .get (
624742 item_url ,
0 commit comments