Implement FR #3435: Add option to remove source folders (#3473)

* Add option to remove source folders when using --upload-only --remove-source-files
* Parental path will only be removed if path is empty - that is, no other folders or files present
This commit is contained in:
abraunegg 2025-10-14 06:47:51 +11:00 committed by GitHub
commit 2f339c5663
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 89 additions and 2 deletions

View file

@ -75,6 +75,7 @@ Cforce
checkinterval
chkconfig
chpst
chrooted
classinfo
clen
cloexec

View file

@ -734,6 +734,23 @@ _**CLI Option Use:**_ `--remove-source-files`
> [!IMPORTANT]
> This configuration option can *only* be used in conjunction with `--upload-only`
### remove_source_folders
_**Description:**_ This configuration option controls whether the OneDrive Client for Linux removes the local directory structure post successful file transfer to Microsoft OneDrive.
_**Value Type:**_ Boolean
_**Default Value:**_ False
_**Config Example:**_ `remove_source_folders = "false"` or `remove_source_folders = "true"`
_**CLI Option Use:**_ `--remove-source-folders`
> [!IMPORTANT]
> This configuration option can *only* be used in conjunction with `--upload-only --remove-source-files`
> [!IMPORTANT]
> The directory structure will only be removed if it is empty.
### resync
_**Description:**_ This configuration option controls whether the known local sync state with Microsoft OneDrive is removed at application startup. When this option is used, a full scan of your data online is performed to ensure that the local sync state is correctly built back up.

View file

@ -288,6 +288,10 @@ Remove a directory on OneDrive - no sync will be performed.
\fB\-\-remove-source-files\fR
Remove source file after successful transfer to OneDrive when using --upload-only.
.TP
\fB\-\-remove-source-folders\fR
Remove the local directory structure post successful file transfer to Microsoft OneDrive when using --upload-only --remove-source-files.
.TP
\fB\-\-resync\fR
Forget the last saved state, perform a full sync.

View file

@ -361,8 +361,10 @@ class ApplicationConfig {
boolValues["dry_run"] = false;
// Do we sync all the files in the 'sync_dir' root?
boolValues["sync_root_files"] = false;
// Do we delete source after successful transfer?
// Do we delete source file after successful transfer?
boolValues["remove_source_files"] = false;
// Do we delete source folders after successful transfer?
boolValues["remove_source_folders"] = false;
// Do we perform strict matching for skip_dir?
boolValues["skip_dir_strict_match"] = false;
// Do we perform a --resync?
@ -1308,6 +1310,9 @@ class ApplicationConfig {
"remove-source-files",
"Remove source file after successful transfer to OneDrive when using --upload-only",
&boolValues["remove_source_files"],
"remove-source-folders",
"Remove the local directory structure post successful file transfer to Microsoft OneDrive when using --upload-only --remove-source-files",
&boolValues["remove_source_folders"],
"single-directory",
"Specify a single local directory within the OneDrive root to sync",
&stringValues["single_directory"],

View file

@ -6727,6 +6727,15 @@ class SyncEngine {
if (debugLogging) {addLogEntry("Removing local file: " ~ localPathToRemove, ["debug"]);}
safeRemove(localPathToRemove);
addLogEntry("Removed local file: " ~ localPathToRemove);
// Do we try and attempt to remove the local source tree?
if (appConfig.getValueBool("remove_source_folders")) {
// Remove the source directory structure but only if it is empty
addLogEntry("Attempting removal of local directory structure as --upload-only & --remove-source-files & --remove-source-folders configured");
string parentPath = dirName(localPathToRemove);
removeEmptyParents(localPathToRemove);
addLogEntry("Removed parental path: " ~ parentPath);
}
} else {
// --dry-run scenario
addLogEntry("Not removing local file as --dry-run configured");
@ -6736,7 +6745,47 @@ class SyncEngine {
addLogEntry("Removing local file not possible as local file does not exist");
}
}
// Remove empty parent directories of `filePath` upwards until:
// - we hit a non-empty directory, or
// - we reach the visible root (i.e. dirName(current) == ".").
// Never tries to remove ".".
void removeEmptyParents(string filePath) {
// Work with a normalised *relative* path inside the chrooted configured 'sync_dir'
// If someone passed an absolute path, normalise it anyway; your codebase
// likely already ensures paths are relative within the sync root.
string current = dirName(buildNormalizedPath(filePath));
while (current.length && current != ".") {
// Safety: dont descend into symlinks
if (isSymlink(current)) {
if (debugLogging) addLogEntry("Skipping removal; parent is a symlink: " ~ current, ["debug"]);
break;
}
// Stop at first non-empty directory
if (!isDirEmpty(current)) {
if (debugLogging) addLogEntry("Stopping prune; directory not empty: " ~ current, ["debug"]);
break;
}
if (!dryRun) {
if (debugLogging) addLogEntry("Removing empty directory: " ~ current, ["debug"]);
// rmdir only succeeds for empty directories; errors are collected not thrown
collectException(rmdir(current));
} else {
addLogEntry("Not removing empty directory as --dry-run configured: " ~ current);
}
// Move up one level
string next = dirName(current);
if (next == current) { // Just in case (shouldnt happen with relative paths)
break;
}
current = next;
}
}
// Perform the upload of a locally modified file to OneDrive
JSONValue performModifiedFileUpload(Item dbItem, string localFilePath, long thisFileSizeLocal) {
// Function Start Time

View file

@ -1917,3 +1917,14 @@ void displayFunctionProcessingTime(string functionName, SysTime functionStartTim
string processingTime = format("[%s] Application Function '%s' Processing Time = %.4f Seconds", strip(logKey), strip(functionName), functionDurationAsSeconds);
addLogEntry(processingTime);
}
// Return true if `dir` exists and has no entries.
// Symlinks are treated as non-removable.
bool isDirEmpty(string dir) {
if (!exists(dir) || !isDir(dir) || isSymlink(dir)) return false;
foreach (_; dirEntries(dir, SpanMode.shallow)) {
// Found at least one entry
return false;
}
return true;
}