2026-04-16 15:21:24 +02:00
// Copyright (C) 2026, The Duplicati Team
2026-01-14 10:04:52 +01:00
// 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
2026-01-14 10:04:52 +01:00
// 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
2026-01-14 10:04:52 +01:00
// 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
2026-01-14 10:04:52 +01:00
// 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 ;
2026-02-05 15:17:14 +01:00
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-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
2026-02-17 10:37:08 +01:00
if ( metadata . TryGetValue ( "CoreSymlinkTarget" , out var k ) && ! string . IsNullOrWhiteSpace ( k ))
2026-01-14 10:04:52 +01:00
{
2026-02-05 15:17:14 +01:00
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 ));
2026-02-05 15:17:14 +01:00
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 );
}
}
}
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 );
}
2026-02-09 11:36:15 +01:00
/// <inheritdoc />
public IList < string > GetPriorityFiles ()
{
// File-based restore doesn't have special priority files
return Array . Empty < string >();
}
2026-01-14 10:04:52 +01:00
}