Check free temporary space
This PR adds checks for free temporary space when starting the server and when restoring. On systems that have limited space in the temp folder a warning will now be shown. This is intended to capture issues on some Docker systems where /tmp is mounted in memory instead of being disk backed. It will also detect the issue on other systems that are space constrained.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drive or volume information for the specified path.
|
||||
/// </summary>
|
||||
/// <returns>The drive or volume information, or null if it could not be determined.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IQuotaInfo?> 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<IQuotaInfo?>(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<IQuotaInfo?>(GetDiskFreeSpace(m_path));
|
||||
}
|
||||
var spaceInfo = Utility.Utility.GetFreeSpaceForPath(m_path);
|
||||
if (spaceInfo != null)
|
||||
return Task.FromResult<IQuotaInfo?>(new QuotaInfo(spaceInfo.Value.TotalSpace, spaceInfo.Value.FreeSpace));
|
||||
|
||||
return Task.FromResult<IQuotaInfo?>(null);
|
||||
}
|
||||
@@ -536,50 +474,6 @@ namespace Duplicati.Library.Backend
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the disk free space using the Win32 API's GetDiskFreeSpaceEx function.
|
||||
/// </summary>
|
||||
/// <param name="directory">Directory</param>
|
||||
/// <returns>Quota info</returns>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for working with Windows drives.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static class WindowsDriveHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the free disk space for the specified directory using the Win32 API.
|
||||
/// </summary>
|
||||
/// <param name="lpDirectoryName">The directory name to check</param>
|
||||
/// <param name="lpFreeBytesAvailable">The available free bytes</param>
|
||||
/// <param name="lpTotalNumberOfBytes">The total number of bytes</param>
|
||||
/// <param name="lpTotalNumberOfFreeBytes">The total number of free bytes</param>
|
||||
/// <returns></returns>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the target file size matches the source file size.
|
||||
/// </summary>
|
||||
|
||||
@@ -51,6 +51,42 @@ namespace Duplicati.Library.Main.Operation
|
||||
m_result = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the compression module by parsing the filename
|
||||
/// </summary>
|
||||
@@ -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)
|
||||
|
||||
@@ -65,6 +65,11 @@ namespace Duplicati.Library.Utility
|
||||
/// </summary>
|
||||
public const FileAttributes ATTRIBUTE_ERROR = (FileAttributes)(1 << 30);
|
||||
|
||||
/// <summary>
|
||||
/// The multiplier used to determine if there is enough free space on the temporary volume
|
||||
/// </summary>
|
||||
public const long VOLUME_SIZE_FREE_SPACE_MULTIPLIER = 10;
|
||||
|
||||
/// <summary>
|
||||
/// The callback delegate type used to collecting file information
|
||||
/// </summary>
|
||||
@@ -2064,5 +2069,91 @@ namespace Duplicati.Library.Utility
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free and total space available for the specified path.
|
||||
/// Uses the same approach as FileBackend.GetQuotaInfoAsync.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to check</param>
|
||||
/// <returns>A tuple with (freeSpace, totalSpace) in bytes, or null if it could not be determined</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free and total disk space using the Win32 API's GetDiskFreeSpaceEx function.
|
||||
/// </summary>
|
||||
/// <param name="directory">Directory</param>
|
||||
/// <returns>A tuple with (freeSpace, totalSpace) in bytes, or null if it could not be determined</returns>
|
||||
[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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="connection">The database connection</param>
|
||||
/// <param name="commandlineOptions">The commandline options</param>
|
||||
private static void CheckTempDirFreeSpace(Connection connection, Dictionary<string, string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum volume size from all configured backups.
|
||||
/// </summary>
|
||||
/// <param name="connection">The database connection</param>
|
||||
/// <param name="commandlineOptions">The commandline options</param>
|
||||
/// <returns>The maximum volume size in bytes</returns>
|
||||
private static long GetMaxVolumeSize(Connection connection, Dictionary<string, string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the application instance to ensure a single instance is running for the current user
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user