// 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);
}
}
}
}