@@ -3,7 +3,7 @@ module util;
33
44// What does this module require to function?
55import core.memory ;
6- import core.stdc.errno : ENOENT , EINTR , EBUSY , EXDEV , EAGAIN ;
6+ import core.stdc.errno : ENOENT , EINTR , EBUSY , EXDEV , EAGAIN , EPERM , EACCES , EROFS ;
77import core.stdc.stdlib ;
88import core.stdc.string ;
99import core.sys.posix.pwd ;
@@ -66,9 +66,10 @@ shared static this() {
6666
6767// To assist with filesystem severity issues, configure an enum that can be used
6868enum FsErrorSeverity {
69- warning,
70- error,
71- fatal
69+ warning,
70+ error,
71+ fatal,
72+ permission
7273}
7374
7475// Creates a safe backup of the given item, and only performs the function if not in a --dry-run scenario.
@@ -1066,8 +1067,9 @@ void displayFileSystemErrorMessage(string message, string callingFunction, strin
10661067
10671068 // Header prefix for logging accuracy
10681069 string headerPrefix = severity == FsErrorSeverity.warning ? " WARNING"
1069- : severity == FsErrorSeverity.fatal ? " FATAL"
1070- : " ERROR" ;
1070+ : severity == FsErrorSeverity.permission ? " WARNING"
1071+ : severity == FsErrorSeverity.fatal ? " FATAL"
1072+ : " ERROR" ;
10711073
10721074 // Filesystem logging header
10731075 addLogEntry(headerPrefix ~ " : The local file system returned an error with the following details:" );
@@ -1114,72 +1116,83 @@ void displayFileSystemErrorMessage(string message, string callingFunction, strin
11141116 addLogEntry(" Error Message: No error message available" );
11151117 }
11161118
1117- // Disk space diagnostics (best-effort)
1118- // We intentionally probe both the current directory and the target path directory when possible.
1119- try {
1120- // Always check the current working directory as a baseline
1121- ulong freeCwd = to! ulong (getAvailableDiskSpace(" ." ));
1122- addLogEntry(" Disk Space (CWD): " ~ to! string (freeCwd) ~ " bytes available" );
1123-
1124- // If we have a context path, also check its parent directory when possible.
1125- // We keep this conservative: if anything throws, just log the exception.
1126- if (! contextPath.empty) {
1127- string targetProbePath = contextPath;
1128-
1129- // If it's a file path, probe the parent directory (where writes/renames happen).
1130- // Avoid throwing if parentDir isn't available or contextPath is weird.
1131- try {
1132- // std.path.dirName handles both file/dir paths; if it returns ".", keep as-is.
1133- import std.path : dirName;
1134- auto parent = dirName(contextPath);
1135- if (! parent.empty) targetProbePath = parent;
1136- } catch (Exception e) {
1137- addLogEntry(" NOTE: Failed to derive parent directory from path: " ~ e.msg);
1138- }
1119+ // Disk space diagnostics (best-effort) - if this is not a permission issue
1120+ if (severity != FsErrorSeverity.permission) {
1121+ // We intentionally probe both the current directory and the target path directory when possible.
1122+ try {
1123+ // Always check the current working directory as a baseline
1124+ ulong freeCwd = to! ulong (getAvailableDiskSpace(" ." ));
1125+ addLogEntry(" Disk Space (CWD): " ~ to! string (freeCwd) ~ " bytes available" );
1126+
1127+ // If we have a context path, also check its parent directory when possible.
1128+ // We keep this conservative: if anything throws, just log the exception.
1129+ if (! contextPath.empty) {
1130+ string targetProbePath = contextPath;
1131+
1132+ // If it's a file path, probe the parent directory (where writes/renames happen).
1133+ // Avoid throwing if parentDir isn't available or contextPath is weird.
1134+ try {
1135+ // std.path.dirName handles both file/dir paths; if it returns ".", keep as-is.
1136+ import std.path : dirName;
1137+ auto parent = dirName(contextPath);
1138+ if (! parent.empty) targetProbePath = parent;
1139+ } catch (Exception e) {
1140+ addLogEntry(" NOTE: Failed to derive parent directory from path: " ~ e.msg);
1141+ }
11391142
1140- ulong freeTarget = to! ulong (getAvailableDiskSpace(targetProbePath));
1141- addLogEntry(" Disk Space (Path): " ~ to! string (freeTarget) ~ " bytes available (parent path: " ~ targetProbePath ~ " )" );
1143+ ulong freeTarget = to! ulong (getAvailableDiskSpace(targetProbePath));
1144+ addLogEntry(" Disk Space (Path): " ~ to! string (freeTarget) ~ " bytes available (parent path: " ~ targetProbePath ~ " )" );
11421145
1143- // Preserve existing behaviour: if disk space check returns 0, force exit.
1144- // (Assumes getAvailableDiskSpace returns 0 on a hard failure in your implementation.)
1145- if (freeTarget == 0 || freeCwd == 0 ) {
1146- // Must force exit here, allow logging to be done
1147- forceExit();
1148- }
1149- } else {
1150- // Preserve existing behaviour: if disk space check returns 0, force exit.
1151- if (freeCwd == 0 ) {
1152- forceExit();
1146+ // Preserve existing behaviour: if disk space check returns 0, force exit.
1147+ // (Assumes getAvailableDiskSpace returns 0 on a hard failure in your implementation.)
1148+ if (freeTarget == 0 || freeCwd == 0 ) {
1149+ // Must force exit here, allow logging to be done
1150+ forceExit();
1151+ }
1152+ } else {
1153+ // Preserve existing behaviour: if disk space check returns 0, force exit.
1154+ if (freeCwd == 0 ) {
1155+ forceExit();
1156+ }
11531157 }
1158+ } catch (Exception e) {
1159+ // Handle exceptions from disk space check or type conversion
1160+ addLogEntry(" NOTE: Exception during disk space check: " ~ e.msg);
11541161 }
1155- } catch (Exception e) {
1156- // Handle exceptions from disk space check or type conversion
1157- addLogEntry(" NOTE: Exception during disk space check: " ~ e.msg);
11581162 }
11591163
11601164 // Add note for WARNING messages
1161- if (headerPrefix == " WARNING " ) {
1165+ if (severity == FsErrorSeverity.warning ) {
11621166 addLogEntry();
1163- addLogEntry(" NOTE: This error is non-fatal; the client will continue to operate, but this may affect future operations if not resolved" );
1167+ addLogEntry(" NOTE: This warning is non-fatal; the client will continue to operate, but this may affect future operations if not resolved" );
1168+ addLogEntry();
1169+ }
1170+
1171+ // Add note for filesystem permission messages
1172+ if (severity == FsErrorSeverity.permission) {
1173+ addLogEntry();
1174+ addLogEntry(" NOTE: Sync will continue. This file’s timestamps could not be updated because the effective user does not own the file." );
1175+ addLogEntry(" Potential Fix:" );
1176+ addLogEntry(" Run the client as the file owner, or change ownership of the sync tree so it is owned by the user running the client." );
1177+ addLogEntry(" Learn more about File Ownership:" );
1178+ addLogEntry(" https://www.redhat.com/en/blog/linux-file-permissions-explained" );
1179+ addLogEntry(" https://unix.stackexchange.com/questions/191940/difference-between-owner-root-and-ruid-euid" );
11641180 addLogEntry();
11651181 }
11661182
11671183 // Add note for ERROR messages
1168- if (headerPrefix == " ERROR " ) {
1184+ if (severity == FsErrorSeverity.error ) {
11691185 addLogEntry();
11701186 addLogEntry(" NOTE: This error requires attention; the client may continue running, but functionality is impaired and the issue should be resolved." );
11711187 addLogEntry();
11721188 }
11731189
11741190 // Add note for FATAL messages
1175- if (headerPrefix == " FATAL " ) {
1191+ if (severity == FsErrorSeverity.fatal ) {
11761192 addLogEntry();
11771193 addLogEntry(" NOTE: This error is fatal; the client cannot continue and this issue must be corrected before retrying. The client will now attempt to exit in a safe and orderly manner." );
11781194 addLogEntry();
11791195 }
1180-
1181- // Separate this block from surrounding log output
1182- addLogEntry();
11831196}
11841197
11851198// Display the POSIX Error Message
@@ -2103,9 +2116,57 @@ private bool safeGetTimes(string path, out SysTime accessTime, out SysTime modTi
21032116 return false ;
21042117}
21052118
2119+ // Some errnos are 'expected' in the wild (permissions, RO mounts, immutable files)
2120+ // What is this errno
2121+ private bool isExpectedPermissionStyleErrno (int err) {
2122+ // Return true of this is an expected error due to permission issues
2123+ return err == EPERM || err == EACCES || err == EROFS ;
2124+ }
2125+
2126+ // Helper function to determine path mismatch against UID|GID and process effective UID
2127+ private bool getPathOwnerMismatch (string path, out uint fileUid, out uint effectiveUid) {
2128+ version (Posix ) {
2129+ stat_t st;
2130+
2131+ // Default outputs
2132+ fileUid = 0 ;
2133+ effectiveUid = cast (uint ) geteuid();
2134+
2135+ try {
2136+ // absolutePath can throw; keep this helper non-throwing
2137+ auto fullPath = absolutePath(path);
2138+
2139+ // Ensure we pass a NUL-terminated string to the C API
2140+ auto cpath = toStringz(fullPath);
2141+
2142+ if (lstat(cpath, &st) != 0 ) {
2143+ if (debugLogging) {
2144+ addLogEntry(" getPathOwnerMismatch(): lstat() failed for '" ~ path ~ " '" , [" debug" ]);
2145+ }
2146+ return false ;
2147+ }
2148+
2149+ fileUid = cast (uint ) st.st_uid;
2150+ // effectiveUid already set above
2151+ return fileUid != effectiveUid;
2152+
2153+ } catch (Exception e) {
2154+ if (debugLogging) {
2155+ addLogEntry(" getPathOwnerMismatch(): exception for '" ~ path ~ " ': " ~ e.msg, [" debug" ]);
2156+ }
2157+ return false ;
2158+ }
2159+ } else {
2160+ fileUid = 0 ;
2161+ effectiveUid = 0 ;
2162+ return false ;
2163+ }
2164+ }
2165+
21062166// Retry wrapper for setTimes()
21072167private bool safeSetTimes (string path, SysTime accessTime, SysTime modTime, string thisFunctionName) {
2108- int maxAttempts = 5 ;
2168+
2169+ enum int maxAttempts = 5 ;
21092170
21102171 foreach (attempt; 0 .. maxAttempts) {
21112172 try {
@@ -2117,18 +2178,72 @@ private bool safeSetTimes(string path, SysTime accessTime, SysTime modTime, stri
21172178 return false ;
21182179 }
21192180
2181+ // Transient filesystem error: retry with backoff
21202182 if (isTransientErrno(e.errno)) {
2121- // slightly longer backoff here is fine too, but keep it simple/consistent
2183+ if (debugLogging) {
2184+ // Log that we hit a transient error when doing debugging, otherwise nothing
2185+ addLogEntry(" safeSetTimes() transient filesystem error response: " ~ e.msg ~ " \n - Attempting retry for setTimes()" , [" debug" ]);
2186+ }
2187+ // Backoff and retry
21222188 Thread .sleep(dur! " msecs" (15 * (attempt + 1 )));
21232189 continue ;
21242190 }
21252191
2192+ // Non-transient: special-case common permission errors
2193+ // The user running the client needs to be the owner of the files if the client needs to set explicit timestamps
2194+ // See https://github.com/abraunegg/onedrive/issues/3651 for details
2195+ if (isExpectedPermissionStyleErrno(e.errno)) {
2196+ // Configure application message to display
2197+ string permissionErrorMessage = " Unable to set local file timestamps (mtime/atime): Operation not permitted" ;
2198+ if (e.errno == EPERM ) {
2199+ permissionErrorMessage = permissionErrorMessage ~ " (EPERM)" ;
2200+ }
2201+
2202+ if (e.errno == EACCES ) {
2203+ permissionErrorMessage = permissionErrorMessage ~ " (EACCES)" ;
2204+ }
2205+
2206+ if (e.errno == EROFS ) {
2207+ permissionErrorMessage = permissionErrorMessage ~ " (EROFS)" ;
2208+ }
2209+
2210+ // Get extra details if required
2211+ string extraHint;
2212+ uint fileUid;
2213+ uint effectiveUid;
2214+
2215+ if (e.errno == EPERM && getPathOwnerMismatch(path, fileUid, effectiveUid)) {
2216+ extraHint =
2217+ " \n The onedrive client user does not own this file. onedrive user effective UID=" ~ to! string (effectiveUid) ~ " , file owner UID=" ~ to! string (fileUid) ~ " ." ~
2218+ " \n On Unix-like systems, setting explicit file timestamps typically requires the process to be the file owner or run with sufficient privileges." ;
2219+
2220+ // Update permissionErrorMessage to add extraHint
2221+ permissionErrorMessage = permissionErrorMessage ~ extraHint;
2222+ }
2223+
2224+ // If we are doing --verbose or --debug display this file system error
2225+ if (verboseLogging) {
2226+ // Display applicable message for the user regarding permission error on path
2227+ displayFileSystemErrorMessage(
2228+ permissionErrorMessage,
2229+ thisFunctionName,
2230+ path,
2231+ FsErrorSeverity.permission
2232+ );
2233+ }
2234+
2235+ // It is pointless attempting a re-try in this scenario as those conditions will not change by retrying 15ms later.
2236+ return false ;
2237+ }
2238+
2239+ // Everything else: preserve existing behaviour
21262240 displayFileSystemErrorMessage(e.msg, thisFunctionName, path);
21272241 return false ;
21282242 }
21292243 }
21302244
2131- displayFileSystemErrorMessage(" Failed to set file timestamps after retries" , thisFunctionName, path);
2245+ // Only reached if transient errors never resolved
2246+ displayFileSystemErrorMessage(" Failed to set path timestamps after retries" , thisFunctionName, path);
21322247 return false ;
21332248}
21342249
0 commit comments