2025-02-09 16:14:22 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-05-21 15:10:27 +02: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
2025-02-09 16:14:22 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-21 15:10:27 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-02-09 16:14:22 +01:00
// all copies or substantial portions of the Software.
2025-05-21 15:10:27 +02: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
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2016-01-28 22:24:51 +01:00
using System ;
using CoCoL ;
using System.Threading.Tasks ;
using System.IO ;
using System.Collections.Generic ;
using Duplicati.Library.Interface ;
2016-02-09 09:17:31 +01:00
using Duplicati.Library.Main.Operation.Common ;
2025-06-19 11:04:09 +02:00
using System.Threading ;
2018-09-07 09:44:23 +02:00
2016-01-28 22:24:51 +01:00
namespace Duplicati.Library.Main.Operation.Backup
{
2016-02-25 19:15:51 +01:00
/// <summary>
/// This class processes paths for metadata and emits the metadata blocks for storage.
/// Folders and symlinks in the database, and paths are forwarded to be scanned for changes
/// </summary>
internal static class MetadataPreProcess
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
private static readonly string FILELOGTAG = Logging . Log . LogTagFromType ( typeof ( MetadataPreProcess )) + ".FileEntry" ;
2016-02-18 16:46:53 +01:00
public class FileEntry
2016-01-28 22:24:51 +01:00
{
2016-02-18 16:46:53 +01:00
// From input
2025-02-21 16:06:55 +01:00
public ISourceProviderEntry Entry ;
2016-02-18 16:46:53 +01:00
2018-06-14 10:12:24 +02:00
// Split
public long PathPrefixID ;
public string Filename ;
2016-02-18 16:46:53 +01:00
// From database
public long OldId ;
2016-01-28 22:24:51 +01:00
public DateTime OldModified ;
public long LastFileSize ;
public string OldMetaHash ;
public long OldMetaSize ;
2016-02-18 16:46:53 +01:00
// From filedata
2016-01-28 22:24:51 +01:00
public DateTime LastWrite ;
public FileAttributes Attributes ;
2016-02-18 16:46:53 +01:00
// After processing metadata
public IMetahash MetaHashAndSize ;
public bool MetadataChanged ;
2025-02-06 15:31:20 +01:00
public bool TimestampChanged ;
2016-01-28 22:24:51 +01:00
}
2024-12-09 13:55:22 +01:00
2025-06-19 11:04:09 +02:00
public static Task Run ( Channels channels , Options options , BackupDatabase database , long lastfilesetid , ITaskReader taskreader )
2016-02-25 19:15:51 +01:00
{
return AutomationExtensions . RunTask ( new
{
2024-12-09 13:55:22 +01:00
Input = channels . SourcePaths . AsRead (),
StreamBlockChannel = channels . StreamBlock . AsWrite (),
Output = channels . ProcessedFiles . AsWrite (),
2016-02-25 19:15:51 +01:00
},
2024-12-09 13:55:22 +01:00
2016-02-25 19:15:51 +01:00
async self =>
{
var emptymetadata = Utility . WrapMetadata ( new Dictionary < string , string >(), options );
2018-06-14 10:12:24 +02:00
var prevprefix = new KeyValuePair < string , long >( null , - 1 );
2016-02-25 19:15:51 +01:00
2018-08-11 11:48:19 +02:00
var CHECKFILETIMEONLY = options . CheckFiletimeOnly ;
var DISABLEFILETIMECHECK = options . DisableFiletimeCheck ;
2016-02-25 19:15:51 +01:00
while ( true )
{
2025-02-09 16:14:22 +01:00
var entry = await self . Input . ReadAsync ();
2016-01-28 22:24:51 +01:00
2024-12-18 08:27:59 +01:00
// We ignore the stop signal, but not the pause and terminate
2025-06-19 11:04:09 +02:00
await taskreader . ProgressRendevouz (). ConfigureAwait ( false );
2024-12-18 08:27:59 +01:00
2016-02-25 19:15:51 +01:00
var lastwrite = new DateTime ( 0 , DateTimeKind . Utc );
2025-02-09 16:14:22 +01:00
var attributes = entry . IsFolder
? FileAttributes . Directory
: FileAttributes . Normal ;
2024-12-09 13:55:22 +01:00
try
{
2025-02-09 16:14:22 +01:00
lastwrite = entry . LastModificationUtc ;
2016-02-25 19:15:51 +01:00
}
2024-12-09 13:55:22 +01:00
catch ( Exception ex )
2016-02-25 19:15:51 +01:00
{
2025-05-20 14:26:10 +02:00
LogExceptionHelper . LogCommonWarning ( ex , FILELOGTAG , "TimestampReadFailed" , entry . Path , "Failed to read timestamp on \"{0}\"" );
2016-02-25 19:15:51 +01:00
}
2016-01-28 22:24:51 +01:00
2024-12-09 13:55:22 +01:00
try
{
2025-02-09 16:14:22 +01:00
attributes = entry . Attributes ;
2016-02-25 19:15:51 +01:00
}
2024-12-09 13:55:22 +01:00
catch ( Exception ex )
2016-02-25 19:15:51 +01:00
{
2025-05-20 14:26:10 +02:00
LogExceptionHelper . LogCommonWarning ( ex , FILELOGTAG , "AttributeReadFailed" , entry . Path , "Failed to read attributes on \"{0}\"" );
2016-02-25 19:15:51 +01:00
}
// If we only have metadata, stop here
2025-06-19 11:04:09 +02:00
if ( await ProcessMetadata ( entry , attributes , lastwrite , options , emptymetadata , database , self . StreamBlockChannel , taskreader . ProgressToken ). ConfigureAwait ( false ))
2016-02-25 19:15:51 +01:00
{
try
{
2025-02-09 16:14:22 +01:00
var split = Database . LocalDatabase . SplitIntoPrefixAndName ( entry . Path );
2018-06-14 10:12:24 +02:00
long prefixid ;
if ( string . Equals ( prevprefix . Key , split . Key , StringComparison . Ordinal ))
prefixid = prevprefix . Value ;
else
{
2025-06-19 11:04:09 +02:00
prefixid = await database . GetOrCreatePathPrefix ( split . Key , taskreader . ProgressToken );
2018-06-14 10:12:24 +02:00
prevprefix = new KeyValuePair < string , long >( split . Key , prefixid );
}
2018-08-11 11:48:19 +02:00
if ( CHECKFILETIMEONLY || DISABLEFILETIMECHECK )
2017-05-30 00:35:31 +02:00
{
2025-06-19 11:04:09 +02:00
var tmp = await database . GetFileLastModifiedAsync ( prefixid , split . Value , lastfilesetid , false , taskreader . ProgressToken );
2019-08-02 13:54:30 -04:00
await self . Output . WriteAsync ( new FileEntry
2019-07-24 15:02:30 -04:00
{
2018-08-11 11:48:19 +02:00
OldId = tmp . Item1 ,
2025-02-09 16:14:22 +01:00
Entry = entry ,
2018-06-14 10:12:24 +02:00
PathPrefixID = prefixid ,
Filename = split . Value ,
2017-05-30 00:35:31 +02:00
Attributes = attributes ,
LastWrite = lastwrite ,
2018-08-11 11:48:19 +02:00
OldModified = tmp . Item2 ,
2019-07-24 15:02:30 -04:00
LastFileSize = tmp . Item3 ,
2017-05-30 00:35:31 +02:00
OldMetaHash = null ,
OldMetaSize = - 1
});
}
else
{
2025-06-19 11:04:09 +02:00
var res = await database . GetFileEntryAsync ( prefixid , split . Value , lastfilesetid , taskreader . ProgressToken );
2019-08-02 13:48:50 -04:00
await self . Output . WriteAsync ( new FileEntry
2019-07-24 15:02:30 -04:00
{
2017-05-30 00:35:31 +02:00
OldId = res == null ? - 1 : res . id ,
2025-02-09 16:14:22 +01:00
Entry = entry ,
2018-06-14 10:12:24 +02:00
PathPrefixID = prefixid ,
Filename = split . Value ,
2017-05-30 00:35:31 +02:00
Attributes = attributes ,
LastWrite = lastwrite ,
OldModified = res == null ? new DateTime ( 0 ) : res . modified ,
LastFileSize = res == null ? - 1 : res . filesize ,
OldMetaHash = res == null ? null : res . metahash ,
OldMetaSize = res == null ? - 1 : res . metasize
});
}
2016-02-25 19:15:51 +01:00
}
2019-07-24 15:02:30 -04:00
catch ( Exception ex )
2016-02-25 19:15:51 +01:00
{
2024-12-18 08:27:59 +01:00
if ( ex . IsRetiredException ())
2019-08-02 13:48:50 -04:00
continue ;
2024-12-18 08:27:59 +01:00
2025-05-20 14:26:10 +02:00
LogExceptionHelper . LogCommonWarning ( ex , FILELOGTAG , "ProcessingMetadataFailed" , entry . Path );
2016-02-25 19:15:51 +01:00
}
}
}
});
2016-01-28 22:24:51 +01:00
}
2016-02-25 19:15:51 +01:00
/// <summary>
/// Processes the metadata for the given path.
/// </summary>
2025-06-19 11:04:09 +02:00
/// <param name="entry">The source provider entry to process.</param>
/// <param name="attributes">The file attributes of the entry.</param>
/// <param name="lastwrite">The last write timestamp of the entry.</param>
/// <param name="options">The options for the backup operation.</param>
/// <param name="emptymetadata">An empty metadata object to use if no metadata is generated.</param>
/// <param name="database">The backup database to use for storing metadata.</param>
/// <param name="streamblockchannel">The channel to write stream blocks to.</param>
/// <param name="cancellationToken">The cancellation token to use for the operation.</param>
2016-02-25 19:15:51 +01:00
/// <returns><c>True</c> if the path should be submitted to more analysis, <c>false</c> if there is nothing else to do</returns>
2025-06-19 11:04:09 +02:00
private static async Task < bool > ProcessMetadata ( ISourceProviderEntry entry , FileAttributes attributes , DateTime lastwrite , Options options , IMetahash emptymetadata , BackupDatabase database , IWriteChannel < StreamBlock > streamblockchannel , CancellationToken cancellationToken )
2016-01-28 22:24:51 +01:00
{
2025-02-09 16:14:22 +01:00
if ( entry . IsSymlink )
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
// Not all reparse points are symlinks.
// For example, on Windows 10 Fall Creator's Update, the OneDrive folder (and all subfolders)
// are reparse points, which allows the folder to hook into the OneDrive service and download things on-demand.
2020-12-06 10:04:44 -08:00
// If we can't find a symlink target for the current path, we won't treat it as a symlink.
string symlinkTarget = null ;
try
{
2025-02-09 16:14:22 +01:00
symlinkTarget = entry . SymlinkTarget ;
2020-12-06 10:04:44 -08:00
}
catch ( Exception ex )
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteExplicitMessage ( FILELOGTAG , "SymlinkTargetReadFailure" , ex , "Failed to read symlink target for path: {0}" , entry . Path );
2020-12-06 10:04:44 -08:00
}
2018-04-11 23:02:47 +02:00
if (! string . IsNullOrWhiteSpace ( symlinkTarget ))
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
if ( options . SymlinkPolicy == Options . SymlinkStrategy . Ignore )
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILELOGTAG , "IgnoreSymlink" , "Ignoring symlink {0}" , entry . Path );
2018-04-11 23:02:47 +02:00
return false ;
}
2016-01-28 22:24:51 +01:00
2018-04-11 23:02:47 +02:00
if ( options . SymlinkPolicy == Options . SymlinkStrategy . Store )
2016-08-08 15:03:42 +02:00
{
2025-02-09 16:14:22 +01:00
var metadata = MetadataGenerator . GenerateMetadata ( entry , attributes , options );
2016-08-08 15:03:42 +02:00
2018-04-11 23:02:47 +02:00
if (! metadata . ContainsKey ( "CoreSymlinkTarget" ))
metadata [ "CoreSymlinkTarget" ] = symlinkTarget ;
2016-01-28 22:24:51 +01:00
2018-04-11 23:02:47 +02:00
var metahash = Utility . WrapMetadata ( metadata , options );
2025-06-19 11:04:09 +02:00
await AddSymlinkToOutputAsync ( entry . Path , DateTime . UtcNow , metahash , database , streamblockchannel , cancellationToken ). ConfigureAwait ( false );
2016-01-28 22:24:51 +01:00
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILELOGTAG , "StoreSymlink" , "Stored symlink {0}" , entry . Path );
2018-04-11 23:02:47 +02:00
// Don't process further
return false ;
}
}
else
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILELOGTAG , "FollowingEmptySymlink" , "Treating empty symlink as regular path {0}" , entry . Path );
2016-01-28 22:24:51 +01:00
}
}
2025-02-09 16:14:22 +01:00
2016-01-28 22:24:51 +01:00
if (( attributes & FileAttributes . Directory ) == FileAttributes . Directory )
{
IMetahash metahash ;
2018-08-11 11:48:19 +02:00
if (! options . SkipMetadata )
2016-01-28 22:24:51 +01:00
{
2025-02-09 16:14:22 +01:00
metahash = Utility . WrapMetadata ( MetadataGenerator . GenerateMetadata ( entry , attributes , options ), options );
2016-01-28 22:24:51 +01:00
}
else
{
2016-02-25 19:15:51 +01:00
metahash = emptymetadata ;
2016-01-28 22:24:51 +01:00
}
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILELOGTAG , "AddDirectory" , "Adding directory {0}" , entry . Path );
2025-06-19 11:04:09 +02:00
await AddFolderToOutputAsync ( entry . Path , lastwrite , metahash , database , streamblockchannel , cancellationToken ). ConfigureAwait ( false );
2016-01-28 22:24:51 +01:00
return false ;
}
// Regular file, keep going
return true ;
}
2016-10-18 13:27:36 +02:00
/// <summary>
/// Adds metadata to output, and returns the metadataset ID
/// </summary>
/// <param name="path">The path for which metadata is processed.</param>
/// <param name="meta">The metadata entry.</param>
/// <param name="database">The database connection.</param>
/// <param name="streamblockchannel">The channel to write streams to.</param>
2025-06-19 11:04:09 +02:00
/// <param name="cancellationToken">The cancellation token to use.</param>
2025-06-18 08:26:20 +02:00
/// <returns>A task that when awaited returns a tuple with the first value indicating if the metadata set was added, and the second value being the metadata ID.</returns>
2025-06-19 11:04:09 +02:00
internal static async Task <( bool , long )> AddMetadataToOutputAsync ( string path , IMetahash meta , BackupDatabase database , IWriteChannel < StreamBlock > streamblockchannel , CancellationToken cancellationToken )
2016-10-18 13:27:36 +02:00
{
StreamProcessResult res ;
using ( var ms = new MemoryStream ( meta . Blob ))
2024-12-09 13:55:22 +01:00
res = await StreamBlock . ProcessStream ( streamblockchannel , path , ms , true , CompressionHint . Default );
2016-10-18 13:27:36 +02:00
2025-06-19 11:04:09 +02:00
return await database . AddMetadatasetAsync ( res . Streamhash , res . Streamlength , res . Blocksetid , cancellationToken ). ConfigureAwait ( false );
2016-10-18 13:27:36 +02:00
}
2016-01-28 22:24:51 +01:00
/// <summary>
2024-12-09 13:55:22 +01:00
/// Adds a file to the output,
2016-01-28 22:24:51 +01:00
/// </summary>
/// <param name="filename">The name of the file to record</param>
/// <param name="lastModified">The value of the lastModified timestamp</param>
2025-06-19 11:04:09 +02:00
/// <param name="meta">The metadata to record</param>
/// <param name="database">The database to use</param>
/// <param name="streamblockchannel">The channel to write blocks to</param>
/// <param name="cancellationToken">The cancellation token to use</param>
/// <returns>A task that completes when the file has been added to the output.</returns>
private static async Task AddFolderToOutputAsync ( string filename , DateTime lastModified , IMetahash meta , BackupDatabase database , IWriteChannel < StreamBlock > streamblockchannel , CancellationToken cancellationToken )
2016-01-28 22:24:51 +01:00
{
2025-06-19 11:04:09 +02:00
var metadataid = await AddMetadataToOutputAsync ( filename , meta , database , streamblockchannel , cancellationToken ). ConfigureAwait ( false );
await database . AddDirectoryEntryAsync ( filename , metadataid . Item2 , lastModified , cancellationToken ). ConfigureAwait ( false );
2016-01-28 22:24:51 +01:00
}
/// <summary>
2024-12-09 13:55:22 +01:00
/// Adds a file to the output,
2016-01-28 22:24:51 +01:00
/// </summary>
/// <param name="filename">The name of the file to record</param>
/// <param name="lastModified">The value of the lastModified timestamp</param>
2025-06-19 11:04:09 +02:00
/// <param name="meta">The metadata ti record</param>
2018-04-11 23:02:47 +02:00
/// <param name="database">The database to use</param>
/// <param name="streamblockchannel">The channel to write blocks to</param>
2025-06-19 11:04:09 +02:00
/// <param name="cancellationToken">The cancellation token to use</param>
/// <returns>A task that completes when the file has been added to the output.</returns>
private static async Task AddSymlinkToOutputAsync ( string filename , DateTime lastModified , IMetahash meta , BackupDatabase database , IWriteChannel < StreamBlock > streamblockchannel , CancellationToken cancellationToken )
2016-01-28 22:24:51 +01:00
{
2025-06-19 11:04:09 +02:00
var metadataid = await AddMetadataToOutputAsync ( filename , meta , database , streamblockchannel , cancellationToken ). ConfigureAwait ( false );
await database . AddSymlinkEntryAsync ( filename , metadataid . Item2 , lastModified , cancellationToken ). ConfigureAwait ( false );
2016-01-28 22:24:51 +01:00
}
}
}