mirror of
https://github.com/abraunegg/onedrive
synced 2026-03-14 14:35:46 +01:00
* This commit implements Feature Request #3142 by introducing a configurable local recycle bin or trash folder mechanism within the OneDrive Client for Linux. This enhancement provides users with a safeguard against unintended data loss during synchronisation operations. This capability is designed to be compatible with the FreeDesktop.org Trash Specification.
This commit is contained in:
parent
68ae5f0b99
commit
898f350336
7 changed files with 402 additions and 72 deletions
1
.github/actions/spelling/allow.txt
vendored
1
.github/actions/spelling/allow.txt
vendored
|
|
@ -383,6 +383,7 @@ thnk
|
|||
tidx
|
||||
timerfd
|
||||
tlsv
|
||||
trashinfo
|
||||
Tting
|
||||
typecons
|
||||
uda
|
||||
|
|
|
|||
6
config
6
config
|
|
@ -115,6 +115,9 @@
|
|||
## This configuration option controls whether the client operates in read-only mode.
|
||||
#read_only_auth_scope = "false"
|
||||
|
||||
## This configuration option allows you to specify the 'Recycle Bin' path for the application. This is only used if 'use_recycle_bin' is enabled.
|
||||
#recycle_bin_path = "/path/to/desired/location/"
|
||||
|
||||
## This option removes the local file after a successful upload to OneDrive.
|
||||
#remove_source_files = "false"
|
||||
|
||||
|
|
@ -169,6 +172,9 @@
|
|||
## Only upload changes to OneDrive, do not download from cloud.
|
||||
#upload_only = "false"
|
||||
|
||||
## This configuration option controls the application function to move online deleted files to a 'Recycle Bin' on your system.
|
||||
#use_recycle_bin = "false"
|
||||
|
||||
## Custom User-Agent string for requests to OneDrive. If you change this, you will get throttled by the Microsoft Graph API. Change with caution.
|
||||
#user_agent = "ISV|abraunegg|OneDrive Client for Linux/vX.Y.Z-A-bcdefghi"
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ Before reading this document, please ensure you are running application version
|
|||
- [permanent_delete](#permanent_delete)
|
||||
- [rate_limit](#rate_limit)
|
||||
- [read_only_auth_scope](#read_only_auth_scope)
|
||||
- [recycle_bin_path](#recycle_bin_path)
|
||||
- [remove_source_files](#remove_source_files)
|
||||
- [resync](#resync)
|
||||
- [resync_auth](#resync_auth)
|
||||
|
|
@ -60,6 +61,7 @@ Before reading this document, please ensure you are running application version
|
|||
- [threads](#threads)
|
||||
- [transfer_order](#transfer_order)
|
||||
- [upload_only](#upload_only)
|
||||
- [use_recycle_bin](#use_recycle_bin)
|
||||
- [user_agent](#user_agent)
|
||||
- [webhook_enabled](#webhook_enabled)
|
||||
- [webhook_expiration_interval](#webhook_expiration_interval)
|
||||
|
|
@ -624,6 +626,17 @@ _**Config Example:**_ `read_only_auth_scope = "false"` or `read_only_auth_scope
|
|||
> [!IMPORTANT]
|
||||
> When using 'read_only_auth_scope' you also will need to remove your existing application access consent otherwise old authentication consent will be valid and will be used. This will mean the application will technically have the consent to upload data until you revoke this consent.
|
||||
|
||||
### recycle_bin_path
|
||||
_**Description:**_ This configuration option allows you to specify the 'Recycle Bin' path for the application.
|
||||
|
||||
_**Value Type:**_ String
|
||||
|
||||
_**Default Value:**_ *None* however the application will use `~/.local/share/Trash` as the pre-defined default so that files will be placed in the correct location for your user profile.
|
||||
|
||||
_**CLI Option Use:**_ *None - this is a config file option only*
|
||||
|
||||
_**Config Example:**_ `recycle_bin_path = "/path/to/desired/location/"`
|
||||
|
||||
### remove_source_files
|
||||
_**Description:**_ This configuration option controls whether the OneDrive Client for Linux removes the local file post successful transfer to Microsoft OneDrive.
|
||||
|
||||
|
|
@ -942,6 +955,17 @@ _**CLI Option Use:**_ `--upload-only`
|
|||
> [!IMPORTANT]
|
||||
> To ensure that data deleted locally remains accessible online, you can use the 'no_remote_delete' option. If you want to delete the data from your local storage after a successful upload to Microsoft OneDrive, you can use the 'remove_source_files' option.
|
||||
|
||||
### use_recycle_bin
|
||||
_**Description:**_ This configuration option controls the application function to move online deleted files to a 'Recycle Bin' on your system. This allows you to review online deleted data manually before this is purged from your actual system.
|
||||
|
||||
_**Value Type:**_ Boolean
|
||||
|
||||
_**Default Value:**_ False
|
||||
|
||||
_**Config Example:**_ `use_recycle_bin = "false"` or `use_recycle_bin = "true"`
|
||||
|
||||
_**CLI Option Use:**_ *None - this is a config file option only*
|
||||
|
||||
### user_agent
|
||||
_**Description:**_ This configuration option controls the 'User-Agent' request header that is presented to Microsoft Graph API when accessing the Microsoft OneDrive service. This string lets servers and network peers identify the application, operating system, vendor, and/or version of the application making the request. We recommend users not to tamper with this option unless strictly necessary.
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ Before reading this document, please ensure you are running application version
|
|||
- [Enabling the Client Activity Log](#enabling-the-client-activity-log)
|
||||
- [Client Activity Log Example:](#client-activity-log-example)
|
||||
- [Client Activity Log Differences](#client-activity-log-differences)
|
||||
- [Using a local Recycle Bin](#using-a-local-recycle-bin)
|
||||
- [GUI Notifications](#gui-notifications)
|
||||
- [Handling a Microsoft OneDrive Account Password Change](#handling-a-microsoft-onedrive-account-password-change)
|
||||
- [Determining the synchronisation result](#determining-the-synchronisation-result)
|
||||
|
|
@ -739,6 +740,31 @@ Using 'user' configuration path for application state data: /home/user/.config/o
|
|||
Using the following path to store the runtime application log: /var/log/onedrive
|
||||
```
|
||||
|
||||
### Using a local Recycle Bin
|
||||
By default, this application will process online deletions and directly delete the corresponding file or folder directly from your configured 'sync_dir'.
|
||||
|
||||
In some cases, it may actually be desirable to move these files to your Linux user default 'Recycle Bin', so that you can manually delete the files at your own discretion.
|
||||
|
||||
To enable this application functionality, add the following to your 'config' file:
|
||||
```
|
||||
use_recycle_bin = "true"
|
||||
```
|
||||
|
||||
This capability is designed to be compatible with the [FreeDesktop.org Trash Specification](https://specifications.freedesktop.org/trash-spec/1.0/), ensuring interoperability with GUI-based desktop environments such as GNOME (GIO) and KDE (KIO). It follows the required structure by:
|
||||
* Moving deleted files and directories to `~/.local/share/Trash/files/`
|
||||
* Creating matching metadata files in `~/.local/share/Trash/info/` with the correct `.trashinfo` format, including the original absolute path and ISO 8601-formatted deletion timestamp
|
||||
* Resolving filename collisions using a `name.N.ext` pattern (e.g., `Document.2.docx`), consistent with GNOME and KDE behaviour.
|
||||
|
||||
|
||||
|
||||
To specify an explicit 'Recycle Bin' directory, add the following to your 'config' file:
|
||||
```
|
||||
recycle_bin_path = "/path/to/desired/location/"
|
||||
```
|
||||
|
||||
The same FreeDesktop.org Trash Specification will be used with this explicit 'Recycle Bin' directory.
|
||||
|
||||
|
||||
### GUI Notifications
|
||||
To enable GUI notifications, you must compile the application with GUI Notification Support. Refer to [GUI Notification Support](install.md#gui-notification-support) for details. Once compiled, GUI notifications will work by default in the display manager session under the following conditions:
|
||||
|
||||
|
|
@ -773,6 +799,11 @@ To enable these specific notifications, add the following to your 'config' file:
|
|||
notify_file_actions = "true"
|
||||
```
|
||||
|
||||
To disable *all* GUI notifications, add the following to your 'config' file:
|
||||
```
|
||||
disable_notifications = "true"
|
||||
```
|
||||
|
||||
### Handling a Microsoft OneDrive Account Password Change
|
||||
If you change your Microsoft OneDrive Account Password, the client will no longer be authorised to sync, and will generate the following error upon next application run:
|
||||
```text
|
||||
|
|
|
|||
75
src/config.d
75
src/config.d
|
|
@ -179,6 +179,7 @@ class ApplicationConfig {
|
|||
// Store items that come in from the 'config' file, otherwise these need to be set the defaults
|
||||
private string configFileSyncDir = defaultSyncDir;
|
||||
private string configFileSkipFile = ""; // Default for now, if post reading in any user configuration, if still empty, default will be used
|
||||
private bool configFileSkipFileReadIn = false; // If we actually read in something from 'config' file, this gets set to true
|
||||
private string configFileSkipDir = ""; // Default here is no directories are skipped
|
||||
private string configFileDriveId = ""; // Default here is that no drive id is specified
|
||||
private bool configFileSkipDotfiles = false;
|
||||
|
|
@ -199,6 +200,11 @@ class ApplicationConfig {
|
|||
bool xdg_exists = false;
|
||||
bool dbus_exists = false;
|
||||
|
||||
// Recycle Bin Configuration
|
||||
// These paths are used by the application, if 'use_recycle_bin' is enabled
|
||||
string recycleBinFilePath;
|
||||
string recycleBinInfoPath;
|
||||
|
||||
// Initialise the application configuration
|
||||
bool initialise(string confdirOption, bool helpRequested) {
|
||||
|
||||
|
|
@ -242,6 +248,12 @@ class ApplicationConfig {
|
|||
// - name_dsc = file name descending
|
||||
stringValues["transfer_order"] = "default";
|
||||
|
||||
// Recycle Bin Configuration
|
||||
// Enable|Disable feature
|
||||
boolValues["use_recycle_bin"] = false;
|
||||
// Recycle Bin Folder - empty string as a default
|
||||
stringValues["recycle_bin_path"] = "";
|
||||
|
||||
// - Store how many times was --verbose added
|
||||
longValues["verbose"] = verbosityCount;
|
||||
// - The amount of time (seconds) between monitor sync loops
|
||||
|
|
@ -395,9 +407,16 @@ class ApplicationConfig {
|
|||
}
|
||||
}
|
||||
|
||||
// outcome of setting defaultHomePath
|
||||
// Outcome of setting 'defaultHomePath'
|
||||
if (debugLogging) {addLogEntry("runtime_environment: Calculated defaultHomePath: " ~ defaultHomePath, ["debug"]);}
|
||||
|
||||
// Configure the default path for the Recycle Bin
|
||||
// Both GNOME and KDE use '~/.local/share/Trash/' as the default path
|
||||
// ~/.local/share/Trash/
|
||||
// ├── files/ # The actual trashed files
|
||||
// └── info/ # .trashinfo metadata about each file (original path, deletion date)
|
||||
setValueString("recycle_bin_path", defaultHomePath ~ "/.local/share/Trash/");
|
||||
|
||||
// DEVELOPER OPTIONS
|
||||
// display_memory = true | false
|
||||
// - It may be desirable to display the memory usage of the application to assist with diagnosing memory issues with the application
|
||||
|
|
@ -803,6 +822,8 @@ class ApplicationConfig {
|
|||
forceExit();
|
||||
}
|
||||
} else if (key == "skip_file") {
|
||||
// Flag this as true
|
||||
configFileSkipFileReadIn = true;
|
||||
// Handle multiple 'config' file entries of skip_file
|
||||
if (configFileSkipFile.empty) {
|
||||
// currently no entry exists
|
||||
|
|
@ -1154,8 +1175,8 @@ class ApplicationConfig {
|
|||
"Specify the local directory used for synchronisation to OneDrive",
|
||||
&stringValues["sync_dir_cli"],
|
||||
"share-password",
|
||||
"Require a password to access the shared link when used with --create-share-link <file>",
|
||||
&stringValues["share_password"],
|
||||
"Require a password to access the shared link when used with --create-share-link <file>",
|
||||
&stringValues["share_password"],
|
||||
"sync|s",
|
||||
"Perform a synchronisation with Microsoft OneDrive",
|
||||
&boolValues["synchronize"],
|
||||
|
|
@ -1445,7 +1466,7 @@ class ApplicationConfig {
|
|||
addLogEntry("Config option 'cleanup_local_files' = " ~ to!string(getValueBool("cleanup_local_files")));
|
||||
addLogEntry("Config option 'disable_permission_set' = " ~ to!string(getValueBool("disable_permission_set")));
|
||||
addLogEntry("Config option 'transfer_order' = " ~ getValueString("transfer_order"));
|
||||
|
||||
|
||||
// data integrity
|
||||
addLogEntry("Config option 'classify_as_big_delete' = " ~ to!string(getValueLong("classify_as_big_delete")));
|
||||
addLogEntry("Config option 'disable_upload_validation' = " ~ to!string(getValueBool("disable_upload_validation")));
|
||||
|
|
@ -1484,6 +1505,10 @@ class ApplicationConfig {
|
|||
addLogEntry("Compile time option --enable-notifications = false");
|
||||
}
|
||||
|
||||
// Recycle Bin
|
||||
addLogEntry("Config option 'use_recycle_bin' = " ~ to!string(getValueBool("use_recycle_bin")));
|
||||
addLogEntry("Config option 'recycle_bin_path' = " ~ getValueString("recycle_bin_path"));
|
||||
|
||||
// Is sync_list configured and contains entries?
|
||||
if (exists(syncListFilePath) && getSize(syncListFilePath) > 0) {
|
||||
addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
|
||||
|
|
@ -1750,8 +1775,8 @@ class ApplicationConfig {
|
|||
skip_file_present = true;
|
||||
// Handle multiple entries of skip_file
|
||||
if (backupConfigFileSkipFile.empty) {
|
||||
// currently no entry exists, include 'defaultSkipFile' entries
|
||||
backupConfigFileSkipFile = defaultSkipFile ~ "|" ~ to!string(c.front.dup);
|
||||
// currently no entry value exists
|
||||
backupConfigFileSkipFile = to!string(c.front.dup);
|
||||
} else {
|
||||
// add to existing backupConfigFileSkipFile entry
|
||||
backupConfigFileSkipFile = backupConfigFileSkipFile ~ "|" ~ to!string(c.front.dup);
|
||||
|
|
@ -1763,8 +1788,8 @@ class ApplicationConfig {
|
|||
skip_dir_present = true;
|
||||
// Handle multiple entries of skip_dir
|
||||
if (backupConfigFileSkipDir.empty) {
|
||||
// currently no entry exists
|
||||
backupConfigFileSkipDir = c.front.dup;
|
||||
// currently no entry value exists
|
||||
backupConfigFileSkipDir = to!string(c.front.dup);
|
||||
} else {
|
||||
// add to existing backupConfigFileSkipDir entry
|
||||
backupConfigFileSkipDir = backupConfigFileSkipDir ~ "|" ~ to!string(c.front.dup);
|
||||
|
|
@ -1792,17 +1817,32 @@ class ApplicationConfig {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
if (debugLogging) {
|
||||
addLogEntry("skip_file in actual config = " ~ to!string(configFileSkipFileReadIn), ["debug"]);
|
||||
addLogEntry("skip_file in backup config = " ~ to!string(skip_file_present), ["debug"]);
|
||||
addLogEntry("defaultSkipFile value = " ~ to!string(defaultSkipFile), ["debug"]);
|
||||
addLogEntry("configFileSkipFile value = " ~ to!string(configFileSkipFile), ["debug"]);
|
||||
addLogEntry("backupConfigFileSkipFile value = " ~ to!string(backupConfigFileSkipFile), ["debug"]);
|
||||
}
|
||||
|
||||
// skip_file can be specified multiple times
|
||||
if (skip_file_present && backupConfigFileSkipFile != configFileSkipFile) logAndSetDifference("skip_file" ~ configOptionModifiedMessage, 4);
|
||||
|
||||
// skip_file can also be an empty string, thus when removed, as an empty string, we are going back to application defaults
|
||||
if (skip_file_present && backupConfigFileSkipFile != defaultSkipFile) logAndSetDifference("skip_file" ~ configOptionModifiedMessage, 4);
|
||||
|
||||
// skip_dir can be specified multiple times
|
||||
if (skip_dir_present && backupConfigFileSkipDir != configFileSkipDir) logAndSetDifference("skip_dir" ~ configOptionModifiedMessage, 5);
|
||||
|
||||
// Check for newly added configuration options
|
||||
// Check for newly added configuration options to the 'config' file vs being present in the 'backup' config file
|
||||
if (!drive_id_present && configFileDriveId != "") logAndSetDifference("drive_id newly added ... --resync needed", 2);
|
||||
if (!sync_dir_present && configFileSyncDir != defaultSyncDir) logAndSetDifference("sync_dir newly added ... --resync needed", 3);
|
||||
if (!skip_file_present && configFileSkipFile != defaultSkipFile) logAndSetDifference("skip_file newly added ... --resync needed", 4);
|
||||
if (configFileSkipFileReadIn) {
|
||||
// We actually read a 'skip_file' configuration line from the 'config' file
|
||||
if (!skip_file_present && configFileSkipFile != defaultSkipFile) logAndSetDifference("skip_file newly added ... --resync needed", 4);
|
||||
}
|
||||
if (!skip_dir_present && configFileSkipDir != "") logAndSetDifference("skip_dir newly added ... --resync needed", 5);
|
||||
if (!skip_dotfiles_present && configFileSkipDotfiles) logAndSetDifference("skip_dotfiles newly added ... --resync needed", 6);
|
||||
if (!skip_symlinks_present && configFileSkipSymbolicLinks) logAndSetDifference("skip_symlinks newly added ... --resync needed", 7);
|
||||
|
|
@ -2545,6 +2585,21 @@ class ApplicationConfig {
|
|||
// Return result
|
||||
return variablesAvailable;
|
||||
}
|
||||
|
||||
// Set the Recycle Bin Paths
|
||||
void setRecycleBinPaths() {
|
||||
// Get the configured base path
|
||||
string basePath = getValueString("recycle_bin_path");
|
||||
|
||||
// Ensure basePath ends with a single '/'
|
||||
if (!basePath.endsWith("/")) {
|
||||
basePath ~= "/";
|
||||
}
|
||||
|
||||
// Append subdirectories based on the recycle bin path
|
||||
recycleBinFilePath = basePath ~ "files/";
|
||||
recycleBinInfoPath = basePath ~ "info/";
|
||||
}
|
||||
}
|
||||
|
||||
// Output the full application help when --help is passed in
|
||||
|
|
|
|||
44
src/main.d
44
src/main.d
|
|
@ -201,6 +201,50 @@ int main(string[] cliArgs) {
|
|||
syncOrMonitorMissing = true; // --sync or --monitor is missing
|
||||
}
|
||||
|
||||
// Has the user configured to use the 'Recycle Bin' locally, for any files that are deleted online?
|
||||
if (appConfig.getValueBool("use_recycle_bin")) {
|
||||
// Configure the internal application paths which will be used to move rather than delete any online deletes to
|
||||
appConfig.setRecycleBinPaths();
|
||||
|
||||
// We need to ensure that the Recycle Bin Paths exist on the file system, and if they do not exist, create them
|
||||
// Test for appConfig.recycleBinFilePath
|
||||
if (!exists(appConfig.recycleBinFilePath)) {
|
||||
try {
|
||||
// Attempt to create the 'Recycle Bin' file path we have been configured with
|
||||
mkdirRecurse(appConfig.recycleBinFilePath);
|
||||
|
||||
// Configure the applicable permissions for the folder
|
||||
if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ appConfig.recycleBinFilePath, ["debug"]);}
|
||||
appConfig.recycleBinFilePath.setAttributes(octal!700); // Set to 0700 as Trash may contain sensitive and is the expected default permissions by GIO or KIO
|
||||
|
||||
} catch (std.file.FileException e) {
|
||||
// Creating the 'Recycle Bin' file path failed
|
||||
addLogEntry("ERROR: Unable to create the configured local 'Recycle Bin' file directory: " ~ e.msg, ["info", "notify"]);
|
||||
// Use exit scopes to shutdown API
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// Test for appConfig.recycleBinInfoPath
|
||||
if (!exists(appConfig.recycleBinInfoPath)) {
|
||||
try {
|
||||
// Attempt to create the 'Recycle Bin' info path we have been configured with
|
||||
mkdirRecurse(appConfig.recycleBinInfoPath);
|
||||
|
||||
// Configure the applicable permissions for the folder
|
||||
if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ appConfig.recycleBinInfoPath, ["debug"]);}
|
||||
appConfig.recycleBinInfoPath.setAttributes(octal!700); // Set to 0700 as Trash may contain sensitive and is the expected default permissions by GIO or KIO
|
||||
|
||||
|
||||
} catch (std.file.FileException e) {
|
||||
// Creating the 'Recycle Bin' info path failed
|
||||
addLogEntry("ERROR: Unable to create the configured local 'Recycle Bin' info directory: " ~ e.msg, ["info", "notify"]);
|
||||
// Use exit scopes to shutdown API
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Are we performing some sort of 'no-sync' operation task?
|
||||
noSyncTaskOperationRequested = appConfig.hasNoSyncOperationBeenRequested(); // returns true if we are
|
||||
|
||||
|
|
|
|||
303
src/sync.d
303
src/sync.d
|
|
@ -4295,7 +4295,7 @@ class SyncEngine {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Process items that need to be removed
|
||||
// Process items that need to be removed from the local filesystem as they were removed online
|
||||
void processDeleteItems() {
|
||||
// Function Start Time
|
||||
SysTime functionStartTime;
|
||||
|
|
@ -4308,78 +4308,154 @@ class SyncEngine {
|
|||
displayFunctionProcessingStart(thisFunctionName, logKey);
|
||||
}
|
||||
|
||||
foreach_reverse (i; idsToDelete) {
|
||||
Item item;
|
||||
string path;
|
||||
if (!itemDB.selectById(i[0], i[1], item)) continue; // check if the item is in the db
|
||||
// Compute this item path
|
||||
path = computeItemPath(i[0], i[1]);
|
||||
|
||||
// Log the action if the path exists .. it may of already been removed and this is a legacy array item
|
||||
if (exists(path)) {
|
||||
if (item.type == ItemType.file) {
|
||||
addLogEntry("Trying to delete local file: " ~ path);
|
||||
} else {
|
||||
addLogEntry("Trying to delete local directory: " ~ path);
|
||||
}
|
||||
}
|
||||
|
||||
// Process the database entry removal. In a --dry-run scenario, this is being done against a DB copy
|
||||
itemDB.deleteById(item.driveId, item.id);
|
||||
if (item.remoteDriveId != null) {
|
||||
// delete the linked remote folder
|
||||
itemDB.deleteById(item.remoteDriveId, item.remoteId);
|
||||
}
|
||||
|
||||
// Add to pathFakeDeletedArray
|
||||
// We dont want to try and upload this item again, so we need to track this objects removal
|
||||
if (dryRun) {
|
||||
// We need to add './' here so that it can be correctly searched to ensure it is not uploaded
|
||||
string pathToAdd = "./" ~ path;
|
||||
pathFakeDeletedArray ~= pathToAdd;
|
||||
}
|
||||
// Has the user configured to use the 'Recycle Bin' locally, for any files that are deleted online?
|
||||
if (!appConfig.getValueBool("use_recycle_bin")) {
|
||||
|
||||
if (debugLogging) {addLogEntry("Performing filesystem deletion, using reverse order of items to delete", ["debug"]);}
|
||||
|
||||
foreach_reverse (i; idsToDelete) {
|
||||
Item item;
|
||||
string path;
|
||||
if (!itemDB.selectById(i[0], i[1], item)) continue; // check if the item is in the db
|
||||
// Compute this item path
|
||||
path = computeItemPath(i[0], i[1]);
|
||||
|
||||
bool needsRemoval = false;
|
||||
if (exists(path)) {
|
||||
// path exists on the local system
|
||||
// make sure that the path refers to the correct item
|
||||
Item pathItem;
|
||||
if (itemDB.selectByPath(path, item.driveId, pathItem)) {
|
||||
if (pathItem.id == item.id) {
|
||||
needsRemoval = true;
|
||||
// Log the action if the path exists .. it may of already been removed and this is a legacy array item
|
||||
if (exists(path)) {
|
||||
if (item.type == ItemType.file) {
|
||||
addLogEntry("Trying to delete local file: " ~ path);
|
||||
} else {
|
||||
addLogEntry("Skipped due to id difference!");
|
||||
addLogEntry("Trying to delete local directory: " ~ path);
|
||||
}
|
||||
} else {
|
||||
// item has disappeared completely
|
||||
needsRemoval = true;
|
||||
}
|
||||
}
|
||||
if (needsRemoval) {
|
||||
// Log the action
|
||||
if (item.type == ItemType.file) {
|
||||
addLogEntry("Deleting local file: " ~ path, fileTransferNotifications());
|
||||
} else {
|
||||
addLogEntry("Deleting local directory: " ~ path, fileTransferNotifications());
|
||||
}
|
||||
|
||||
// Perform the action
|
||||
if (!dryRun) {
|
||||
if (isFile(path)) {
|
||||
remove(path);
|
||||
} else {
|
||||
try {
|
||||
// Remove any children of this path if they still exist
|
||||
// Resolve 'Directory not empty' error when deleting local files
|
||||
foreach (DirEntry child; dirEntries(path, SpanMode.depth, false)) {
|
||||
attrIsDir(child.linkAttributes) ? rmdir(child.name) : remove(child.name);
|
||||
}
|
||||
// Remove the path now that it is empty of children
|
||||
rmdirRecurse(path);
|
||||
} catch (FileException e) {
|
||||
// display the error message
|
||||
displayFileSystemErrorMessage(e.msg, thisFunctionName);
|
||||
// Process the database entry removal. In a --dry-run scenario, this is being done against a DB copy
|
||||
itemDB.deleteById(item.driveId, item.id);
|
||||
if (item.remoteDriveId != null) {
|
||||
// delete the linked remote folder
|
||||
itemDB.deleteById(item.remoteDriveId, item.remoteId);
|
||||
}
|
||||
|
||||
// Add to pathFakeDeletedArray
|
||||
// We dont want to try and upload this item again, so we need to track this objects removal
|
||||
if (dryRun) {
|
||||
// We need to add './' here so that it can be correctly searched to ensure it is not uploaded
|
||||
string pathToAdd = "./" ~ path;
|
||||
pathFakeDeletedArray ~= pathToAdd;
|
||||
}
|
||||
|
||||
bool needsRemoval = false;
|
||||
if (exists(path)) {
|
||||
// path exists on the local system
|
||||
// make sure that the path refers to the correct item
|
||||
Item pathItem;
|
||||
if (itemDB.selectByPath(path, item.driveId, pathItem)) {
|
||||
if (pathItem.id == item.id) {
|
||||
needsRemoval = true;
|
||||
} else {
|
||||
addLogEntry("Skipped due to id difference!");
|
||||
}
|
||||
} else {
|
||||
// item has disappeared completely
|
||||
needsRemoval = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsRemoval) {
|
||||
// Log the action
|
||||
if (item.type == ItemType.file) {
|
||||
addLogEntry("Deleting local file: " ~ path, fileTransferNotifications());
|
||||
} else {
|
||||
addLogEntry("Deleting local directory: " ~ path, fileTransferNotifications());
|
||||
}
|
||||
|
||||
// Perform the action
|
||||
if (!dryRun) {
|
||||
if (isFile(path)) {
|
||||
remove(path);
|
||||
} else {
|
||||
try {
|
||||
// Remove any children of this path if they still exist
|
||||
// Resolve 'Directory not empty' error when deleting local files
|
||||
foreach (DirEntry child; dirEntries(path, SpanMode.depth, false)) {
|
||||
attrIsDir(child.linkAttributes) ? rmdir(child.name) : remove(child.name);
|
||||
}
|
||||
// Remove the path now that it is empty of children
|
||||
rmdirRecurse(path);
|
||||
} catch (FileException e) {
|
||||
// display the error message
|
||||
displayFileSystemErrorMessage(e.msg, thisFunctionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (debugLogging) {addLogEntry("Moving online deleted files to configured local Recycle Bin", ["debug"]);}
|
||||
|
||||
// Process in normal order, so that the parent, if a folder, gets moved 'first' mirroring how files / folders are deleted in GNOME and KDE
|
||||
foreach (i; idsToDelete) {
|
||||
Item item;
|
||||
string path;
|
||||
if (!itemDB.selectById(i[0], i[1], item)) continue; // check if the item is in the db
|
||||
// Compute this item path
|
||||
path = computeItemPath(i[0], i[1]);
|
||||
|
||||
// Log the action if the path exists .. it may of already been removed and this is a legacy array item
|
||||
if (exists(path)) {
|
||||
if (item.type == ItemType.file) {
|
||||
addLogEntry("Trying to move this local file to the configured 'Recycle Bin': " ~ path);
|
||||
} else {
|
||||
addLogEntry("Trying to move this local directory to the configured 'Recycle Bin': " ~ path);
|
||||
}
|
||||
}
|
||||
|
||||
// Process the database entry removal. In a --dry-run scenario, this is being done against a DB copy
|
||||
itemDB.deleteById(item.driveId, item.id);
|
||||
if (item.remoteDriveId != null) {
|
||||
// delete the linked remote folder
|
||||
itemDB.deleteById(item.remoteDriveId, item.remoteId);
|
||||
}
|
||||
|
||||
// Add to pathFakeDeletedArray
|
||||
// We dont want to try and upload this item again, so we need to track this objects removal
|
||||
if (dryRun) {
|
||||
// We need to add './' here so that it can be correctly searched to ensure it is not uploaded
|
||||
string pathToAdd = "./" ~ path;
|
||||
pathFakeDeletedArray ~= pathToAdd;
|
||||
}
|
||||
|
||||
bool needsRemoval = false;
|
||||
if (exists(path)) {
|
||||
// path exists on the local system
|
||||
// make sure that the path refers to the correct item
|
||||
Item pathItem;
|
||||
if (itemDB.selectByPath(path, item.driveId, pathItem)) {
|
||||
if (pathItem.id == item.id) {
|
||||
needsRemoval = true;
|
||||
} else {
|
||||
addLogEntry("Skipped due to id difference!");
|
||||
}
|
||||
} else {
|
||||
// item has disappeared completely
|
||||
needsRemoval = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsRemoval) {
|
||||
// Log the action
|
||||
if (item.type == ItemType.file) {
|
||||
addLogEntry("Moving this local file to the configured 'Recycle Bin': " ~ path, fileTransferNotifications());
|
||||
} else {
|
||||
addLogEntry("Moving this local directory to the configured 'Recycle Bin': " ~ path, fileTransferNotifications());
|
||||
}
|
||||
|
||||
// Perform the action
|
||||
if (!dryRun) {
|
||||
// Move the 'path' to the configured recycle bin
|
||||
movePathToRecycleBin(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4396,6 +4472,99 @@ class SyncEngine {
|
|||
displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the 'Recycle Bin' rather than a hard delete locally of the online deleted item
|
||||
void movePathToRecycleBin(string path) {
|
||||
// Function Start Time
|
||||
SysTime functionStartTime;
|
||||
string logKey;
|
||||
string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({})));
|
||||
// Only set this if we are generating performance processing times
|
||||
if (appConfig.getValueBool("display_processing_time") && debugLogging) {
|
||||
functionStartTime = Clock.currTime();
|
||||
logKey = generateAlphanumericString();
|
||||
displayFunctionProcessingStart(thisFunctionName, logKey);
|
||||
}
|
||||
|
||||
// This is a 2 step process
|
||||
// 1. Move the file
|
||||
// - If the destination 'name' already exists, the file being moved to the 'Recycle Bin' needs to have a number added to it.
|
||||
// 2. Create the metadata about where the file came from
|
||||
// - This is in a specific format:
|
||||
// [Trash Info]
|
||||
// Path=/original/absolute/path/to/the/file/or/folder
|
||||
// DeletionDate=YYYY-MM-DDTHH:MM:SS
|
||||
|
||||
// Calculate all the initial paths required
|
||||
string computedFullLocalPath = absolutePath(path);
|
||||
string fileNameOnly = baseName(path);
|
||||
string computedRecycleBinFilePath = appConfig.recycleBinFilePath ~ fileNameOnly;
|
||||
string computedRecycleBinInfoPath = appConfig.recycleBinInfoPath ~ fileNameOnly ~ ".trashinfo";
|
||||
bool isPathFile = isFile(computedFullLocalPath);
|
||||
|
||||
// The 'destination' needs to be unique, but if there is a 'collision' the RecycleBin paths need to be updated to be:
|
||||
// - file1.data (1)
|
||||
// - file1.data (1).trashinfo
|
||||
if (exists(computedRecycleBinFilePath)) {
|
||||
// There is an existing file with the same name already in the 'Recycle Bin'
|
||||
// - Testing has show that this counter MUST start at 2 to be compatible with FreeDesktop.org Trash Specification ....
|
||||
int n = 2;
|
||||
|
||||
// We need to split this out
|
||||
string nameOnly = stripExtension(fileNameOnly); // "file1"
|
||||
string extension = extension(fileNameOnly); // ".data"
|
||||
|
||||
// We need to test for this: nameOnly.n.extension
|
||||
while (exists(format(appConfig.recycleBinFilePath ~ nameOnly ~ ".%d." ~ extension, n))) {
|
||||
n++;
|
||||
}
|
||||
|
||||
// Generate newFileNameOnly
|
||||
string newFileNameOnly = format(nameOnly ~ ".%d." ~ extension, n);
|
||||
|
||||
// UPDATE:
|
||||
// - computedRecycleBinFilePath
|
||||
// - computedRecycleBinInfoPath
|
||||
computedRecycleBinFilePath = appConfig.recycleBinFilePath ~ newFileNameOnly;
|
||||
computedRecycleBinInfoPath = appConfig.recycleBinInfoPath ~ newFileNameOnly ~ ".trashinfo";
|
||||
}
|
||||
|
||||
// Move the file to the 'Recycle Bin' path computedRecycleBinFilePath
|
||||
// - DMD has no 'move' specifically, it uses 'rename' to achieve this
|
||||
// https://forum.dlang.org/thread/kwnwrlqtjehldckyfmau@forum.dlang.org
|
||||
// Use rename() as Linux is POSIX compliant, we have an atomic operation where at no point in time the 'to' is missing.
|
||||
try {
|
||||
rename(computedFullLocalPath, computedRecycleBinFilePath);
|
||||
} catch (Exception e) {
|
||||
// Handle exceptions, e.g., log error
|
||||
if (isPathFile) {
|
||||
addLogEntry("Move of local file failed for " ~ to!string(path) ~ ": " ~ e.msg, ["error"]);
|
||||
} else {
|
||||
addLogEntry("Move of local directory failed for " ~ to!string(path) ~ ": " ~ e.msg, ["error"]);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the 'Recycle Bin' metadata file using computedRecycleBinInfoPath
|
||||
auto now = Clock.currTime().toLocalTime();
|
||||
string deletionDate = format("%04d-%02d-%02dT%02d:%02d:%02d",now.year, now.month, now.day, now.hour, now.minute, now.second);
|
||||
|
||||
// Format the content of the .trashinfo file
|
||||
string content = format("[Trash Info]\nPath=%s\nDeletionDate=%s\n", computedFullLocalPath, deletionDate);
|
||||
// Write the metadata file
|
||||
|
||||
try {
|
||||
std.file.write(computedRecycleBinInfoPath, content);
|
||||
} catch (Exception e) {
|
||||
// Handle exceptions, e.g., log error
|
||||
addLogEntry("Writing of .trashinfo metadata file failed for " ~ computedRecycleBinInfoPath ~ ": " ~ e.msg, ["error"]);
|
||||
}
|
||||
|
||||
// Display function processing time if configured to do so
|
||||
if (appConfig.getValueBool("display_processing_time") && debugLogging) {
|
||||
// Combine module name & running Function
|
||||
displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);
|
||||
}
|
||||
}
|
||||
|
||||
// List items that were deleted online, but, due to --download-only being used, will not be deleted locally
|
||||
void listDeletedItems() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue