Skip to content

Commit

Permalink
Implement Feature Request: Support Permanent Delete on OneDrive (#2999)
Browse files Browse the repository at this point in the history
* Implement Feature Request: Support Permanent Delete on OneDrive (#2803)
  • Loading branch information
abraunegg authored Nov 26, 2024
1 parent e5de5a2 commit 228e7db
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 8 deletions.
33 changes: 33 additions & 0 deletions docs/application-config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Before reading this document, please ensure you are running application version
- [no_remote_delete](#no_remote_delete)
- [notify_file_actions](#notify_file_actions)
- [operation_timeout](#operation_timeout)
- [permanent_delete](#permanent_delete)
- [rate_limit](#rate_limit)
- [read_only_auth_scope](#read_only_auth_scope)
- [remove_source_files](#remove_source_files)
Expand Down Expand Up @@ -505,6 +506,38 @@ _**Default Value:**_ 3600

_**Config Example:**_ `operation_timeout = "3600"`

### permanent_delete
_**Description:**_ Permanently delete an item online when it is removed locally. When using this method, they're permanently removed and aren't sent to the Microsoft OneDrive Recycle Bin. Therefore, permanently deleted drive items can't be restored afterward. Online data loss MAY occur in this scenario.

_**Value Type:**_ Boolean

_**Default Value:**_ False

_**Config Example:**_ `permanent_delete = "true"`

> [!IMPORTANT]
> The Microsoft OneDrive API for this capability is also very narrow:
> | Account Type | Config Option is Supported |
> |--------------|------------------|
> | Personal ||
> | Business ||
> | SharePoint ||
> | Microsoft Cloud Germany ||
> | Microsoft Cloud for US Government ||
> | Azure and Office365 operated by VNET in China ||
>
> When using this config option against an unsupported Personal Accounts the following message will be generated:
> ```
> WARNING: The application is configured to permanently delete files online; however, this action is not supported by Microsoft OneDrive Personal Accounts.
> ```
>
> When using this config option against a supported account the following message will be generated:
> ```
> WARNING: Application has been configured to permanently remove files online rather than send to the recycle bin. Permanently deleted items can't be restored.
> WARNING: Online data loss MAY occur in this scenario.
> ```
>
### rate_limit
_**Description:**_ This configuration option controls the bandwidth used by the application, per thread, when interacting with Microsoft OneDrive.

Expand Down
3 changes: 3 additions & 0 deletions src/config.d
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
Expand Down
18 changes: 18 additions & 0 deletions src/onedrive.d
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,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";
Expand Down Expand Up @@ -962,6 +972,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
Expand Down
81 changes: 73 additions & 8 deletions src/sync.d
Original file line number Diff line number Diff line change
Expand Up @@ -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 bin
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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -408,6 +412,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)
// - Azure and Office365 operated by VNET in China
//
// 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;
Expand Down Expand Up @@ -6668,8 +6712,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();
Expand Down Expand Up @@ -6736,16 +6785,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();
Expand Down Expand Up @@ -7734,8 +7794,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) {
Expand Down

0 comments on commit 228e7db

Please sign in to comment.