From cb3c2b689529a4514d9b712f805ab3dbe25bf453 Mon Sep 17 00:00:00 2001 From: abraunegg Date: Sat, 23 Nov 2024 15:48:17 +1100 Subject: [PATCH] Implement Feature Request #2803: Support Permanent Delete on OneDrive * Initial work on developing feature request --- src/config.d | 3 ++ src/onedrive.d | 18 +++++++++++ src/sync.d | 81 +++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/config.d b/src/config.d index 95e7e30d2..6bfa44d9f 100644 --- a/src/config.d +++ b/src/config.d @@ -332,6 +332,8 @@ class ApplicationConfig { boolValues["read_only_auth_scope"] = false; // - Flag to cleanup local files when using --download-only boolValues["cleanup_local_files"] = false; + // - Perform a permanentDelete on deletion activities + boolValues["permanent_delete"] = false; // Webhook Feature Options boolValues["webhook_enabled"] = false; @@ -1411,6 +1413,7 @@ class ApplicationConfig { addLogEntry("Config option 'sync_dir_permissions' = " ~ to!string(getValueLong("sync_dir_permissions"))); addLogEntry("Config option 'sync_file_permissions' = " ~ to!string(getValueLong("sync_file_permissions"))); addLogEntry("Config option 'space_reservation' = " ~ to!string(getValueLong("space_reservation"))); + addLogEntry("Config option 'permanent_delete' = " ~ to!string(getValueBool("permanent_delete"))); // curl operations addLogEntry("Config option 'application_id' = " ~ getValueString("application_id")); diff --git a/src/onedrive.d b/src/onedrive.d index 02429a562..20e3034ee 100644 --- a/src/onedrive.d +++ b/src/onedrive.d @@ -639,6 +639,16 @@ class OneDriveApi { performDelete(url); } + // https://learn.microsoft.com/en-us/graph/api/driveitem-permanentdelete?view=graph-rest-1.0 + void permanentDeleteById(const(char)[] driveId, const(char)[] id, const(char)[] eTag = null) { + // string[string] requestHeaders; + const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/permanentDelete"; + //TODO: investigate why this always fail with 412 (Precondition Failed) + // if (eTag) requestHeaders["If-Match"] = eTag; + // as per documentation, a permanentDelete needs to be a HTTP POST + performPermanentDelete(url); + } + // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children JSONValue createById(string parentDriveId, string parentId, JSONValue item) { string url = driveByIdUrl ~ parentDriveId ~ "/items/" ~ parentId ~ "/children"; @@ -971,6 +981,14 @@ class OneDriveApi { }, validateJSONResponse, callingFunction, lineno); } + private void performPermanentDelete(const(char)[] url, string[string] requestHeaders=null, string callingFunction=__FUNCTION__, int lineno=__LINE__) { + bool validateJSONResponse = false; + oneDriveErrorHandlerWrapper((CurlResponse response) { + connect(HTTP.Method.post, url, false, response, requestHeaders); + return curlEngine.execute(); + }, validateJSONResponse, callingFunction, lineno); + } + private void downloadFile(const(char)[] url, string filename, long fileSize, string callingFunction=__FUNCTION__, int lineno=__LINE__) { // Threshold for displaying download bar long thresholdFileSize = 4 * 2^^20; // 4 MiB diff --git a/src/sync.d b/src/sync.d index 858cf302f..b96aadba6 100644 --- a/src/sync.d +++ b/src/sync.d @@ -170,6 +170,8 @@ class SyncEngine { // Is bypass_data_preservation set via config file // Local data loss MAY occur in this scenario bool bypassDataPreservation = false; + // Has the user configured to permanently delete files online rather than send to online recycle bind + bool permanentDelete = false; // Maximum file size upload // https://support.microsoft.com/en-us/office/invalid-file-names-and-file-types-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa?ui=en-us&rs=en-us&ad=us // July 2020, maximum file size for all accounts is 100GB @@ -307,8 +309,10 @@ class SyncEngine { // Are we forcing the client to bypass any data preservation techniques to NOT rename any local files if there is a conflict? // The enabling of this function could lead to data loss if (appConfig.getValueBool("bypass_data_preservation")) { + addLogEntry(); addLogEntry("WARNING: Application has been configured to bypass local data preservation in the event of file conflict."); addLogEntry("WARNING: Local data loss MAY occur in this scenario."); + addLogEntry(); this.bypassDataPreservation = true; } @@ -405,6 +409,46 @@ class SyncEngine { forceExit(); } + // Has the client been configured to permanently delete files online rather than send these to the online recycle bin? + if (appConfig.getValueBool("permanent_delete")) { + // This can only be set if not using: + // - US Government L4 + // - US Government L5 (DOD) + // - China operated by 21Vianet + // + // Additionally, this is not supported by OneDrive Personal accounts: + // + // This is a doc bug. In fact, OneDrive personal accounts do not support the permanentDelete API, it only applies to OneDrive for Business and SharePoint document libraries. + // + // Reference: https://learn.microsoft.com/en-us/answers/questions/1501170/onedrive-permanently-delete-a-file + string azureConfigValue = appConfig.getValueString("azure_ad_endpoint"); + + // Now that we know the 'accountType' we can configure this correctly + if ((appConfig.accountType != "personal") && (azureConfigValue.empty || azureConfigValue == "DE")) { + // Only supported for Global Service and DE based on https://learn.microsoft.com/en-us/graph/api/driveitem-permanentdelete?view=graph-rest-1.0 + addLogEntry(); + addLogEntry("WARNING: Application has been configured to permanently remove files online rather than send to the recycle bin. Permanently deleted items can't be restored."); + addLogEntry("WARNING: Online data loss MAY occur in this scenario."); + addLogEntry(); + this.permanentDelete = true; + } else { + // what error message do we present + if (appConfig.accountType == "personal") { + // personal account type - API not supported + addLogEntry(); + addLogEntry("WARNING: The application is configured to permanently delete files online; however, this action is not supported by Microsoft OneDrive Personal Accounts."); + addLogEntry(); + } else { + // Not a personal account + addLogEntry(); + addLogEntry("WARNING: The application is configured to permanently delete files online; however, this action is not supported by the National Cloud Deployment in use."); + addLogEntry(); + } + // ensure this is false regardless + this.permanentDelete = false; + } + } + // API was initialised if (verboseLogging) {addLogEntry("Sync Engine Initialised with new Onedrive API instance", ["verbose"]);} return true; @@ -6596,8 +6640,13 @@ class SyncEngine { uploadDeletedItemOneDriveApiInstance = new OneDriveApi(appConfig); uploadDeletedItemOneDriveApiInstance.initialise(); - // Perform the delete via the default OneDrive API instance - uploadDeletedItemOneDriveApiInstance.deleteById(actualItemToDelete.driveId, actualItemToDelete.id); + if (!permanentDelete) { + // Perform the delete via the default OneDrive API instance + uploadDeletedItemOneDriveApiInstance.deleteById(actualItemToDelete.driveId, actualItemToDelete.id); + } else { + // Perform the permanent delete via the default OneDrive API instance + uploadDeletedItemOneDriveApiInstance.permanentDeleteById(actualItemToDelete.driveId, actualItemToDelete.id); + } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadDeletedItemOneDriveApiInstance.releaseCurlEngine(); @@ -6664,16 +6713,27 @@ class SyncEngine { // Log the action if (debugLogging) {addLogEntry("Attempting to delete this child item id: " ~ child.id ~ " from drive: " ~ child.driveId, ["debug"]);} - // perform the delete via the default OneDrive API instance - performReverseDeletionOneDriveApiInstance.deleteById(child.driveId, child.id, child.eTag); + if (!permanentDelete) { + // Perform the delete via the default OneDrive API instance + performReverseDeletionOneDriveApiInstance.deleteById(child.driveId, child.id, child.eTag); + } else { + // Perform the permanent delete via the default OneDrive API instance + performReverseDeletionOneDriveApiInstance.permanentDeleteById(child.driveId, child.id, child.eTag); + } + // delete the child reference in the local database itemDB.deleteById(child.driveId, child.id); } // Log the action if (debugLogging) {addLogEntry("Attempting to delete this parent item id: " ~ itemToDelete.id ~ " from drive: " ~ itemToDelete.driveId, ["debug"]);} - // Perform the delete via the default OneDrive API instance - performReverseDeletionOneDriveApiInstance.deleteById(itemToDelete.driveId, itemToDelete.id, itemToDelete.eTag); + if (!permanentDelete) { + // Perform the delete via the default OneDrive API instance + performReverseDeletionOneDriveApiInstance.deleteById(itemToDelete.driveId, itemToDelete.id, itemToDelete.eTag); + } else { + // Perform the permanent delete via the default OneDrive API instance + performReverseDeletionOneDriveApiInstance.permanentDeleteById(itemToDelete.driveId, itemToDelete.id, itemToDelete.eTag); + } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory performReverseDeletionOneDriveApiInstance.releaseCurlEngine(); @@ -7662,8 +7722,13 @@ class SyncEngine { // Try the online deletion try { - // Perform the delete via the default OneDrive API instance - deleteByPathNoSyncAPIInstance.deleteById(deletionItem.driveId, deletionItem.id); + if (!permanentDelete) { + // Perform the delete via the default OneDrive API instance + deleteByPathNoSyncAPIInstance.deleteById(deletionItem.driveId, deletionItem.id); + } else { + // Perform the permanent delete via the default OneDrive API instance + deleteByPathNoSyncAPIInstance.permanentDeleteById(deletionItem.driveId, deletionItem.id); + } // If we get here without error, directory was deleted addLogEntry("The requested directory to delete online has been deleted"); } catch (OneDriveException exception) {