2026-01-14 10:04:52 +01:00
// 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 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
2026-01-14 10:04:52 +01:00
{
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 />
2026-01-14 10:04:52 +01:00
public Task ClearReadOnlyAttribute ( string path , CancellationToken cancel )
{
2026-01-27 16:25:59 +01:00
VerifyPath ( path );
2026-01-14 10:04:52 +01:00
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 );
2026-01-14 10:04:52 +01:00
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 />
2026-01-14 10:04:52 +01:00
public Task DeleteFile ( string path , CancellationToken cancel )
{
2026-01-27 16:25:59 +01:00
VerifyPath ( path );
2026-01-14 10:04:52 +01:00
SystemIO . IO_OS . FileDelete ( path );
return Task . CompletedTask ;
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
2026-01-14 10:04:52 +01:00
public Task DeleteFolder ( string path , CancellationToken cancel )
{
2026-01-27 16:25:59 +01:00
VerifyPath ( path );
2026-01-14 10:04:52 +01:00
SystemIO . IO_OS . DirectoryDelete ( path , true );
return Task . CompletedTask ;
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
2026-01-14 10:04:52 +01:00
public void Dispose ()
{
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
2026-01-14 10:04:52 +01:00
public Task < bool > FileExists ( string path , CancellationToken cancel )
2026-01-27 16:25:59 +01:00
{
VerifyPath ( path );
return Task . FromResult ( SystemIO . IO_OS . FileExists ( path ));
}
2026-01-14 10:04:52 +01:00
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
2026-01-14 10:04:52 +01:00
public Task < long > GetFileLength ( string path , CancellationToken cancel )
2026-01-27 16:25:59 +01:00
{
VerifyPath ( path );
return Task . FromResult ( SystemIO . IO_OS . FileLength ( path ));
}
2026-01-14 10:04:52 +01:00
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
2026-01-14 10:04:52 +01:00
public Task < bool > HasReadOnlyAttribute ( string path , CancellationToken cancel )
{
2026-01-27 16:25:59 +01:00
VerifyPath ( path );
2026-01-14 10:04:52 +01:00
var currentAttr = SystemIO . IO_OS . GetFileAttributes ( path );
return Task . FromResult ( currentAttr . HasFlag ( FileAttributes . ReadOnly ));
}
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
2026-01-14 10:04:52 +01:00
public Task Initialize ( CancellationToken cancel )
2026-01-16 11:09:13 +01:00
=> Task . CompletedTask ;
/// <inheritdoc />
2026-01-20 10:36:37 +01:00
public Task Finalize ( Action < double >? progressCallback , CancellationToken cancel )
2026-01-16 11:09:13 +01:00
=> Task . CompletedTask ;
2026-01-14 10:04:52 +01:00
2026-01-25 11:56:28 +01:00
/// <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 />
2026-01-14 10:04:52 +01:00
public Task < Stream > OpenRead ( string path , CancellationToken cancel )
2026-01-27 16:25:59 +01:00
{
VerifyPath ( path );
return Task . FromResult < Stream >( SystemIO . IO_OS . FileOpenRead ( path ));
}
2026-01-14 10:04:52 +01:00
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
2026-01-14 10:04:52 +01:00
public Task < Stream > OpenReadWrite ( string path , CancellationToken cancel )
2026-01-27 16:25:59 +01:00
{
VerifyPath ( path );
return Task . FromResult < Stream >( SystemIO . IO_OS . FileOpenReadWrite ( path ));
}
2026-01-14 10:04:52 +01:00
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
2026-01-14 10:04:52 +01:00
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 ;
var fullPath = Path . GetFullPath ( path );
var fullTarget = Path . GetFullPath ( TargetDestination );
// Resolve the target destination once
var realTarget = GetFinalPath ( fullTarget );
// Resolve the path of the file/folder we are about to create
var realPath = GetFinalPath ( fullPath );
var relative = Path . GetRelativePath ( realTarget , realPath );
if ( relative . StartsWith ( ".." ) || Path . IsPathRooted ( relative ))
throw new UserInformationException ( $"Path traversal detected: {path} resolves outside {TargetDestination}" , "RestorePathTraversal" );
}
/// <summary>
/// Get the final resolved path, accounting for symlinks in existing segments.
/// </summary>
/// <param name="path">The path to resolve.</param>
/// <returns>The fully resolved path, with all symlinks resolved.</returns>
private string GetFinalPath ( string path )
{
if ( string . IsNullOrEmpty ( path )) return path ;
var current = Path . GetFullPath ( path );
var ghostSegments = new Stack < string >();
// 1. Walk up the tree until we find a segment that exists on disk
while (! Path . Exists ( current ))
{
var parent = Path . GetDirectoryName ( current );
// If we've reached the root and nothing exists (rare/impossible for absolute paths),
// we have to stop and return the original path.
if ( string . IsNullOrEmpty ( parent ) || parent == current )
return path ;
// Store the part that doesn't exist so we can put it back later
ghostSegments . Push ( Path . GetFileName ( current ));
current = parent ;
}
// 2. Resolve symlinks for the part of the path that actually exists
FileSystemInfo info = Directory . Exists ( current )
? new DirectoryInfo ( current )
: new FileInfo ( current );
var resolvedPath = info . ResolveLinkTarget ( returnFinalTarget : true )?. FullName ?? info . FullName ;
// 3. Re-attach the non-existent segments to the resolved base path
while ( ghostSegments . Count > 0 )
resolvedPath = Path . Combine ( resolvedPath , ghostSegments . Pop ());
return resolvedPath ;
}
2026-01-14 10:04:52 +01:00
2026-01-16 11:09:13 +01:00
/// <inheritdoc />
2026-01-14 10:04:52 +01:00
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 );
2026-01-14 10:04:52 +01:00
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
if ( metadata . TryGetValue ( "CoreSymlinkTarget" , out var k ))
{
2026-01-27 15:51:30 +01:00
if (! allowRestoreOutsideTargetDirectory )
{
var fullPath = Path . GetFullPath ( path );
var parent = Path . GetDirectoryName ( fullPath );
if (! string . IsNullOrEmpty ( parent ))
{
var target = Path . GetFullPath ( Path . Combine ( parent , k ));
var fullTarget = Path . GetFullPath ( TargetDestination );
if (! Util . AppendDirSeparator ( target ). StartsWith ( Util . AppendDirSeparator ( fullTarget ), StringComparison . Ordinal ))
{
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 );
}
}
}
2026-01-14 10:04:52 +01:00
// 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 );
}
if ( metadata . TryGetValue ( "CoreLastWritetime" , out k ) && long . TryParse ( k , out var t ))
{
if ( isDirTarget )
SystemIO . IO_OS . DirectorySetLastWriteTimeUtc ( targetpath , new DateTime ( t , DateTimeKind . Utc ));
else
SystemIO . IO_OS . FileSetLastWriteTimeUtc ( targetpath , new DateTime ( t , DateTimeKind . Utc ));
}
if ( metadata . TryGetValue ( "CoreCreatetime" , out k ) && long . TryParse ( k , out t ))
{
if ( isDirTarget )
SystemIO . IO_OS . DirectorySetCreationTimeUtc ( targetpath , new DateTime ( t , DateTimeKind . Utc ));
else
SystemIO . IO_OS . FileSetCreationTimeUtc ( targetpath , new DateTime ( t , DateTimeKind . Utc ));
}
if ( metadata . TryGetValue ( "CoreAttributes" , out k ) && Enum . TryParse < FileAttributes >( k , true , out var fa ))
SystemIO . IO_OS . SetFileAttributes ( targetpath , fa );
SystemIO . IO_OS . SetMetadata ( path , metadata , restorePermissions );
return Task . FromResult ( wrote_something );
}
}