diff --git a/Duplicati/Library/Backend/File/FileBackend.cs b/Duplicati/Library/Backend/File/FileBackend.cs
index 537d563a1..7f2434fd1 100644
--- a/Duplicati/Library/Backend/File/FileBackend.cs
+++ b/Duplicati/Library/Backend/File/FileBackend.cs
@@ -24,8 +24,6 @@ using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
using Duplicati.Library.Utility.Options;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
namespace Duplicati.Library.Backend
{
@@ -451,72 +449,12 @@ namespace Duplicati.Library.Backend
throw new FileMissingException(Strings.FileBackend.FileNotFoundError(ex.Message), ex);
}
- ///
- /// Gets the drive or volume information for the specified path.
- ///
- /// The drive or volume information, or null if it could not be determined.
- private DriveInfo? GetDrive()
- {
- string root;
- if (!OperatingSystem.IsWindows())
- {
- string path = Util.AppendDirSeparator(systemIO.PathGetFullPath(m_path));
-
- // If the built-in .NET DriveInfo works, use it
- try { return new DriveInfo(path); }
- catch { }
-
- root = "/";
-
- //Find longest common prefix from mounted devices
- //TODO: Can trick this with symlinks, where the symlink is on one mounted volume,
- // and the actual storage is on another
- foreach (var di in DriveInfo.GetDrives())
- if (path.StartsWith(Util.AppendDirSeparator(di.Name), StringComparison.Ordinal) && di.Name.Length > root.Length)
- root = di.Name;
- }
- else
- {
- root = systemIO.GetPathRoot(m_path);
- }
-
- // On Windows, DriveInfo is only valid for lettered drives. (e.g., not for UNC paths and shares)
- // So only attempt to get it if we aren't on Windows or if the root starts with a letter.
- if (!OperatingSystem.IsWindows() || (root.Length > 0 && char.IsLetter(root[0])))
- {
- try
- {
- return new DriveInfo(root);
- }
- catch (ArgumentException)
- {
- // If there was a problem, fall back to returning null
- }
- }
-
- return null;
- }
-
///
public Task GetQuotaInfoAsync(CancellationToken cancelToken)
{
- var driveInfo = this.GetDrive();
- if (driveInfo != null)
- {
- // Check that the total space is above 0, because Mono sometimes reports 0 for unknown file systems
- // If the drive actually has a total size of 0, this should be obvious immediately due to write errors
- if (driveInfo.TotalSize > 0)
- {
- return Task.FromResult(new QuotaInfo(driveInfo.TotalSize, driveInfo.AvailableFreeSpace));
- }
- }
-
- if (OperatingSystem.IsWindows())
- {
- // If we can't get the DriveInfo on Windows, fallback to GetFreeDiskSpaceEx
- // https://stackoverflow.com/questions/2050343/programmatically-determining-space-available-from-unc-path
- return Task.FromResult(GetDiskFreeSpace(m_path));
- }
+ var spaceInfo = Utility.Utility.GetFreeSpaceForPath(m_path);
+ if (spaceInfo != null)
+ return Task.FromResult(new QuotaInfo(spaceInfo.Value.TotalSpace, spaceInfo.Value.FreeSpace));
return Task.FromResult(null);
}
@@ -536,50 +474,6 @@ namespace Duplicati.Library.Backend
return Task.CompletedTask;
}
- ///
- /// Get the disk free space using the Win32 API's GetDiskFreeSpaceEx function.
- ///
- /// Directory
- /// Quota info
- [MethodImpl(MethodImplOptions.NoInlining)]
- [SupportedOSPlatform("windows")]
- public static QuotaInfo? GetDiskFreeSpace(string directory)
- {
- ulong available;
- ulong total;
- if (WindowsDriveHelper.GetDiskFreeSpaceEx(directory, out available, out total, out _))
- {
- return new QuotaInfo((long)total, (long)available);
- }
- else
- {
- return null;
- }
- }
-
- ///
- /// Provides helper methods for working with Windows drives.
- ///
- [SupportedOSPlatform("windows")]
- private static class WindowsDriveHelper
- {
- ///
- /// Gets the free disk space for the specified directory using the Win32 API.
- ///
- /// The directory name to check
- /// The available free bytes
- /// The total number of bytes
- /// The total number of free bytes
- ///
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool GetDiskFreeSpaceEx(
- string lpDirectoryName,
- out ulong lpFreeBytesAvailable,
- out ulong lpTotalNumberOfBytes,
- out ulong lpTotalNumberOfFreeBytes);
- }
-
///
/// Verifies that the target file size matches the source file size.
///
diff --git a/Duplicati/Library/Main/Operation/RestoreHandler.cs b/Duplicati/Library/Main/Operation/RestoreHandler.cs
index 23d50dabc..2a03260ea 100644
--- a/Duplicati/Library/Main/Operation/RestoreHandler.cs
+++ b/Duplicati/Library/Main/Operation/RestoreHandler.cs
@@ -51,6 +51,42 @@ namespace Duplicati.Library.Main.Operation
m_result = result;
}
+ ///
+ /// Checks if the tempdir has enough free space relative to the volume size.
+ /// Logs a warning if free space is less than 4 times the volume size.
+ ///
+ private void CheckTempDirFreeSpace()
+ {
+ try
+ {
+ // Get the tempdir path
+ var tempDir = TempFolder.SystemTempPath;
+
+ // Get free space in tempdir using the Utility method
+ var spaceInfo = Library.Utility.Utility.GetFreeSpaceForPath(tempDir);
+ if (spaceInfo == null)
+ {
+ // Could not determine free space, skip the check
+ return;
+ }
+
+ // Get the volume size from options
+ var volumeSize = m_options.VolumeSize;
+
+ // Check if free space is less than 4 times the volume size
+ if (spaceInfo.Value.FreeSpace < volumeSize * Library.Utility.Utility.VOLUME_SIZE_FREE_SPACE_MULTIPLIER)
+ {
+ Logging.Log.WriteWarningMessage(LOGTAG, "TempDirLowSpace", null,
+ $"The temporary folder '{tempDir}' has limited free space ({Library.Utility.Utility.FormatSizeString(spaceInfo.Value.FreeSpace)}). " +
+ $"It is recommended to have at least {Library.Utility.Utility.VOLUME_SIZE_FREE_SPACE_MULTIPLIER} times the volume size ({Library.Utility.Utility.FormatSizeString(volumeSize * Library.Utility.Utility.VOLUME_SIZE_FREE_SPACE_MULTIPLIER)}) available for optimal restore operation.");
+ }
+ }
+ catch
+ {
+ // Ignore errors during free space check
+ }
+ }
+
///
/// Gets the compression module by parsing the filename
///
@@ -106,6 +142,9 @@ namespace Duplicati.Library.Main.Operation
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_Begin);
+ // Check tempdir free space before starting restore
+ CheckTempDirFreeSpace();
+
// Set the restore path in the results for logging purposes
var restorePath = restoreDestination.TargetDestination;
m_result.RestorePath = string.IsNullOrEmpty(restorePath)
diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs
index 1a4802f77..0f87a2865 100644
--- a/Duplicati/Library/Utility/Utility.cs
+++ b/Duplicati/Library/Utility/Utility.cs
@@ -65,6 +65,11 @@ namespace Duplicati.Library.Utility
///
public const FileAttributes ATTRIBUTE_ERROR = (FileAttributes)(1 << 30);
+ ///
+ /// The multiplier used to determine if there is enough free space on the temporary volume
+ ///
+ public const long VOLUME_SIZE_FREE_SPACE_MULTIPLIER = 10;
+
///
/// The callback delegate type used to collecting file information
///
@@ -2064,5 +2069,91 @@ namespace Duplicati.Library.Utility
return value;
}
+ ///
+ /// Gets the free and total space available for the specified path.
+ /// Uses the same approach as FileBackend.GetQuotaInfoAsync.
+ ///
+ /// The path to check
+ /// A tuple with (freeSpace, totalSpace) in bytes, or null if it could not be determined
+ public static (long FreeSpace, long TotalSpace)? GetFreeSpaceForPath(string path)
+ {
+ try
+ {
+ // Get the drive info for the path
+ var root = Path.GetPathRoot(path);
+ if (string.IsNullOrEmpty(root))
+ return null;
+
+ // On Windows, DriveInfo is only valid for lettered drives
+ if (OperatingSystem.IsWindows() && root.Length > 0 && char.IsLetter(root[0]))
+ {
+ try
+ {
+ var driveInfo = new DriveInfo(root);
+ if (driveInfo.TotalSize > 0)
+ return (driveInfo.AvailableFreeSpace, driveInfo.TotalSize);
+ }
+ catch (ArgumentException)
+ {
+ // Fall through to Win32 API fallback
+ }
+
+ // Fallback to Win32 API for UNC paths and other cases
+ return GetDiskFreeSpaceWin32(path);
+ }
+ else if (!OperatingSystem.IsWindows())
+ {
+ var driveInfo = new DriveInfo(root);
+ if (driveInfo.TotalSize > 0)
+ return (driveInfo.AvailableFreeSpace, driveInfo.TotalSize);
+ }
+ else
+ {
+ // Windows but not a lettered drive (e.g., UNC path)
+ return GetDiskFreeSpaceWin32(path);
+ }
+ }
+ catch
+ {
+ // Ignore errors and return null
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the free and total disk space using the Win32 API's GetDiskFreeSpaceEx function.
+ ///
+ /// Directory
+ /// A tuple with (freeSpace, totalSpace) in bytes, or null if it could not be determined
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ [SupportedOSPlatform("windows")]
+ private static (long FreeSpace, long TotalSpace)? GetDiskFreeSpaceWin32(string directory)
+ {
+ if (!OperatingSystem.IsWindows())
+ return null;
+
+ try
+ {
+ if (GetDiskFreeSpaceEx(directory, out var available, out var total, out _))
+ return ((long)available, (long)total);
+ }
+ catch
+ {
+ // Ignore errors
+ }
+
+ return null;
+ }
+
+ [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
+ [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
+ [SupportedOSPlatform("windows")]
+ private static extern bool GetDiskFreeSpaceEx(
+ string lpDirectoryName,
+ out ulong lpFreeBytesAvailable,
+ out ulong lpTotalNumberOfBytes,
+ out ulong lpTotalNumberOfFreeBytes);
+
}
}
diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs
index 7b520f1c2..c577e62b9 100644
--- a/Duplicati/Server/Program.cs
+++ b/Duplicati/Server/Program.cs
@@ -639,6 +639,9 @@ namespace Duplicati.Server
connection.DismissNotification(n.ID);
}
+ // Check tempdir free space against volume sizes
+ CheckTempDirFreeSpace(connection, commandlineOptions);
+
// Emit warnings if the application has been updated
if (connection.ApplicationSettings.LastConfigIssueCheckVersion != UpdaterManager.SelfVersion.Version)
{
@@ -688,6 +691,132 @@ namespace Duplicati.Server
}
}
+ ///
+ /// Checks if the tempdir has enough free space relative to the volume sizes of configured backups.
+ /// Emits a warning if free space is less than 4 times the volume size.
+ ///
+ /// The database connection
+ /// The commandline options
+ private static void CheckTempDirFreeSpace(Connection connection, Dictionary commandlineOptions)
+ {
+ const string TEMPDIR_WARNING_ACTION = "config:issue:tempdir-low-space";
+
+ // Get the tempdir path - check Connection settings first, then fall back to system default
+ var tempDir = TempFolder.SystemTempPath;
+ var serverSettings = connection.GetSettings(Connection.ANY_BACKUP_ID);
+ var tempDirSetting = serverSettings?.FirstOrDefault(s =>
+ string.Equals(s.Name, "tempdir", StringComparison.OrdinalIgnoreCase) || string.Equals(s.Name, "--tempdir", StringComparison.OrdinalIgnoreCase));
+ if (tempDirSetting != null && !string.IsNullOrEmpty(tempDirSetting.Value))
+ tempDir = tempDirSetting.Value;
+
+ // Get free space in tempdir using the Utility method
+ var spaceInfo = Library.Utility.Utility.GetFreeSpaceForPath(tempDir);
+ // Could not determine free space, skip the check
+ if (spaceInfo == null)
+ return;
+
+ // Get the maximum volume size from all configured backups
+ var maxVolumeSize = GetMaxVolumeSize(connection, commandlineOptions);
+
+ // Check if free space is less than 4 times the volume size
+ if (spaceInfo.Value.FreeSpace < maxVolumeSize * Library.Utility.Utility.VOLUME_SIZE_FREE_SPACE_MULTIPLIER)
+ {
+ // Emit warning
+ connection.RegisterNotification(
+ Serialization.NotificationType.Warning,
+ "Low temporary folder space",
+ $"The temporary folder '{tempDir}' has limited free space ({Library.Utility.Utility.FormatSizeString(spaceInfo.Value.FreeSpace)}). " +
+ $"It is recommended to have at least {Library.Utility.Utility.VOLUME_SIZE_FREE_SPACE_MULTIPLIER} times the volume size ({Library.Utility.Utility.FormatSizeString(maxVolumeSize * Library.Utility.Utility.VOLUME_SIZE_FREE_SPACE_MULTIPLIER)}) available for optimal operation.",
+ null,
+ null,
+ TEMPDIR_WARNING_ACTION,
+ null,
+ "TempDirLowSpace",
+ null,
+ (self, all) =>
+ {
+ return all.FirstOrDefault(x => x.Action == TEMPDIR_WARNING_ACTION) ?? self;
+ }
+ );
+ }
+ else
+ {
+ // Clear any existing warning
+ var existingNotifications = connection.GetNotifications().Where(x => x.Action == TEMPDIR_WARNING_ACTION).ToList();
+ foreach (var n in existingNotifications)
+ connection.DismissNotification(n.ID);
+ }
+ }
+
+ ///
+ /// Gets the maximum volume size from all configured backups.
+ ///
+ /// The database connection
+ /// The commandline options
+ /// The maximum volume size in bytes
+ private static long GetMaxVolumeSize(Connection connection, Dictionary commandlineOptions)
+ {
+ const string DEFAULT_VOLUME_SIZE = "50mb";
+ long maxVolumeSize = 0;
+
+ // Check if there's a global dblock-size in commandline options
+ if (commandlineOptions.TryGetValue("dblock-size", out var globalDblockSize) && !string.IsNullOrEmpty(globalDblockSize))
+ {
+ try
+ {
+ maxVolumeSize = Sizeparser.ParseSize(globalDblockSize, "mb");
+ }
+ catch
+ {
+ // Ignore parsing errors
+ }
+ }
+
+ // Check if there's a global dblock-size in the database settings
+ var serverSettings = connection.GetSettings(Connection.ANY_BACKUP_ID);
+ var defaultDblockSize = serverSettings?.FirstOrDefault(s => string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase) || string.Equals(s.Name, "--dblock-size", StringComparison.OrdinalIgnoreCase));
+ if (!string.IsNullOrEmpty(defaultDblockSize?.Value))
+ try
+ {
+ maxVolumeSize = Math.Max(Sizeparser.ParseSize(defaultDblockSize.Value, "mb"), maxVolumeSize);
+ }
+ catch
+ {
+ // Ignore parsing errors
+ }
+
+ // If no global setting, use default
+ maxVolumeSize = Math.Max(maxVolumeSize, Sizeparser.ParseSize(DEFAULT_VOLUME_SIZE, "mb"));
+
+ // Check all configured backups for their volume sizes
+ foreach (var backup in connection.Backups)
+ {
+ try
+ {
+ // Get settings for this backup
+ var backupId = long.Parse(backup.ID);
+ var settings = connection.GetSettings(backupId);
+
+ // Look for dblock-size setting
+ var dblockSizeSetting = settings?.FirstOrDefault(s =>
+ string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase) || string.Equals(s.Name, "--dblock-size", StringComparison.OrdinalIgnoreCase));
+
+ if (dblockSizeSetting != null && !string.IsNullOrEmpty(dblockSizeSetting.Value))
+ {
+ var backupVolumeSize = Sizeparser.ParseSize(dblockSizeSetting.Value, "mb");
+ if (backupVolumeSize > maxVolumeSize)
+ maxVolumeSize = backupVolumeSize;
+ }
+ }
+ catch
+ {
+ // Ignore errors for individual backups
+ }
+ }
+
+ return maxVolumeSize;
+ }
+
///
/// Creates the application instance to ensure a single instance is running for the current user
///