2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, 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.
2019-05-26 19:30:02 -07:00
using Duplicati.Library.Common ;
using Duplicati.Library.Common.IO ;
2019-02-22 21:58:40 -06:00
using Duplicati.Library.Interface ;
2009-01-14 17:29:30 +00:00
using System ;
using System.Collections.Generic ;
2022-03-18 11:03:23 -07:00
using System.IO ;
2017-10-02 23:11:28 -06:00
using System.Runtime.InteropServices ;
2019-02-22 21:58:40 -06:00
using System.Threading ;
using System.Threading.Tasks ;
2009-01-14 17:29:30 +00:00
namespace Duplicati.Library.Backend
{
2019-05-26 19:30:02 -07:00
// ReSharper disable once UnusedMember.Global
// This class is instantiated dynamically in the BackendLoader.
2014-08-16 22:14:14 +02:00
public class File : IBackend , IStreamingBackend , IQuotaEnabledBackend , IRenameEnabledBackend
2009-01-14 17:29:30 +00:00
{
2010-12-15 19:08:16 +00:00
private const string OPTION_DESTINATION_MARKER = "alternate-destination-marker" ;
private const string OPTION_ALTERNATE_PATHS = "alternate-target-paths" ;
2010-12-29 14:31:33 +00:00
private const string OPTION_MOVE_FILE = "use-move-for-put" ;
2013-10-29 09:42:16 +01:00
private const string OPTION_FORCE_REAUTH = "force-smb-authentication" ;
2022-06-14 22:44:15 +02:00
private const string OPTION_DISABLE_LENGTH_VERIFICATION = "disable-length-verification" ;
2010-12-15 19:08:16 +00:00
2018-05-23 21:18:01 -07:00
private readonly string m_path ;
2009-01-15 19:42:14 +00:00
private string m_username ;
private string m_password ;
2018-05-23 21:18:01 -07:00
private readonly bool m_moveFile ;
2011-09-24 11:39:50 +00:00
private bool m_hasAutenticated ;
2018-05-23 21:18:01 -07:00
private readonly bool m_forceReauth ;
2022-06-14 22:44:15 +02:00
private readonly bool m_verifyDestinationLength ;
2009-01-14 17:29:30 +00:00
2018-11-01 17:56:48 +01:00
private readonly byte [] m_copybuffer = new byte [ Utility . Utility . DEFAULT_BUFFER_SIZE ];
2014-11-10 18:38:18 +01:00
2019-10-19 10:56:21 -07:00
private static readonly ISystemIO systemIO = SystemIO . IO_OS ;
2014-11-10 18:38:18 +01:00
2009-01-14 17:29:30 +00:00
public File ()
{
}
public File ( string url , Dictionary < string , string > options )
{
2013-05-05 23:46:00 +02:00
var uri = new Utility . Uri ( url );
m_path = uri . HostAndPath ;
2022-03-18 11:03:23 -07:00
2013-05-05 23:46:00 +02:00
if ( options . ContainsKey ( "auth-username" ))
m_username = options [ "auth-username" ];
if ( options . ContainsKey ( "auth-password" ))
m_password = options [ "auth-password" ];
if (! string . IsNullOrEmpty ( uri . Username ))
m_username = uri . Username ;
if (! string . IsNullOrEmpty ( uri . Password ))
m_password = uri . Password ;
2022-03-18 11:03:23 -07:00
2010-05-24 09:00:03 +00:00
if (! System . IO . Path . IsPathRooted ( m_path ))
2018-11-01 17:56:48 +01:00
m_path = systemIO . PathGetFullPath ( m_path );
2009-01-15 19:42:14 +00:00
2010-12-15 19:08:16 +00:00
if ( options . ContainsKey ( OPTION_ALTERNATE_PATHS ))
{
2019-05-26 19:30:02 -07:00
List < string > paths = new List < string >
{
m_path
2018-11-01 17:56:48 +01:00
};
2010-12-15 19:08:16 +00:00
paths . AddRange ( options [ OPTION_ALTERNATE_PATHS ]. Split ( new string [] { System . IO . Path . PathSeparator . ToString () }, StringSplitOptions . RemoveEmptyEntries ));
//On windows we expand the drive letter * to all drives
2018-11-03 09:26:04 +01:00
if (! Platform . IsClientPosix )
2010-12-15 19:08:16 +00:00
{
System . IO . DriveInfo [] drives = System . IO . DriveInfo . GetDrives ();
2022-03-18 11:03:23 -07:00
2010-12-15 19:08:16 +00:00
for ( int i = 0 ; i < paths . Count ; i ++)
{
2017-11-26 10:53:14 -08:00
if ( paths [ i ]. StartsWith ( "*:" , StringComparison . Ordinal ))
2010-12-15 19:08:16 +00:00
{
string rpl_path = paths [ i ]. Substring ( 1 );
paths . RemoveAt ( i );
i --;
foreach ( System . IO . DriveInfo di in drives )
paths . Insert (++ i , di . Name [ 0 ] + rpl_path );
}
}
}
string markerfile = null ;
//If there is a marker file, we do not allow the primary target path
// to be accepted, unless it contains the marker file
if ( options . ContainsKey ( OPTION_DESTINATION_MARKER ))
{
markerfile = options [ OPTION_DESTINATION_MARKER ];
m_path = null ;
}
foreach ( string p in paths )
{
try
{
2018-11-01 17:56:48 +01:00
if ( systemIO . DirectoryExists ( p ) && ( markerfile == null || systemIO . FileExists ( systemIO . PathCombine ( p , markerfile ))))
2010-12-15 19:08:16 +00:00
{
m_path = p ;
break ;
}
}
catch
{
}
}
if ( m_path == null )
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( Strings . FileBackend . NoDestinationWithMarkerFileError ( markerfile , paths . ToArray ()), "NoDestinationWithMarker" );
2010-12-15 19:08:16 +00:00
}
2010-12-29 14:31:33 +00:00
2011-09-27 05:35:49 +00:00
m_moveFile = Utility . Utility . ParseBoolOption ( options , OPTION_MOVE_FILE );
2013-10-29 09:42:16 +01:00
m_forceReauth = Utility . Utility . ParseBoolOption ( options , OPTION_FORCE_REAUTH );
2022-06-14 22:44:15 +02:00
m_verifyDestinationLength = Utility . Utility . ParseBoolOption ( options , OPTION_DISABLE_LENGTH_VERIFICATION );
2011-09-24 11:39:50 +00:00
m_hasAutenticated = false ;
2010-12-29 14:31:33 +00:00
}
2011-09-24 11:39:50 +00:00
private void PreAuthenticate ()
2010-12-29 14:31:33 +00:00
{
try
{
2018-11-01 17:56:48 +01:00
if (! string . IsNullOrEmpty ( m_username ) && m_password != null && ! m_hasAutenticated )
2011-09-24 11:39:50 +00:00
{
2018-11-01 17:56:48 +01:00
Win32 . PreAuthenticate ( m_path , m_username , m_password , m_forceReauth );
m_hasAutenticated = true ;
2011-09-24 11:39:50 +00:00
}
2010-12-29 14:31:33 +00:00
}
catch
{ }
2011-09-24 11:39:50 +00:00
}
private string GetRemoteName ( string remotename )
{
PreAuthenticate ();
2010-12-29 14:31:33 +00:00
2018-11-01 17:56:48 +01:00
if (! systemIO . DirectoryExists ( m_path ))
2015-01-20 21:07:24 +01:00
throw new FolderMissingException ( Strings . FileBackend . FolderMissingError ( m_path ));
2010-12-29 14:31:33 +00:00
2018-11-01 17:56:48 +01:00
return systemIO . PathCombine ( m_path , remotename );
2009-01-14 17:29:30 +00:00
}
#region IBackendInterface Members
public string DisplayName
{
2009-06-14 21:51:06 +00:00
get { return Strings . FileBackend . DisplayName ; }
2009-01-14 17:29:30 +00:00
}
public string ProtocolKey
{
get { return "file" ; }
}
2017-09-25 23:17:45 -06:00
public IEnumerable < IFileEntry > List ()
2009-01-14 17:29:30 +00:00
{
2011-09-24 11:39:50 +00:00
PreAuthenticate ();
2009-01-14 17:29:30 +00:00
2018-11-01 17:56:48 +01:00
if (! systemIO . DirectoryExists ( m_path ))
2015-01-20 21:07:24 +01:00
throw new FolderMissingException ( Strings . FileBackend . FolderMissingError ( m_path ));
2010-10-23 11:40:43 +00:00
2023-11-19 20:04:37 +01:00
return systemIO . EnumerateFileEntries ( m_path );
2009-01-14 17:29:30 +00:00
}
2013-07-23 20:53:34 +02:00
#if DEBUG_RETRY
private static Random random = new Random ();
2019-02-23 09:14:54 -06:00
public async Task Put ( string remotename , System . IO . Stream stream , CancellationToken cancelToken )
2009-01-25 19:33:36 +00:00
{
2021-04-20 16:05:35 -07:00
using ( System . IO . FileStream writestream = systemIO . FileCreate ( GetRemoteName ( remotename )))
2013-07-23 20:53:34 +02:00
{
if ( random . NextDouble () > 0.6666 )
throw new Exception ( "Random upload failure" );
2019-02-23 09:14:54 -06:00
await Utility . Utility . CopyStreamAsync ( stream , writestream , cancelToken );
2013-07-23 20:53:34 +02:00
}
}
#else
2022-03-18 11:03:23 -07:00
public async Task PutAsync ( string targetFilename , Stream sourceStream , CancellationToken cancelToken )
2013-07-23 20:53:34 +02:00
{
2022-03-18 11:03:23 -07:00
string targetFilePath = GetRemoteName ( targetFilename );
long copiedBytes = 0 ;
using ( var targetStream = systemIO . FileCreate ( targetFilePath ))
copiedBytes = await Utility . Utility . CopyStreamAsync ( sourceStream , targetStream , true , cancelToken , m_copybuffer );
VerifyMatchingSize ( targetFilePath , sourceStream , copiedBytes );
2009-01-25 19:33:36 +00:00
}
2013-07-23 20:53:34 +02:00
#endif
2009-01-25 19:33:36 +00:00
2010-12-29 14:31:33 +00:00
public void Get ( string remotename , System . IO . Stream stream )
2009-01-25 19:33:36 +00:00
{
2018-11-01 17:56:48 +01:00
// FileOpenRead has flags System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read
using ( System . IO . FileStream readstream = systemIO . FileOpenRead ( GetRemoteName ( remotename )))
2014-11-10 18:38:18 +01:00
Utility . Utility . CopyStream ( readstream , stream , true , m_copybuffer );
2009-01-25 19:33:36 +00:00
}
2022-03-18 11:03:23 -07:00
public Task PutAsync ( string targetFilename , string sourceFilePath , CancellationToken cancelToken )
2009-01-14 17:29:30 +00:00
{
2022-03-18 11:03:23 -07:00
string targetFilePath = GetRemoteName ( targetFilename );
2010-12-29 14:31:33 +00:00
if ( m_moveFile )
{
2022-03-18 11:03:23 -07:00
if ( systemIO . FileExists ( targetFilePath ))
systemIO . FileDelete ( targetFilePath );
2022-06-14 22:44:15 +02:00
var sourceFileInfo = new FileInfo ( sourceFilePath );
var sourceFileLength = sourceFileInfo . Exists ? ( long? ) sourceFileInfo . Length : null ;
2022-03-18 11:03:23 -07:00
systemIO . FileMove ( sourceFilePath , targetFilePath );
2022-06-14 22:44:15 +02:00
if ( m_verifyDestinationLength )
VerifyMatchingSize ( targetFilePath , null , sourceFileLength );
2010-12-29 14:31:33 +00:00
}
else
2022-03-18 11:03:23 -07:00
{
2022-06-14 22:44:15 +02:00
systemIO . FileCopy ( sourceFilePath , targetFilePath , true );
if ( m_verifyDestinationLength )
VerifyMatchingSize ( targetFilePath , sourceFilePath );
2022-03-18 11:03:23 -07:00
}
2019-02-22 21:58:40 -06:00
return Task . FromResult ( true );
2009-01-14 17:29:30 +00:00
}
public void Get ( string remotename , string filename )
{
2018-11-01 17:56:48 +01:00
systemIO . FileCopy ( GetRemoteName ( remotename ), filename , true );
2009-01-14 17:29:30 +00:00
}
public void Delete ( string remotename )
{
2018-11-01 17:56:48 +01:00
systemIO . FileDelete ( GetRemoteName ( remotename ));
2009-01-14 17:29:30 +00:00
}
2009-03-21 12:46:54 +00:00
public IList < ICommandLineArgument > SupportedCommands
{
get
{
return new List < ICommandLineArgument >( new ICommandLineArgument [] {
2013-05-05 16:40:37 +02:00
new CommandLineArgument ( "auth-password" , CommandLineArgument . ArgumentType . Password , Strings . FileBackend . DescriptionAuthPasswordShort , Strings . FileBackend . DescriptionAuthPasswordLong ),
new CommandLineArgument ( "auth-username" , CommandLineArgument . ArgumentType . String , Strings . FileBackend . DescriptionAuthUsernameShort , Strings . FileBackend . DescriptionAuthUsernameLong ),
2015-01-20 21:07:24 +01:00
new CommandLineArgument ( OPTION_DESTINATION_MARKER , CommandLineArgument . ArgumentType . String , Strings . FileBackend . AlternateDestinationMarkerShort , Strings . FileBackend . AlternateDestinationMarkerLong ( OPTION_ALTERNATE_PATHS )),
new CommandLineArgument ( OPTION_ALTERNATE_PATHS , CommandLineArgument . ArgumentType . Path , Strings . FileBackend . AlternateTargetPathsShort , Strings . FileBackend . AlternateTargetPathsLong ( OPTION_DESTINATION_MARKER , System . IO . Path . PathSeparator )),
2010-12-29 14:31:33 +00:00
new CommandLineArgument ( OPTION_MOVE_FILE , CommandLineArgument . ArgumentType . Boolean , Strings . FileBackend . UseMoveForPutShort , Strings . FileBackend . UseMoveForPutLong ),
2013-10-29 09:42:16 +01:00
new CommandLineArgument ( OPTION_FORCE_REAUTH , CommandLineArgument . ArgumentType . Boolean , Strings . FileBackend . ForceReauthShort , Strings . FileBackend . ForceReauthLong ),
2022-06-14 22:44:15 +02:00
new CommandLineArgument ( OPTION_DISABLE_LENGTH_VERIFICATION , CommandLineArgument . ArgumentType . Boolean , Strings . FileBackend . DisableLengthVerificationShort , Strings . FileBackend . DisableLengthVerificationShort ),
2009-03-21 12:46:54 +00:00
});
}
}
public string Description
{
get
{
2009-06-14 21:51:06 +00:00
return Strings . FileBackend . Description ;
2009-03-21 12:46:54 +00:00
}
}
2010-10-23 11:40:43 +00:00
public void Test ()
{
2017-09-25 23:17:45 -06:00
this . TestList ();
2010-10-23 11:40:43 +00:00
}
public void CreateFolder ()
{
2018-11-01 17:56:48 +01:00
if ( systemIO . DirectoryExists ( m_path ))
2010-10-23 11:40:43 +00:00
throw new FolderAreadyExistedException ();
2018-11-01 17:56:48 +01:00
systemIO . DirectoryCreate ( m_path );
2010-10-23 11:40:43 +00:00
}
#endregion
2009-01-25 19:33:36 +00:00
#region IDisposable Members
2010-10-23 11:40:43 +00:00
2009-01-25 19:33:36 +00:00
public void Dispose ()
2019-05-26 19:30:02 -07:00
{
m_username = null ;
2018-11-01 17:56:48 +01:00
m_password = null ;
2009-01-25 19:33:36 +00:00
}
#endregion
2012-05-14 18:48:21 +00:00
private System . IO . DriveInfo GetDrive ()
{
string root ;
2018-11-03 09:26:04 +01:00
if ( Platform . IsClientPosix )
2012-05-14 18:48:21 +00:00
{
2018-11-01 17:56:48 +01:00
string path = Util . AppendDirSeparator ( systemIO . PathGetFullPath ( m_path ));
2012-05-14 18:48:21 +00:00
root = "/" ;
//Find longest common prefix from mounted devices
//TODO: Can trick this with symlinks, where the symlink is on one mounted volume,
// and the actual storage is on another
foreach ( System . IO . DriveInfo di in System . IO . DriveInfo . GetDrives ())
2018-10-27 12:17:07 +02:00
if ( path . StartsWith ( Util . AppendDirSeparator ( di . Name ), StringComparison . Ordinal ) && di . Name . Length > root . Length )
2012-05-14 18:48:21 +00:00
root = di . Name ;
}
else
{
2018-11-01 17:56:48 +01:00
root = systemIO . GetPathRoot ( m_path );
2012-05-14 18:48:21 +00:00
}
2017-10-02 23:11:28 -06:00
// On Windows, DriveInfo is only valid for lettered drives. (e.g., not for UNC paths and shares)
// So only attempt to get it if we aren't on Windows or if the root starts with a letter.
2018-11-02 22:13:25 +01:00
if (! Platform . IsClientWindows || ( root . Length > 0 && char . IsLetter ( root [ 0 ])))
2017-10-02 23:11:28 -06:00
{
try
{
return new System . IO . DriveInfo ( root );
}
catch ( ArgumentException )
{
// If there was a problem, fall back to returning null
}
}
return null ;
2012-05-14 18:48:21 +00:00
}
2017-09-24 17:30:52 -06:00
public IQuotaInfo Quota
2012-05-14 18:48:21 +00:00
{
get
{
2017-09-24 17:30:52 -06:00
System . IO . DriveInfo driveInfo = this . GetDrive ();
2017-10-02 23:11:28 -06:00
if ( driveInfo != null )
{
return new QuotaInfo ( driveInfo . TotalSize , driveInfo . AvailableFreeSpace );
}
2018-11-01 17:56:48 +01:00
2018-11-02 22:13:25 +01:00
if ( Platform . IsClientWindows )
2017-10-02 23:11:28 -06:00
{
// If we can't get the DriveInfo on Windows, fallback to GetFreeDiskSpaceEx
// https://stackoverflow.com/questions/2050343/programmatically-determining-space-available-from-unc-path
return GetDiskFreeSpace ( m_path );
}
2018-11-01 17:56:48 +01:00
return null ;
2012-05-14 18:48:21 +00:00
}
}
2014-08-16 22:14:14 +02:00
2018-02-26 11:37:10 +01:00
public string [] DNSName
2018-02-21 22:38:01 +01:00
{
get { return null ; }
}
2018-02-18 00:18:44 +01:00
2014-08-16 22:14:14 +02:00
public void Rename ( string oldname , string newname )
{
var source = GetRemoteName ( oldname );
var target = GetRemoteName ( newname );
2018-11-01 17:56:48 +01:00
if ( systemIO . FileExists ( target ))
systemIO . FileDelete ( target );
systemIO . FileMove ( source , target );
2014-08-16 22:14:14 +02:00
}
2017-10-02 23:11:28 -06:00
/// <summary>
/// Get the disk free space using the Win32 API's GetDiskFreeSpaceEx function.
/// </summary>
/// <param name="directory">Directory</param>
/// <returns>Quota info</returns>
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
public static QuotaInfo GetDiskFreeSpace ( string directory )
{
ulong available ;
ulong total ;
2019-04-16 21:35:02 -07:00
if ( WindowsDriveHelper . GetDiskFreeSpaceEx ( directory , out available , out total , out _ ))
2017-10-02 23:11:28 -06:00
{
return new QuotaInfo (( long ) total , ( long ) available );
}
else
{
return null ;
}
}
private static class WindowsDriveHelper
{
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetDiskFreeSpaceEx (
string lpDirectoryName ,
out ulong lpFreeBytesAvailable ,
out ulong lpTotalNumberOfBytes ,
out ulong lpTotalNumberOfFreeBytes );
}
2022-03-18 11:03:23 -07:00
private static void VerifyMatchingSize ( string targetFilePath , string sourceFilePath )
{
try
{
var targetFileInfo = new FileInfo ( targetFilePath );
if (! targetFileInfo . Exists )
throw new FileMissingException ( $"Target file does not exist. Target: {targetFilePath}" );
var sourceFileInfo = new FileInfo ( sourceFilePath );
if (! sourceFileInfo . Exists )
throw new FileMissingException ( $"Source file does not exist. Source: {sourceFilePath}" );
if ( targetFileInfo . Length != sourceFileInfo . Length )
throw new FileMissingException ( $"Target file size ({targetFileInfo.Length:n0}) is different from source file size ({sourceFileInfo.Length:n0}). Target: {targetFilePath}" );
}
catch
{
try { System . IO . File . Delete ( targetFilePath ); } catch { }
throw ;
}
}
private static void VerifyMatchingSize ( string targetFilePath , Stream sourceStream , long? expectedLength )
{
try
{
var targetFileInfo = new FileInfo ( targetFilePath );
if (! targetFileInfo . Exists )
throw new FileMissingException ( $"Target file does not exist. Target: {targetFilePath}" );
2022-06-14 22:44:15 +02:00
bool isStreamPostion = false ;
long? sourceStreamLength = sourceStream == null ? null : Utility . Utility . GetStreamLength ( sourceStream , out isStreamPostion );
2022-03-18 11:03:23 -07:00
if ( sourceStreamLength . HasValue && targetFileInfo . Length != sourceStreamLength . Value )
throw new FileMissingException ( $"Target file size ({targetFileInfo.Length:n0}) is different from the source length ({sourceStreamLength.Value:n0}){(isStreamPostion ? " - ending stream position ) " : "")}. Target: {targetFilePath}" );
if ( expectedLength . HasValue && targetFileInfo . Length != expectedLength . Value )
throw new FileMissingException ( $"Target file size ({targetFileInfo.Length:n0}) is different from the expected length ({expectedLength.Value:n0}). Target: {targetFilePath}" );
}
catch
{
try { System . IO . File . Delete ( targetFilePath ); } catch { }
throw ;
}
}
2009-01-14 17:29:30 +00:00
}
}