mirror of
https://github.com/duplicati/duplicati.git
synced 2025-11-28 11:30:24 +08:00
This PR updates the check for permission denied to include EPERM and EACCES on macOS/Linux.
288 lines
No EOL
10 KiB
C#
288 lines
No EOL
10 KiB
C#
// Copyright (C) 2025, The Duplicati Team
|
|
// https://duplicati.com, hello@duplicati.com
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Duplicati.Library.Common.IO;
|
|
|
|
namespace Duplicati.Library.Utility;
|
|
|
|
/// <summary>
|
|
/// Extension methods for exceptions
|
|
/// </summary>
|
|
public static class ExceptionExtensions
|
|
{
|
|
/// <summary>
|
|
/// Flattens an exception and its inner exceptions
|
|
/// </summary>
|
|
/// <param name="ex">The exception to flatten</param>
|
|
/// <returns>An enumerable of exceptions</returns>
|
|
public static IEnumerable<Exception> FlattenException(Exception? ex)
|
|
{
|
|
if (ex == null)
|
|
yield break;
|
|
|
|
yield return ex;
|
|
|
|
if (ex is AggregateException aex)
|
|
foreach (var iex in aex.Flatten().InnerExceptions)
|
|
foreach (var iex2 in FlattenException(iex))
|
|
yield return iex2;
|
|
|
|
foreach (var iex in FlattenException(ex.InnerException))
|
|
yield return iex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an exception is a stop, cancel or timeout exception
|
|
/// </summary>
|
|
/// <param name="ex">The operation to check</param>
|
|
/// <returns><c>true</c> if the exception is a stop or cancel exception, <c>false</c> otherwise</returns>
|
|
public static bool IsAbortOrCancelException(this Exception ex)
|
|
=> ex is OperationCanceledException || ex is ThreadAbortException || ex is TaskCanceledException || ex is TimeoutException;
|
|
|
|
/// <summary>
|
|
/// Checks if an exception is a stop exception
|
|
/// </summary>
|
|
/// <param name="ex">The operation to check</param>
|
|
/// <returns><c>true</c> if the exception is a stop exception, <c>false</c> otherwise</returns>
|
|
public static bool IsAbortException(this Exception ex)
|
|
=> ex is OperationCanceledException || ex is ThreadAbortException;
|
|
|
|
/// <summary>
|
|
/// List of Windows error codes that indicate a permission denied error
|
|
/// </summary>
|
|
private static readonly int[] WindowsPermissionDeniedCodes = {
|
|
5, // ERROR_ACCESS_DENIED
|
|
19, // ERROR_WRITE_PROTECT
|
|
65, // ERROR_NETWORK_ACCESS_DENIED
|
|
82, // ERROR_CANNOT_MAKE
|
|
1314, // ERROR_PRIVILEGE_NOT_HELD
|
|
};
|
|
|
|
/// <summary>
|
|
/// List of Windows error codes that indicate a file lock error
|
|
/// </summary>
|
|
private static readonly int[] WindowsFileLockCodes = {
|
|
32, // ERROR_SHARING_VIOLATION
|
|
33 // ERROR_LOCK_VIOLATION
|
|
};
|
|
|
|
/// <summary>
|
|
/// List of Windows error codes that indicate a path not found error
|
|
/// </summary>
|
|
private static readonly int[] WindowsPathNotFoundCodes = {
|
|
2, // ERROR_FILE_NOT_FOUND
|
|
3, // ERROR_PATH_NOT_FOUND
|
|
21, // ERROR_NOT_READY (e.g., missing drive)
|
|
267 // ERROR_DIRECTORY
|
|
};
|
|
|
|
/// <summary>
|
|
/// List of Windows error codes that indicate a path too long error
|
|
/// </summary>
|
|
private static readonly int[] WindowsPathTooLongCodes = {
|
|
206, // ERROR_FILENAME_EXCED_RANGE
|
|
3 // ERROR_PATH_NOT_FOUND (used when long path causes resolution failure)
|
|
};
|
|
|
|
/// <summary>
|
|
/// List of Posix error codes that indicate a permission denied error
|
|
/// </summary>
|
|
private static readonly int[] PosixPermissionErrnos = {
|
|
1, // EPERM
|
|
13, // EACCES
|
|
30, // EROFS - Read-only file system
|
|
};
|
|
|
|
/// <summary>
|
|
/// List of Posix error codes that indicate a file lock error
|
|
/// </summary>
|
|
private static readonly int[] PosixFileLockErrnos = {
|
|
// Linux file lock errors are not always distinguishable
|
|
11 // EAGAIN (on NFS), or temporary unavailable (used for lock contention in some cases)
|
|
};
|
|
|
|
/// <summary>
|
|
/// List of Posix error codes that indicate a path not found error
|
|
/// </summary>
|
|
private static readonly int[] PosixPathNotFoundErrnos = {
|
|
2, // ENOENT - No such file or directory
|
|
20 // ENOTDIR - A path component is not a directory
|
|
};
|
|
|
|
/// <summary>
|
|
/// List of Posix error codes that indicate a path too long error
|
|
/// </summary>
|
|
private static readonly int[] PosixPathTooLongErrnos = {
|
|
36, // ENAMETOOLONG - File name too long
|
|
63 // ENAMETOOLONG (on some macOS versions)
|
|
};
|
|
|
|
|
|
/// <summary>
|
|
/// Checks if an exception is a permission denied error
|
|
/// </summary>
|
|
/// <param name="ex">The operation to check</param>
|
|
/// <returns><c>true</c> if the exception is a permission denied error, <c>false</c> otherwise</returns>
|
|
public static bool IsPermissionDeniedException(this Exception? ex)
|
|
{
|
|
if (ex is UnauthorizedAccessException)
|
|
return true;
|
|
|
|
if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
|
|
return WindowsPermissionDeniedCodes.Contains(win32Ex.NativeErrorCode);
|
|
|
|
if (ex is IOException ioEx)
|
|
{
|
|
if (ioEx.HResult == unchecked((int)0x80070005)) // E_ACCESSDENIED
|
|
return true;
|
|
|
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
|
{
|
|
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
|
PosixPermissionErrnos.Contains(innerWin32.NativeErrorCode))
|
|
return true;
|
|
|
|
var errno = ioEx.HResult & 0xFFFF;
|
|
return PosixPermissionErrnos.Contains(errno);
|
|
}
|
|
}
|
|
|
|
if (ex is PosixFile.PosixException posixEx)
|
|
{
|
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
|
{
|
|
return posixEx.Errno == Mono.Unix.Native.Errno.EPERM || posixEx.Errno == Mono.Unix.Native.Errno.EACCES;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an exception is a file lock error
|
|
/// </summary>
|
|
/// <param name="ex">The operation to check</param>
|
|
/// <returns><c>true</c> if the exception is a file lock error, <c>false</c> otherwise</returns>
|
|
public static bool IsFileLockedException(this Exception? ex)
|
|
{
|
|
if (ex is IOException ioEx)
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
// Check inner Win32Exception
|
|
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
|
WindowsFileLockCodes.Contains(innerWin32.NativeErrorCode))
|
|
return true;
|
|
|
|
// Fallback to HRESULT
|
|
var winCode = ioEx.HResult & 0xFFFF;
|
|
return WindowsFileLockCodes.Contains(winCode);
|
|
}
|
|
|
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
|
{
|
|
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
|
PosixFileLockErrnos.Contains(innerWin32.NativeErrorCode))
|
|
return true;
|
|
|
|
var errno = ioEx.HResult & 0xFFFF;
|
|
return PosixFileLockErrnos.Contains(errno);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an exception is a path not found error
|
|
/// </summary>
|
|
/// <param name="ex">The operation to check</param>
|
|
/// <returns><c>true</c> if the exception is a path not found error, <c>false</c> otherwise</returns>
|
|
public static bool IsPathNotFoundException(this Exception? ex)
|
|
{
|
|
if (ex is FileNotFoundException || ex is DirectoryNotFoundException)
|
|
return true;
|
|
|
|
if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
|
|
return WindowsPathNotFoundCodes.Contains(win32Ex.NativeErrorCode);
|
|
|
|
if (ex is IOException ioEx)
|
|
{
|
|
var code = ioEx.HResult & 0xFFFF;
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
return WindowsPathNotFoundCodes.Contains(code);
|
|
|
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
|
{
|
|
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
|
PosixPathNotFoundErrnos.Contains(innerWin32.NativeErrorCode))
|
|
return true;
|
|
|
|
return PosixPathNotFoundErrnos.Contains(code);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an exception is a path too long error
|
|
/// </summary>
|
|
/// <param name="ex">The operation to check</param>
|
|
/// <returns><c>true</c> if the exception is a path too long error, <c>false</c> otherwise</returns>
|
|
public static bool IsPathTooLongException(this Exception? ex)
|
|
{
|
|
if (ex is PathTooLongException)
|
|
return true;
|
|
|
|
if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
|
|
return WindowsPathTooLongCodes.Contains(win32Ex.NativeErrorCode);
|
|
|
|
if (ex is IOException ioEx)
|
|
{
|
|
var code = ioEx.HResult & 0xFFFF;
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
return WindowsPathTooLongCodes.Contains(code);
|
|
|
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
|
{
|
|
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
|
PosixPathTooLongErrnos.Contains(innerWin32.NativeErrorCode))
|
|
return true;
|
|
|
|
return PosixPathTooLongErrnos.Contains(code);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
} |