// 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. using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Duplicati.Library.Common.IO; using Duplicati.Library.Interface; using Microsoft.Win32.SafeHandles; namespace Duplicati.Library.Snapshots { /// /// Handler for providing a snapshot like access to files and folders /// [SupportedOSPlatform("linux")] [SupportedOSPlatform("macOS")] public sealed class NoSnapshotLinux : SnapshotBase { /// /// PInvoke methods /// private static class PInvoke { [DllImport("libc", EntryPoint = "fopen", SetLastError = true)] public static extern IntPtr fopen(string path, string mode); [DllImport("libc", EntryPoint = "fclose", SetLastError = true)] public static extern int fclose(IntPtr handle); [DllImport("libc", EntryPoint = "fileno", SetLastError = true)] public static extern int fileno(IntPtr stream); [DllImport("libc", EntryPoint = "strerror", SetLastError = true)] public static extern IntPtr strerror(int errnum); public static string GetErrorMessage(int errno) { IntPtr strPtr = PInvoke.strerror(errno); return Marshal.PtrToStringAnsi(strPtr) ?? $"Unknown error: {errno}"; } } /// /// Flag indicating if advisory locks should be ignored /// private readonly bool m_ignoreAdvisoryLocks; /// /// The list of folders to create snapshots of /// private readonly IEnumerable m_sources; /// /// Initializes a new instance of the class. /// /// The list of sources to create snapshots of /// A flag indicating if advisory locks should be ignored /// A flag indicating if symlinks should be followed public NoSnapshotLinux(IEnumerable sources, bool ignoreAdvisoryLocks, bool followSymlinks) : base(followSymlinks) { m_sources = sources; m_ignoreAdvisoryLocks = ignoreAdvisoryLocks; } /// /// Gets the source folders /// public override IEnumerable SourceEntries => m_sources; /// /// Enumerates the root source files and folders /// /// The source files and folders public override IEnumerable EnumerateFilesystemEntries() { foreach (var folder in m_sources) { if (DirectoryExists(folder) || folder.EndsWith(Path.DirectorySeparatorChar)) yield return new SnapshotSourceFileEntry(this, Util.AppendDirSeparator(folder), true, true); else yield return new SnapshotSourceFileEntry(this, folder, false, true); } } /// /// Returns the symlink target if the entry is a symlink, and null otherwise /// /// The file or folder to examine /// The symlink target public override string GetSymlinkTarget(string localPath) { return SystemIO.IO_OS.GetSymlinkTarget(localPath); } /// /// Gets the metadata for the given file or folder /// /// The metadata for the given file or folder /// The file or folder to examine /// A flag indicating if the target is a symlink public override Dictionary GetMetadata(string localPath, bool isSymlink) { return SystemIO.IO_OS.GetMetadata(localPath, isSymlink, FollowSymlinks); } /// /// Gets a value indicating if the path points to a block device /// /// true if this instance is a block device; otherwise, false. /// The file or folder to examine public override bool IsBlockDevice(string localPath) { var n = PosixFile.GetFileType(SystemIOLinux.NormalizePath(localPath)); switch (n) { case PosixFile.FileType.Directory: case PosixFile.FileType.Symlink: case PosixFile.FileType.File: return false; default: return true; } } /// /// Gets a unique hardlink target ID /// /// The hardlink ID /// The file or folder to examine public override string HardlinkTargetID(string localPath) { var normalizePath = SystemIOLinux.NormalizePath(localPath); return PosixFile.GetHardlinkCount(normalizePath) <= 1 ? null : PosixFile.GetInodeTargetID(normalizePath); } /// public override string ConvertToLocalPath(string snapshotPath) { return snapshotPath; } /// public override string ConvertToSnapshotPath(string localPath) { return SystemIOLinux.NormalizePath(localPath); } /// /// In case of Linux without snapshot support, we just open the file directly with fopen /// in order to avoid the issues related to file locks, which on linux are advisory, and /// since .net 6 when using FileStream, the framework is taking the advisory as mandatory /// therefore we can't open the file with FileShare.ReadWrite. /// /// file to be opened /// public override Stream OpenRead(string localPath) { if (m_ignoreAdvisoryLocks) { var filePtr = PInvoke.fopen(localPath, "r"); var errorNo = Marshal.GetLastWin32Error(); // Surprisingly, this is to be used on linux/macos if (filePtr == IntPtr.Zero) { throw new IOException($"Unable to open file: {localPath}. Error: {errorNo} - {PInvoke.GetErrorMessage(errorNo)}"); } try { SafeFileHandle safeFileHandle = new(PInvoke.fileno(filePtr), false); return new UnixFileStream(safeFileHandle, filePtr, FileAccess.Read); } catch // Catch all exceptions and rethrow { if (filePtr != IntPtr.Zero) PInvoke.fclose(filePtr); throw; } } else { return base.OpenRead(localPath); } } /// /// Stream wrapping a file handle /// private class UnixFileStream : FileStream { IntPtr _fileHandle; SafeFileHandle _handle; public UnixFileStream(SafeFileHandle handle, IntPtr fileHandle, FileAccess access) : base(handle, access) { _fileHandle = fileHandle; _handle = handle; } protected override void Dispose(bool disposing) { if (_fileHandle != IntPtr.Zero) { PInvoke.fclose(_fileHandle); _fileHandle = IntPtr.Zero; } _handle?.Dispose(); base.Dispose(disposing); } } } }