Files
duplicati/Duplicati/Library/SourceProvider/Builtin/FileRestoreDestinationProvider.cs
T

286 lines
11 KiB
C#
Raw Normal View History

// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2026-02-09 12:27:49 +01:00
//
// 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:
2026-02-09 12:27:49 +01:00
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
2026-02-09 12:27:49 +01:00
//
// 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 Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Duplicati.Library.Snapshots;
namespace Duplicati.Library.SourceProvider;
2026-01-27 15:51:30 +01:00
public class FileRestoreDestinationProvider(string mountedPath, bool allowRestoreOutsideTargetDirectory) : IRestoreDestinationProvider
{
private static readonly string LOGTAG = Logging.Log.LogTagFromType<FileRestoreDestinationProvider>();
private static readonly string DIRSEP = Path.DirectorySeparatorChar.ToString();
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public string TargetDestination => mountedPath;
/// <inheritdoc />
public Task ClearReadOnlyAttribute(string path, CancellationToken cancel)
{
VerifyPath(path);
var currentAttr = SystemIO.IO_OS.GetFileAttributes(path);
SystemIO.IO_OS.SetFileAttributes(path, currentAttr & ~FileAttributes.ReadOnly);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<bool> CreateFolderIfNotExists(string path, CancellationToken cancel)
{
2026-01-27 15:51:30 +01:00
VerifyPath(path);
if (SystemIO.IO_OS.DirectoryExists(path))
return Task.FromResult(false);
SystemIO.IO_OS.DirectoryCreate(path);
return Task.FromResult(true);
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task DeleteFile(string path, CancellationToken cancel)
{
VerifyPath(path);
SystemIO.IO_OS.FileDelete(path);
return Task.CompletedTask;
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task DeleteFolder(string path, CancellationToken cancel)
{
VerifyPath(path);
SystemIO.IO_OS.DirectoryDelete(path, true);
return Task.CompletedTask;
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public void Dispose()
{
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task<bool> FileExists(string path, CancellationToken cancel)
{
VerifyPath(path);
return Task.FromResult(SystemIO.IO_OS.FileExists(path));
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task<long> GetFileLength(string path, CancellationToken cancel)
{
VerifyPath(path);
return Task.FromResult(SystemIO.IO_OS.FileLength(path));
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task<bool> HasReadOnlyAttribute(string path, CancellationToken cancel)
{
VerifyPath(path);
var currentAttr = SystemIO.IO_OS.GetFileAttributes(path);
return Task.FromResult(currentAttr.HasFlag(FileAttributes.ReadOnly));
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task Initialize(CancellationToken cancel)
2026-01-16 11:09:13 +01:00
=> Task.CompletedTask;
/// <inheritdoc />
public Task Finalize(Action<double>? progressCallback, CancellationToken cancel)
2026-01-16 11:09:13 +01:00
=> Task.CompletedTask;
/// <inheritdoc />
public Task Test(CancellationToken cancellationToken)
=> SystemIO.IO_OS.DirectoryExists(mountedPath) ? Task.CompletedTask : throw new Exception($"The path {mountedPath} does not exist");
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task<Stream> OpenRead(string path, CancellationToken cancel)
{
VerifyPath(path);
return Task.FromResult<Stream>(SystemIO.IO_OS.FileOpenRead(path));
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task<Stream> OpenReadWrite(string path, CancellationToken cancel)
{
VerifyPath(path);
return Task.FromResult<Stream>(SystemIO.IO_OS.FileOpenReadWrite(path));
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task<Stream> OpenWrite(string path, CancellationToken cancel)
2026-01-27 15:51:30 +01:00
{
VerifyPath(path);
return Task.FromResult<Stream>(SystemIO.IO_OS.FileOpenWrite(path));
}
/// <summary>
/// Verify that the given path is within the target destination.
/// </summary>
/// <param name="path">The path to verify.</param>
private void VerifyPath(string path)
{
if (allowRestoreOutsideTargetDirectory || string.IsNullOrWhiteSpace(TargetDestination))
return;
if (!Util.IsPathInsideTarget(path, TargetDestination))
2026-01-27 15:51:30 +01:00
throw new UserInformationException($"Path traversal detected: {path} resolves outside {TargetDestination}", "RestorePathTraversal");
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
public Task<bool> WriteMetadata(string path, Dictionary<string, string?> metadata, bool restoreSymlinkMetadata, bool restorePermissions, CancellationToken cancel)
{
2026-01-27 15:51:30 +01:00
VerifyPath(path);
var wrote_something = false;
var isDirTarget = path.EndsWith(DIRSEP, StringComparison.Ordinal);
var targetpath = isDirTarget ? path.Substring(0, path.Length - 1) : path;
// Make the symlink first, otherwise we cannot apply metadata to it
2026-02-17 10:37:08 +01:00
if (metadata.TryGetValue("CoreSymlinkTarget", out var k) && !string.IsNullOrWhiteSpace(k))
{
if (!allowRestoreOutsideTargetDirectory && !string.IsNullOrWhiteSpace(TargetDestination))
2026-01-27 15:51:30 +01:00
{
var fullPath = Path.GetFullPath(path);
var parent = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(parent))
{
var target = Path.GetFullPath(Path.Combine(parent, k));
if (!Util.IsPathInsideTarget(target, TargetDestination))
2026-01-27 15:51:30 +01:00
{
Logging.Log.WriteWarningMessage(LOGTAG, "SymlinkTargetOutside", null, "Skipping creation of symlink {0} -> {1} because it points outside the restore target", path, k);
return Task.FromResult(false);
}
}
}
// Check if the target exists, and overwrite it if it does.
if (SystemIO.IO_OS.FileExists(targetpath))
{
SystemIO.IO_OS.FileDelete(targetpath);
}
else if (SystemIO.IO_OS.DirectoryExists(targetpath))
{
SystemIO.IO_OS.DirectoryDelete(targetpath, true);
}
SystemIO.IO_OS.CreateSymlink(targetpath, k, isDirTarget);
wrote_something = true;
}
// If the target is a folder, make sure we create it first
else if (isDirTarget && !SystemIO.IO_OS.DirectoryExists(targetpath))
SystemIO.IO_OS.DirectoryCreate(targetpath);
// Avoid setting restoring symlink metadata, as that writes the symlink target, not the symlink itself
if (!restoreSymlinkMetadata && SystemIO.IO_OS.IsSymlink(targetpath))
{
Logging.Log.WriteVerboseMessage(LOGTAG, "no-symlink-metadata-restored", "Not applying metadata to symlink: {0}", targetpath);
return Task.FromResult(wrote_something);
}
2026-07-07 14:40:39 +02:00
// The read-only attribute prevents setting timestamps on files on Windows
var clearedReadOnly = false;
if (!isDirTarget && OperatingSystem.IsWindows())
{
try
{
var currentAttr = SystemIO.IO_OS.GetFileAttributes(targetpath);
if (currentAttr.HasFlag(FileAttributes.ReadOnly))
{
SystemIO.IO_OS.SetFileAttributes(targetpath, currentAttr & ~FileAttributes.ReadOnly);
clearedReadOnly = true;
}
}
catch (Exception ex)
{
Logging.Log.WriteVerboseMessage(LOGTAG, "ClearReadOnlyBeforeMetadataFailed", ex, "Failed to clear read-only attribute before applying metadata to: {0}", targetpath);
}
}
// Applies the stored permissions/ACLs. On some platforms (notably Windows) the current
// DACL on the restored entry may deny the write access needed to set timestamps, e.g. when
// the parent directory has a protected ACL without inheritable entries, so the freshly
// created child inherits no rights. Restoring the stored permissions grants the correct
// access again. This is invoked lazily as a fallback if a timestamp write is denied.
var permissionsApplied = false;
void ApplyStoredPermissions()
{
if (permissionsApplied)
return;
SystemIO.IO_OS.SetMetadata(path, metadata, restorePermissions);
permissionsApplied = true;
}
// Sets a timestamp, retrying once after applying the stored permissions if access is denied.
void SetTimestampWithRetry(Action apply)
{
try
{
apply();
}
catch (UnauthorizedAccessException) when (!permissionsApplied)
{
Logging.Log.WriteVerboseMessage(LOGTAG, "TimestampAccessDenied", "Access denied setting timestamp on {0}, applying stored permissions and retrying", targetpath);
ApplyStoredPermissions();
apply();
}
}
if (metadata.TryGetValue("CoreLastWritetime", out k) && long.TryParse(k, out var t))
{
2026-07-07 14:40:39 +02:00
var time = new DateTime(t, DateTimeKind.Utc);
if (isDirTarget)
2026-07-07 14:40:39 +02:00
SetTimestampWithRetry(() => SystemIO.IO_OS.DirectorySetLastWriteTimeUtc(targetpath, time));
else
2026-07-07 14:40:39 +02:00
SetTimestampWithRetry(() => SystemIO.IO_OS.FileSetLastWriteTimeUtc(targetpath, time));
}
if (metadata.TryGetValue("CoreCreatetime", out k) && long.TryParse(k, out t))
{
2026-07-07 14:40:39 +02:00
var time = new DateTime(t, DateTimeKind.Utc);
if (isDirTarget)
2026-07-07 14:40:39 +02:00
SetTimestampWithRetry(() => SystemIO.IO_OS.DirectorySetCreationTimeUtc(targetpath, time));
else
2026-07-07 14:40:39 +02:00
SetTimestampWithRetry(() => SystemIO.IO_OS.FileSetCreationTimeUtc(targetpath, time));
}
if (metadata.TryGetValue("CoreAttributes", out k) && Enum.TryParse<FileAttributes>(k, true, out var fa))
2026-07-07 14:40:39 +02:00
{
// Apply the stored attributes, which also restores the read-only attribute if it was set.
SystemIO.IO_OS.SetFileAttributes(targetpath, fa);
2026-07-07 14:40:39 +02:00
}
else if (clearedReadOnly)
{
// No attributes were stored, but we cleared read-only above, so restore it.
SystemIO.IO_OS.SetFileAttributes(targetpath, SystemIO.IO_OS.GetFileAttributes(targetpath) | FileAttributes.ReadOnly);
}
2026-07-07 14:40:39 +02:00
// Apply permissions last (if not already applied by the timestamp fallback above) so the
// final ACL state matches the backup.
ApplyStoredPermissions();
return Task.FromResult(wrote_something);
}
/// <inheritdoc />
public IList<string> GetPriorityFiles()
{
// File-based restore doesn't have special priority files
return Array.Empty<string>();
}
}