diff --git a/Duplicati/Library/Interface/USNJournalDataEntry.cs b/Duplicati/Library/Interface/USNJournalDataEntry.cs index 7970b99b5..12d1c4f99 100644 --- a/Duplicati/Library/Interface/USNJournalDataEntry.cs +++ b/Duplicati/Library/Interface/USNJournalDataEntry.cs @@ -28,21 +28,21 @@ namespace Duplicati.Library.Interface /// /// Volume of journal /// - public string Volume; + public string Volume { get; set; } /// /// Journal ID, as recorded in the journal /// - public long JournalId; + public long JournalId { get; set; } /// /// USN to start at for next fileset (backup) /// - public long NextUsn; + public long NextUsn { get; set; } /// /// A hash value representing the active exclusion filter /// - public string ConfigHash; + public string ConfigHash { get; set; } } } diff --git a/Duplicati/Library/Main/Operation/BackupHandler.cs b/Duplicati/Library/Main/Operation/BackupHandler.cs index b7c57a45e..3c5eabd2c 100644 --- a/Duplicati/Library/Main/Operation/BackupHandler.cs +++ b/Duplicati/Library/Main/Operation/BackupHandler.cs @@ -420,7 +420,7 @@ namespace Duplicati.Library.Main.Operation { try { - var journalService = new UsnJournalService(UsnErrorHandler, () => + var journalService = new UsnJournalService(snapshot, UsnErrorHandler, () => { if (m_result.TaskControlRendevouz() == TaskControlState.Stop) throw new OperationCanceledException(); @@ -436,7 +436,7 @@ namespace Duplicati.Library.Main.Operation // filter sources using USN journal, to obtain a sub-set of files / folders that // may have been modified // ReSharper disable once PossibleMultipleEnumeration - var modifiedSources = journalService.FilterSources(sources, configHash, journalData); + var modifiedSources = journalService.FilterSources(sources, filterhandler.AttributeFilter, configHash, journalData); // scan modified sources for modifications if (modifiedSources.Files.Any() || modifiedSources.Folders.Any()) @@ -477,6 +477,8 @@ namespace Duplicati.Library.Main.Operation else { // add full previous fileset, as nothing has changed + //TODO: possible optimization is to somehow skip this step if compacting is enabled, + // as all this informatino will be removed anyway. m_database.AppendFilesFromPreviousSet(m_transaction); } diff --git a/Duplicati/Library/Main/Operation/SystemInfoHandler.cs b/Duplicati/Library/Main/Operation/SystemInfoHandler.cs index 56fd95bbd..14935a15b 100644 --- a/Duplicati/Library/Main/Operation/SystemInfoHandler.cs +++ b/Duplicati/Library/Main/Operation/SystemInfoHandler.cs @@ -30,7 +30,7 @@ namespace Duplicati.Library.Main.Operation public static IEnumerable GetSystemInfo() { - yield return string.Format("Duplicati: {0} ({1})", Duplicati.Library.Utility.Utility.getEntryAssembly().FullName, System.Reflection.Assembly.GetExecutingAssembly().FullName); + yield return string.Format("Duplicati: {0} ({1})", Duplicati.Library.Utility.Utility.GetEntryAssembly().FullName, System.Reflection.Assembly.GetExecutingAssembly().FullName); yield return string.Format("Autoupdate urls: {0}", string.Join(";", Duplicati.Library.AutoUpdater.AutoUpdateSettings.URLs)); yield return string.Format("Update folder: {0}", Duplicati.Library.AutoUpdater.UpdaterManager.INSTALLDIR); diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs index 1e9f0eea4..351d90d16 100644 --- a/Duplicati/Library/Main/Options.cs +++ b/Duplicati/Library/Main/Options.cs @@ -395,7 +395,7 @@ namespace Duplicati.Library.Main { get { - return System.IO.Path.GetFileNameWithoutExtension(Library.Utility.Utility.getEntryAssembly().Location); + return System.IO.Path.GetFileNameWithoutExtension(Library.Utility.Utility.GetEntryAssembly().Location); } } diff --git a/Duplicati/Library/Modules/Builtin/ReportHelper.cs b/Duplicati/Library/Modules/Builtin/ReportHelper.cs index 4f5f145c3..1a9df0b35 100644 --- a/Duplicati/Library/Modules/Builtin/ReportHelper.cs +++ b/Duplicati/Library/Modules/Builtin/ReportHelper.cs @@ -314,7 +314,7 @@ namespace Duplicati.Library.Modules.Builtin if (m_options.ContainsKey("backup-name")) extra["backup-name"] = m_options["backup-name"]; else - extra["backup-name"] = System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.getEntryAssembly().Location); + extra["backup-name"] = System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.GetEntryAssembly().Location); } foreach (KeyValuePair kv in m_options) @@ -344,7 +344,7 @@ namespace Duplicati.Library.Modules.Builtin input = Regex.Replace(input, "\\%" + kv.Key + "\\%", kv.Value ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); if (!m_options.ContainsKey("backup-name")) - input = Regex.Replace(input, "\\%backup-name\\%", System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.getEntryAssembly().Location) ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + input = Regex.Replace(input, "\\%backup-name\\%", System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.GetEntryAssembly().Location) ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); input = Regex.Replace(input, "\\%[^\\%]+\\%", ""); return input; diff --git a/Duplicati/Library/Modules/Builtin/RunScript.cs b/Duplicati/Library/Modules/Builtin/RunScript.cs index 25db9a530..b787a088c 100644 --- a/Duplicati/Library/Modules/Builtin/RunScript.cs +++ b/Duplicati/Library/Modules/Builtin/RunScript.cs @@ -210,7 +210,7 @@ namespace Duplicati.Library.Modules.Builtin psi.EnvironmentVariables["DUPLICATI__" + kv.Key.Replace('-', '_')] = kv.Value; if (!options.ContainsKey("backup-name")) - psi.EnvironmentVariables["DUPLICATI__backup_name"] = System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.getEntryAssembly().Location); + psi.EnvironmentVariables["DUPLICATI__backup_name"] = System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.GetEntryAssembly().Location); psi.EnvironmentVariables["DUPLICATI__EVENTNAME"] = eventname; psi.EnvironmentVariables["DUPLICATI__OPERATIONNAME"] = operationname; diff --git a/Duplicati/Library/Snapshots/IChangeJournalService.cs b/Duplicati/Library/Snapshots/IChangeJournalService.cs index 15984f427..b2e3ad79d 100644 --- a/Duplicati/Library/Snapshots/IChangeJournalService.cs +++ b/Duplicati/Library/Snapshots/IChangeJournalService.cs @@ -32,10 +32,11 @@ namespace Duplicati.Library.Snapshots /// change, as specified by journalData. /// /// Sources to filter + /// Filter callback to exclude filtered items /// A hash value representing current exclusion filter. A full scan is triggered if hash has changed. /// The journal data of the previous backup (fileset) /// Filtered sources - FilterData FilterSources(IEnumerable sources, string filterHash, IEnumerable journalData); + FilterData FilterSources(IEnumerable sources, Utility.Utility.EnumerationFilterDelegate filter, string filterHash, IEnumerable journalData); } /// diff --git a/Duplicati/Library/Snapshots/SnapshotBase.cs b/Duplicati/Library/Snapshots/SnapshotBase.cs index f06b13931..130222cb9 100644 --- a/Duplicati/Library/Snapshots/SnapshotBase.cs +++ b/Duplicati/Library/Snapshots/SnapshotBase.cs @@ -205,6 +205,8 @@ namespace Duplicati.Library.Snapshots return Directory.GetFiles(localFolderPath); } + #region IDisposable interface + /// public void Dispose() { @@ -213,7 +215,9 @@ namespace Duplicati.Library.Snapshots // Now suppress finalization for this object, since we've already handled our resource cleanup tasks GC.SuppressFinalize(this); - } + } + + #endregion /// /// Releases unmanaged and - optionally - managed resources diff --git a/Duplicati/Library/Snapshots/UsnJournalService.cs b/Duplicati/Library/Snapshots/UsnJournalService.cs index 45a17fad4..38bb53b9a 100644 --- a/Duplicati/Library/Snapshots/UsnJournalService.cs +++ b/Duplicati/Library/Snapshots/UsnJournalService.cs @@ -22,6 +22,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Duplicati.Library.Interface; using Duplicati.Library.Logging; @@ -35,16 +36,20 @@ namespace Duplicati.Library.Snapshots /// private static readonly string LOGTAG = Log.LogTagFromType(); + private readonly ISnapshotService m_snapshot; private readonly Utility.Utility.ReportAccessError m_errorCallback; private readonly Action m_cancelHandler; /// /// Constructor. /// + /// /// Callback for handling errors /// Callback for aborting operation. Throw OperationCancelledException() to abort. - public UsnJournalService(Utility.Utility.ReportAccessError errorCallback, Action cancelHandler) + public UsnJournalService(ISnapshotService snapshot, Utility.Utility.ReportAccessError errorCallback, + Action cancelHandler) { + m_snapshot = snapshot; m_errorCallback = errorCallback; m_cancelHandler = cancelHandler; } @@ -52,7 +57,8 @@ namespace Duplicati.Library.Snapshots #region IChangeJournalService Members /// - public FilterData FilterSources(IEnumerable sources, string filterHash, IEnumerable journalData) + public FilterData FilterSources(IEnumerable sources, Utility.Utility.EnumerationFilterDelegate filter, + string filterHash, IEnumerable journalData) { // create lookup for journal data var journalDataDict = journalData.ToDictionary(data => data.Volume); @@ -100,7 +106,7 @@ namespace Duplicati.Library.Snapshots var changedFiles = new HashSet(Utility.Utility.ClientFilenameStringComparer); var changedFolders = new HashSet(Utility.Utility.ClientFilenameStringComparer); - // iterate over sources and retrieve *renamed* directories + // obtain changed files and folders, per volume foreach (var source in sourcesPerVolume.Value) { foreach (var entry in journal.GetChangedFileSystemEntries(source, data.NextUsn)) @@ -116,10 +122,38 @@ namespace Duplicati.Library.Snapshots } } - var reducedFolderList = Utility.Utility.SimplifyFolderList(changedFolders).ToList(); - var reducedFileList = Utility.Utility.GetFilesNotInFolders(changedFiles, reducedFolderList); - result.Folders.AddRange(reducedFolderList); - result.Files.UnionWith(reducedFileList); + // At this point we have: + // - a list of folders (changedFolders) that were possibly modified + // - a list of files (changedFiles) that were possibly modified + // + // With this, we need still need to do the following: + // + // 1. Simplify the folder list, such that it only contains the parent-most entries + // (eg. { "C:\A\B\", "C:\A\B\C\", "C:\A\B\D\E\" } => { "C:\A\B\" } + var simplifiedFolders = Utility.Utility.SimplifyFolderList(changedFolders).ToList(); + + // 2. Check the simplified folders, and their parent folders against the exclusion filter. + // This is needed because the filter may exclude "C:\A\", but this won't match the more + // specific "C:\A\B\" in our list, even though it's meant to be excluded. + // The reason why the filter doesn't exclude it is because during a regular (non-USN) full scan, + // FilterHandler.EnumerateFilesAndFolders() works top-down, and won't even enumerate child + // folders. + var cache = new HashSet(); // cache to speed up processing + result.Folders.AddRange(FilterExcludedFolders(simplifiedFolders, filter, cache)); + + // 3. Our list of files may contain entries inside one of the simplified folders (from step 1., above). + // Since that folder is going to be fully scanned, those files can be removed. + // Note: it would be wrong to use the result from step 2. as the folder list! The entries removed + // between 1. and 2. are *excluded* folders, and files below them are to be *excluded*, too. + var simplifiedFiles = Utility.Utility.GetFilesNotInFolders(changedFiles, simplifiedFolders); + + // 4. The simplified file list still needs to be checked against the exclusion filter, as it + // may contain entries excluded due to attributes, but also because they are below excluded + // folders, which themselves aren't in the folder list from step 1. + // Note that the simplified file list may contain entries that have been deleted! They need to + // be kept in the list (unless excluded by the filter) in order for the backup handler to record their + // deletion. + result.Files.UnionWith(FilterExcludedFiles(simplifiedFiles, filter, cache)); } } catch (UsnJournalSoftFailureException) @@ -138,6 +172,125 @@ namespace Duplicati.Library.Snapshots return result; } + /// + /// Filter supplied files, removing any files which itself, or one + /// of its parent folders, is excluded by the filter. + /// + /// Files to filter + /// Exclusion filter + /// Cache of excluded folders (optional) + /// Filtered files + private IEnumerable FilterExcludedFiles(IEnumerable files, + Utility.Utility.EnumerationFilterDelegate filter, ISet cache = null) + { + var result = new List(); + + foreach (var file in files) + { + var attr = m_snapshot.FileExists(file) ? m_snapshot.GetAttributes(file) : FileAttributes.Normal; + try + { + if (!filter(file, file, attr)) + continue; + + if (!IsFolderOrAncestorsExcluded(Utility.Utility.GetParent(file, true), filter, cache)) + { + result.Add(file); + } + } + catch (System.Threading.ThreadAbortException) + { + throw; + } + catch (Exception ex) + { + m_errorCallback?.Invoke(file, file, ex); + filter(file, file, attr | Utility.Utility.ATTRIBUTE_ERROR); + } + } + + return result; + } + + /// + /// Filter supplied folders, removing any folder which itself, or one + /// of its ancestors, is excluded by the filter. + /// + /// Folder to filter + /// Exclusion filter + /// Cache of excluded folders (optional) + /// Filtered folders + private IEnumerable FilterExcludedFolders(IEnumerable folders, + Utility.Utility.EnumerationFilterDelegate filter, ISet cache = null) + { + var result = new List(); + + foreach (var folder in folders) + { + try + { + if (!IsFolderOrAncestorsExcluded(folder, filter, cache)) + { + result.Add(folder); + } + } + catch (System.Threading.ThreadAbortException) + { + throw; + } + catch (Exception ex) + { + m_errorCallback?.Invoke(folder, folder, ex); + filter(folder, folder, FileAttributes.Directory | Utility.Utility.ATTRIBUTE_ERROR); + } + } + + return result; + } + + /// + /// Tests if specified folder, or any of its ancestors, is excluded by the filter + /// + /// Folder to test + /// Filter + /// Cache of excluded folders (optional) + /// True if excluded, false otherwise + private bool IsFolderOrAncestorsExcluded(string folder, Utility.Utility.EnumerationFilterDelegate filter, ISet cache = null) + { + List parents = null; + while (folder != null) + { + // first check cache + if (cache != null) + { + if (cache.Contains(folder)) + break; // hit! + + // remember folder for cache + if (parents == null) + { + parents = new List(); // create on-demand + } + parents.Add(folder); + } + + var attr = m_snapshot.FolderExists(folder) ? m_snapshot.GetAttributes(folder) : FileAttributes.Directory; + + if (!filter(folder, folder, attr)) + break; // excluded + + folder = Utility.Utility.GetParent(folder, true); + } + + if (folder != null && parents != null) + { + // update cache + cache.UnionWith(parents); + } + + return folder != null; + } + /// /// Add ALL sources for volume to result set /// diff --git a/Duplicati/Library/Utility/TempFile.cs b/Duplicati/Library/Utility/TempFile.cs index ba7a49706..e2934109c 100644 --- a/Duplicati/Library/Utility/TempFile.cs +++ b/Duplicati/Library/Utility/TempFile.cs @@ -31,7 +31,7 @@ namespace Duplicati.Library.Utility /// /// The prefix applied to all temporary files /// - public static string APPLICATION_PREFIX = Utility.getEntryAssembly().FullName.Substring(0, 3).ToLower() + "-"; + public static string APPLICATION_PREFIX = Utility.GetEntryAssembly().FullName.Substring(0, 3).ToLower() + "-"; private string m_path; private bool m_protect; diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs index 1abf66f59..2882f03cc 100644 --- a/Duplicati/Library/Utility/Utility.cs +++ b/Duplicati/Library/Utility/Utility.cs @@ -38,12 +38,12 @@ namespace Duplicati.Library.Utility /// /// A cache of the FileSystemCaseSensitive property, which is computed upon the first access. /// - private static bool? CachedIsFSCaseSensitive = null; + private static bool? CachedIsFSCaseSensitive; /// /// Gets the hash algorithm used for calculating a hash /// - public static string HashAlgorithm { get { return "SHA256"; } } + public static string HashAlgorithm => "SHA256"; /// /// The EPOCH offset (unix style) @@ -53,7 +53,7 @@ namespace Duplicati.Library.Utility /// /// The attribute value used to indicate error /// - public const System.IO.FileAttributes ATTRIBUTE_ERROR = (System.IO.FileAttributes)(1 << 30); + public const FileAttributes ATTRIBUTE_ERROR = (FileAttributes)(1 << 30); /// /// The callback delegate type used to collecting file information @@ -62,14 +62,14 @@ namespace Duplicati.Library.Utility /// The current element /// The attributes of the element /// A value indicating if the folder should be recursed, ignored for other types - public delegate bool EnumerationFilterDelegate(string rootpath, string path, System.IO.FileAttributes attributes); + public delegate bool EnumerationFilterDelegate(string rootpath, string path, FileAttributes attributes); /// /// Copies the content of one stream into another /// /// The stream to read from /// The stream to write to - public static void CopyStream(System.IO.Stream source, System.IO.Stream target) + public static void CopyStream(Stream source, Stream target) { CopyStream(source, target, true); } @@ -80,11 +80,15 @@ namespace Duplicati.Library.Utility /// The stream to read from /// The stream to write to /// True if an attempt should be made to rewind the source stream, false otherwise - public static void CopyStream(System.IO.Stream source, System.IO.Stream target, bool tryRewindSource, byte[] buf = null) + /// Temporary buffer to use (optional) + public static void CopyStream(Stream source, Stream target, bool tryRewindSource, byte[] buf = null) { if (tryRewindSource && source.CanSeek) try { source.Position = 0; } - catch { } + catch + { + // ignored + } buf = buf ?? new byte[DEFAULT_BUFFER_SIZE]; @@ -96,21 +100,7 @@ namespace Duplicati.Library.Utility /// /// These are characters that must be escaped when using a globbing expression /// - private static readonly string BADCHARS = "\\" + string.Join("|\\", new string[] { - "\\", - "+", - "|", - "{", - "[", - "(", - ")", - "]", - "}", - "^", - "$", - "#", - "." - }); + private static readonly string BADCHARS = @"\\|\+|\||\{|\[|\(|\)|\]|\}|\^|\$|\#|\."; /// /// Most people will probably want to use fileglobbing, but RegExp's are more flexible. @@ -121,7 +111,7 @@ namespace Duplicati.Library.Utility public static string ConvertGlobbingToRegExp(string globexp) { //First escape all special characters - globexp = Regex.Replace(globexp, BADCHARS, "\\$&"); + globexp = Regex.Replace(globexp, BADCHARS, @"\$&"); //Replace the globbing expressions with the corresponding regular expressions globexp = globexp.Replace('?', '.').Replace("*", ".*"); @@ -194,11 +184,9 @@ namespace Duplicati.Library.Utility /// A list of the full filenames and foldernames. Foldernames ends with the directoryseparator char public static IEnumerable EnumerateFileSystemEntries(string basepath, IFilter filter) { - IFilter match; filter = filter ?? new FilterExpression(); return EnumerateFileSystemEntries(basepath, (rootpath, path, attributes) => { - bool result; - if (!filter.Matches(path, out result, out match)) + if (!filter.Matches(path, out var result, out _)) result = true; return result; @@ -217,7 +205,7 @@ namespace Duplicati.Library.Utility /// /// The path to return data from /// Attributes for the file or folder - public delegate System.IO.FileAttributes ExtractFileAttributes(string path); + public delegate FileAttributes ExtractFileAttributes(string path); /// /// A callback delegate used for extracting attributes from a file or folder @@ -236,7 +224,7 @@ namespace Duplicati.Library.Utility /// A list of the full filenames public static IEnumerable EnumerateFileSystemEntries(string rootpath, EnumerationFilterDelegate callback) { - return EnumerateFileSystemEntries(rootpath, callback, new FileSystemInteraction(System.IO.Directory.GetDirectories), new FileSystemInteraction(System.IO.Directory.GetFiles)); + return EnumerateFileSystemEntries(rootpath, callback, Directory.GetDirectories, Directory.GetFiles); } /// @@ -250,7 +238,7 @@ namespace Duplicati.Library.Utility /// A list of the full filenames public static IEnumerable EnumerateFileSystemEntries(string rootpath, EnumerationFilterDelegate callback, FileSystemInteraction folderList, FileSystemInteraction fileList) { - return EnumerateFileSystemEntries(rootpath, callback, folderList, fileList, null, null); + return EnumerateFileSystemEntries(rootpath, callback, folderList, fileList, null); } /// @@ -274,7 +262,7 @@ namespace Duplicati.Library.Utility try { - System.IO.FileAttributes attr = attributeReader == null ? System.IO.FileAttributes.Directory : attributeReader(rootpath); + FileAttributes attr = attributeReader == null ? FileAttributes.Directory : attributeReader(rootpath); if (callback(rootpath, rootpath, attr)) lst.Push(rootpath); } @@ -286,7 +274,7 @@ namespace Duplicati.Library.Utility { if (errorCallback != null) errorCallback(rootpath, rootpath, ex); - callback(rootpath, rootpath, System.IO.FileAttributes.Directory | ATTRIBUTE_ERROR); + callback(rootpath, rootpath, FileAttributes.Directory | ATTRIBUTE_ERROR); } while (lst.Count > 0) @@ -302,7 +290,7 @@ namespace Duplicati.Library.Utility var sf = AppendDirSeparator(s); try { - System.IO.FileAttributes attr = attributeReader == null ? System.IO.FileAttributes.Directory : attributeReader(sf); + FileAttributes attr = attributeReader == null ? FileAttributes.Directory : attributeReader(sf); if (callback(rootpath, sf, attr)) lst.Push(sf); } @@ -314,7 +302,7 @@ namespace Duplicati.Library.Utility { if (errorCallback != null) errorCallback(rootpath, sf, ex); - callback(rootpath, sf, System.IO.FileAttributes.Directory | ATTRIBUTE_ERROR); + callback(rootpath, sf, FileAttributes.Directory | ATTRIBUTE_ERROR); } } } @@ -326,7 +314,7 @@ namespace Duplicati.Library.Utility { if (errorCallback != null) errorCallback(rootpath, f, ex); - callback(rootpath, f, System.IO.FileAttributes.Directory | ATTRIBUTE_ERROR); + callback(rootpath, f, FileAttributes.Directory | ATTRIBUTE_ERROR); } string[] files = null; @@ -343,7 +331,7 @@ namespace Duplicati.Library.Utility { if (errorCallback != null) errorCallback(rootpath, f, ex); - callback(rootpath, f, System.IO.FileAttributes.Directory | ATTRIBUTE_ERROR); + callback(rootpath, f, FileAttributes.Directory | ATTRIBUTE_ERROR); } if (files != null) @@ -351,7 +339,7 @@ namespace Duplicati.Library.Utility { try { - System.IO.FileAttributes attr = attributeReader == null ? System.IO.FileAttributes.Normal : attributeReader(s); + FileAttributes attr = attributeReader == null ? FileAttributes.Normal : attributeReader(s); if (!callback(rootpath, s, attr)) continue; } @@ -374,7 +362,7 @@ namespace Duplicati.Library.Utility { try { - System.IO.FileAttributes attr = attributeReader == null ? System.IO.FileAttributes.Normal : attributeReader(rootpath); + FileAttributes attr = attributeReader == null ? FileAttributes.Normal : attributeReader(rootpath); if (!callback(rootpath, rootpath, attr)) yield break; } @@ -407,7 +395,7 @@ namespace Duplicati.Library.Utility try { - return attributeReader(path).HasFlag(System.IO.FileAttributes.Directory); + return attributeReader(path).HasFlag(FileAttributes.Directory); } catch { @@ -416,25 +404,16 @@ namespace Duplicati.Library.Utility } /// - /// Tests if path refers to a file, or folder, below the parent folder + /// Tests if path refers to a file, or folder, below the parent folder /// /// File or folder to test /// Candidate parent folder /// True if below parent folder, false otherwise (note that this returns false if the two argument paths are identical!) public static bool IsPathBelowFolder(string fileOrFolderPath, string parentFolder) { - var candidatePath = fileOrFolderPath; var sanitizedParentFolder = AppendDirSeparator(parentFolder); - - while (true) - { - candidatePath = GetParent(candidatePath, true); - if (candidatePath == null) - return false; - - if (candidatePath.Equals(sanitizedParentFolder, ClientFilenameStringComparision)) - return true; - } + return fileOrFolderPath.StartsWith(sanitizedParentFolder, ClientFilenameStringComparision) && + !fileOrFolderPath.Equals(sanitizedParentFolder, ClientFilenameStringComparision); } /// @@ -535,13 +514,13 @@ namespace Duplicati.Library.Utility /// The combined size of all files that match the filter public static long GetDirectorySize(string folder, IFilter filter) { - return EnumerateFolders(folder, filter).Sum((path) => new System.IO.FileInfo(path).Length); + return EnumerateFolders(folder, filter).Sum((path) => new FileInfo(path).Length); } /// /// A cached instance of the directory separator as a string /// - public static readonly string DirectorySeparatorString = System.IO.Path.DirectorySeparatorChar.ToString(); + public static readonly string DirectorySeparatorString = Path.DirectorySeparatorChar.ToString(); /// /// Appends the appropriate directory separator to paths, depending on OS. @@ -551,10 +530,9 @@ namespace Duplicati.Library.Utility /// The path with the directory separator appended public static string AppendDirSeparator(string path) { - if (!path.EndsWith(DirectorySeparatorString, StringComparison.Ordinal)) - return path += DirectorySeparatorString; - else - return path; + return !path.EndsWith(DirectorySeparatorString, StringComparison.Ordinal) + ? path + DirectorySeparatorString + : path; } /// @@ -566,10 +544,7 @@ namespace Duplicati.Library.Utility /// The path with the directory separator appended public static string AppendDirSeparator(string path, string separator) { - if (!path.EndsWith(separator, StringComparison.Ordinal)) - return path += separator; - else - return path; + return AppendDirSeparator(separator); } /// @@ -591,7 +566,7 @@ namespace Duplicati.Library.Utility /// The buffer to read into /// The amout of bytes to read /// The actual number of bytes read - public static int ForceStreamRead(System.IO.Stream stream, byte[] buf, int count) + public static int ForceStreamRead(Stream stream, byte[] buf, int count) { int a; int index = 0; @@ -612,7 +587,7 @@ namespace Duplicati.Library.Utility /// Another stream /// True if the length of the two streams should be compared /// True if they are equal, false otherwise - public static bool CompareStreams(System.IO.Stream stream1, System.IO.Stream stream2, bool checkLength) + public static bool CompareStreams(Stream stream1, Stream stream2, bool checkLength) { if (checkLength) { @@ -661,7 +636,7 @@ namespace Duplicati.Library.Utility /// The base64 encoded hash public static string CalculateHash(string path) { - using (System.IO.FileStream fs = System.IO.File.Open(path, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read)) + using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) return CalculateHash(fs); } @@ -670,7 +645,7 @@ namespace Duplicati.Library.Utility /// /// The stream to calculate the hash for /// The base64 encoded hash - public static string CalculateHash(System.IO.Stream stream) + public static string CalculateHash(Stream stream) { return Convert.ToBase64String(HashAlgorithmHelper.Create(HashAlgorithm).ComputeHash(stream)); } @@ -685,14 +660,15 @@ namespace Duplicati.Library.Utility // Since StreamReader defaults to UTF8 and most text files will NOT be UTF8 without BOM, // we need to detect the encoding (at least that it's not UTF8). // So we read the first 4096 bytes and try to decode them as UTF8. - byte[] buffer = new byte[4096]; - using (System.IO.FileStream file = new System.IO.FileStream(filename, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read)) + var buffer = new byte[4096]; + using (var file = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) file.Read(buffer, 0, 4096); - Encoding enc = Encoding.UTF8; + var enc = Encoding.UTF8; try { // this will throw an error if not really UTF8 + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed new UTF8Encoding(false, true).GetString(buffer); } catch (Exception) @@ -701,7 +677,7 @@ namespace Duplicati.Library.Utility } // This will load the text using the BOM, or the detected encoding if no BOM. - using (System.IO.StreamReader reader = new System.IO.StreamReader(filename, enc, true)) + using (var reader = new StreamReader(filename, enc, true)) { // Remove all \r from the file and split on \n, then pass directly to ExtractOptions return reader.ReadToEnd(); @@ -709,7 +685,7 @@ namespace Duplicati.Library.Utility } /// - /// Formats a size into a human readable format, eg. 2048 becomes "2 KB" or -2283 becomes &qout;-2.23 KB%quot. + /// Formats a size into a human readable format, eg. 2048 becomes "2 KB" or -2283 becomes "-2.23 KB%quot. /// /// The size to format /// A human readable string representing the size @@ -797,7 +773,7 @@ namespace Duplicati.Library.Utility /// The parsed value, or the default value. public static bool ParseBool(string value, bool @default) { - return Utility.ParseBool(value, () => @default); + return ParseBool(value, () => @default); } /// @@ -825,12 +801,7 @@ namespace Duplicati.Library.Utility /// The enum type parameter. public static T ParseEnumOption(IDictionary options, string value, T @default) { - string opt; - if (options.TryGetValue(value, out opt)) - return ParseEnum(opt, @default); - else - return @default; - + return options.TryGetValue(value, out var opt) ? ParseEnum(opt, @default) : @default; } /// @@ -842,7 +813,7 @@ namespace Duplicati.Library.Utility /// The enum type parameter. public static T ParseEnum(string value, T @default) { - foreach (string s in Enum.GetNames(typeof(T))) + foreach (var s in Enum.GetNames(typeof(T))) if (s.Equals(value, StringComparison.OrdinalIgnoreCase)) return (T)Enum.Parse(typeof(T), s); @@ -918,9 +889,11 @@ namespace Duplicati.Library.Utility { if (UNAME == null) { - var psi = new System.Diagnostics.ProcessStartInfo("uname"); - psi.RedirectStandardOutput = true; - psi.UseShellExecute = false; + var psi = new System.Diagnostics.ProcessStartInfo("uname") + { + RedirectStandardOutput = true, + UseShellExecute = false + }; var pi = System.Diagnostics.Process.Start(psi); pi.WaitForExit(5000); @@ -959,6 +932,7 @@ namespace Duplicati.Library.Utility } catch { + // ignored } return null; @@ -967,24 +941,12 @@ namespace Duplicati.Library.Utility /// /// Gets or sets a value indicating if the client is Linux/Unix based /// - public static bool IsClientLinux - { - get - { - return Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; - } - } + public static bool IsClientLinux => Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; /// /// Gets a value indicating if the client is Windows based /// - public static bool IsClientWindows - { - get - { - return !IsClientLinux; - } - } + public static bool IsClientWindows => !IsClientLinux; /// /// Returns a value indicating if the filesystem, is case sensitive @@ -999,10 +961,7 @@ namespace Duplicati.Library.Utility // TODO: This should probably be determined by filesystem rather than OS, // OSX can actually have the disks formated as Case Sensitive, but insensitive is default - - Func defaultReply = () => Utility.IsClientLinux && !Utility.IsClientOSX; - - CachedIsFSCaseSensitive = Utility.ParseBool(str, defaultReply); + CachedIsFSCaseSensitive = ParseBool(str, () => IsClientLinux && !IsClientOSX); } return CachedIsFSCaseSensitive.Value; @@ -1012,13 +971,7 @@ namespace Duplicati.Library.Utility /// /// Returns a value indicating if the app is running under Mono /// - public static bool IsMono - { - get - { - return Type.GetType("Mono.Runtime") != null; - } - } + public static bool IsMono => Type.GetType("Mono.Runtime") != null; /// /// Gets the current Mono runtime version, will return 0.0 if not running Mono @@ -1032,7 +985,7 @@ namespace Duplicati.Library.Utility var v = MonoDisplayVersion; if (v != null) { - var regex = new System.Text.RegularExpressions.Regex(@"\d+\.\d+(\.\d+)?(\.\d+)?"); + var regex = new Regex(@"\d+\.\d+(\.\d+)?(\.\d+)?"); var match = regex.Match(v); if (match.Success) return new Version(match.Value); @@ -1040,6 +993,7 @@ namespace Duplicati.Library.Utility } catch { + // ignored } return new Version(); @@ -1055,16 +1009,17 @@ namespace Duplicati.Library.Utility { try { - Type t = Type.GetType("Mono.Runtime"); + var t = Type.GetType("Mono.Runtime"); if (t != null) { - System.Reflection.MethodInfo mi = t.GetMethod("GetDisplayName", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); + var mi = t.GetMethod("GetDisplayName", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); if (mi != null) return (string)mi.Invoke(null, null); } } catch { + // ignored } return null; @@ -1074,28 +1029,17 @@ namespace Duplicati.Library.Utility /// /// Gets the users default UI language /// - public static System.Globalization.CultureInfo DefaultCulture - { - get - { - System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(DummyMethod)); - return t.CurrentUICulture; - } - } - //Unused function, used to create a dummy thread - private static void DummyMethod() - { - } + public static System.Globalization.CultureInfo DefaultCulture => new System.Threading.Thread(() => { }).CurrentUICulture; /// /// Gets a string comparer that matches the client filesystems case sensitivity /// - public static StringComparer ClientFilenameStringComparer { get { return Utility.IsFSCaseSensitive ? StringComparer.CurrentCulture : StringComparer.CurrentCultureIgnoreCase; } } + public static StringComparer ClientFilenameStringComparer => IsFSCaseSensitive ? StringComparer.CurrentCulture : StringComparer.CurrentCultureIgnoreCase; /// /// Gets the string comparision that matches the client filesystems case sensitivity /// - public static StringComparison ClientFilenameStringComparision { get { return Utility.IsFSCaseSensitive ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase; } } + public static StringComparison ClientFilenameStringComparision => IsFSCaseSensitive ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase; /// /// Searches the system paths for the file specified @@ -1106,33 +1050,36 @@ namespace Duplicati.Library.Utility { try { - if (System.IO.Path.IsPathRooted(filename)) - return System.IO.File.Exists(filename) ? filename : null; + if (Path.IsPathRooted(filename)) + return File.Exists(filename) ? filename : null; try { - filename = System.IO.Path.GetFileName(filename); + filename = Path.GetFileName(filename); } catch { + // ignored } - string homedir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + System.IO.Path.PathSeparator.ToString(); + string homedir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + Path.PathSeparator.ToString(); //Look in application base folder and all system path folders - foreach (string s in (homedir + Environment.GetEnvironmentVariable("PATH")).Split(System.IO.Path.PathSeparator)) + foreach (string s in (homedir + Environment.GetEnvironmentVariable("PATH")).Split(Path.PathSeparator)) if (!string.IsNullOrEmpty(s) && s.Trim().Length > 0) try { - foreach (string sx in System.IO.Directory.GetFiles(ExpandEnvironmentVariables(s), filename)) + foreach (string sx in Directory.GetFiles(ExpandEnvironmentVariables(s), filename)) return sx; } catch { + // ignored } } catch { + // ignored } return null; @@ -1172,14 +1119,14 @@ namespace Duplicati.Library.Utility public static string ExpandEnvironmentVariablesRegexp(string str, Func lookup = null) { if (lookup == null) - lookup = x => Environment.GetEnvironmentVariable(x); + lookup = Environment.GetEnvironmentVariable; return // TODO: Should we switch to using the native format, instead of following the Windows scheme? //IsClientLinux ? ENVIRONMENT_VARIABLE_MATCHER_LINUX : ENVIRONMENT_VARIABLE_MATCHER_WINDOWS - ENVIRONMENT_VARIABLE_MATCHER_WINDOWS.Replace(str, (m) => Regex.Escape(lookup(m.Groups["name"].Value))); + ENVIRONMENT_VARIABLE_MATCHER_WINDOWS.Replace(str, m => Regex.Escape(lookup(m.Groups["name"].Value))); } /// @@ -1232,8 +1179,7 @@ namespace Duplicati.Library.Utility /// The parsed instance public static DateTime DeserializeDateTime(string str) { - DateTime dt; - if (!TryDeserializeDateTime(str, out dt)) + if (!TryDeserializeDateTime(str, out var dt)) throw new Exception(Strings.Utility.InvalidDateError(str)); return dt; @@ -1248,7 +1194,7 @@ namespace Duplicati.Library.Utility /// The unique items from . public static ISet GetUniqueItems(IEnumerable collection, out ISet duplicateItems) { - return Utility.GetUniqueItems(collection, EqualityComparer.Default, out duplicateItems); + return GetUniqueItems(collection, EqualityComparer.Default, out duplicateItems); } /// @@ -1261,12 +1207,14 @@ namespace Duplicati.Library.Utility /// The unique items from . public static ISet GetUniqueItems(IEnumerable collection, IEqualityComparer comparer, out ISet duplicateItems) { - HashSet uniqueItems = new HashSet(comparer); + var uniqueItems = new HashSet(comparer); duplicateItems = new HashSet(comparer); - foreach (T item in collection) + foreach (var item in collection) + { if (!uniqueItems.Add(item)) duplicateItems.Add(item); + } return uniqueItems; } @@ -1278,8 +1226,8 @@ namespace Duplicati.Library.Utility /// The file to replace with public static void ReplaceFile(string target, string sourcefile) { - if (System.IO.File.Exists(target)) - System.IO.File.Delete(target); + if (File.Exists(target)) + File.Delete(target); //Nasty workaround for the fact that a recently deleted file occasionally blocks a new write long i = 5; @@ -1287,13 +1235,14 @@ namespace Duplicati.Library.Utility { try { - System.IO.File.Move(sourcefile, target); + File.Move(sourcefile, target); break; } catch (Exception ex) { if (i == 0) - throw new Exception(string.Format("Failed to replace the file \"{0}\" volume with the \"{1}\", error: {2}", target, sourcefile, ex.Message)); + throw new Exception( + $"Failed to replace the file \"{target}\" volume with the \"{sourcefile}\", error: {ex.Message}"); System.Threading.Thread.Sleep(250); } } while (i-- > 0); @@ -1305,7 +1254,7 @@ namespace Duplicati.Library.Utility // so this layer of indirection is necessary // // entry assembly or reasonable approximation - public static System.Reflection.Assembly getEntryAssembly() + public static System.Reflection.Assembly GetEntryAssembly() { return System.Reflection.Assembly.GetEntryAssembly() ?? System.Reflection.Assembly.GetExecutingAssembly(); } @@ -1394,7 +1343,7 @@ namespace Duplicati.Library.Utility /// true, the item was printed, false otherwise. /// The item to write. /// The target writer. - private static bool PrintSerializeIfPrimitive(object item, System.IO.TextWriter writer) + private static bool PrintSerializeIfPrimitive(object item, TextWriter writer) { if (item == null) { @@ -1430,7 +1379,7 @@ namespace Duplicati.Library.Utility /// The string indentation /// A lookup table with visited objects, used to avoid inifinite recursion /// The maximum number of items to report from an IEnumerable instance - public static void PrintSerializeObject(object item, System.IO.TextWriter writer, Func filter = null, bool recurseobjects = false, int indentation = 0, int collectionlimit = 0, Dictionary visited = null) + public static void PrintSerializeObject(object item, TextWriter writer, Func filter = null, bool recurseobjects = false, int indentation = 0, int collectionlimit = 0, Dictionary visited = null) { visited = visited ?? new Dictionary(); var indentstring = new string(' ', indentation); @@ -1549,7 +1498,7 @@ namespace Duplicati.Library.Utility public static StringBuilder PrintSerializeObject(object item, StringBuilder sb = null, Func filter = null, bool recurseobjects = false, int indentation = 0, int collectionlimit = 10) { sb = sb ?? new StringBuilder(); - using (var sw = new System.IO.StringWriter(sb)) + using (var sw = new StringWriter(sb)) PrintSerializeObject(item, sw, filter, recurseobjects, indentation, collectionlimit); return sb; } @@ -1566,8 +1515,8 @@ namespace Duplicati.Library.Utility public static byte[] RepeatedHashWithSalt(string data, string salt, int repeats = 1200) { return RepeatedHashWithSalt( - System.Text.Encoding.UTF8.GetBytes(data ?? ""), - System.Text.Encoding.UTF8.GetBytes(salt ?? ""), + Encoding.UTF8.GetBytes(data ?? ""), + Encoding.UTF8.GetBytes(salt ?? ""), repeats); } @@ -1645,16 +1594,16 @@ namespace Duplicati.Library.Utility [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] public static IEnumerable> GetVolumeGuidsAndDriveLetters() { - using (System.Management.ManagementObjectSearcher searcher = new System.Management.ManagementObjectSearcher("Select * from Win32_Volume")) + using (var searcher = new System.Management.ManagementObjectSearcher("Select * from Win32_Volume")) { - foreach (System.Management.ManagementObject obj in searcher.Get()) + foreach (var obj in searcher.Get()) { - object deviceIdObj = obj["DeviceID"]; - object driveLetterObj = obj["DriveLetter"]; + var deviceIdObj = obj["DeviceID"]; + var driveLetterObj = obj["DriveLetter"]; if (deviceIdObj != null && driveLetterObj != null) { - string deviceId = deviceIdObj.ToString(); - string driveLetter = driveLetterObj.ToString(); + var deviceId = deviceIdObj.ToString(); + var driveLetter = driveLetterObj.ToString(); if (!string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(driveLetter)) { yield return new KeyValuePair(driveLetter + @"\", deviceId); @@ -1684,7 +1633,7 @@ namespace Duplicati.Library.Utility if (string.IsNullOrWhiteSpace(arg)) return arg; - if (!Library.Utility.Utility.IsClientWindows) + if (!IsClientWindows) { // We could consider using single quotes that prevents all expansions //if (!allowEnvExpansion) @@ -1694,12 +1643,12 @@ namespace Duplicati.Library.Utility arg = COMMANDLINE_ESCAPED_LINUX.Replace(arg, (match) => { if (match.Value == "!") - return "\"'!'\""; + return @"""'!'"""; if (match.Value == "$" && allowEnvExpansion) return match.Value; - return "\\" + match.Value; + return @"\" + match.Value; }); } else @@ -1714,16 +1663,16 @@ namespace Duplicati.Library.Utility // and sadly it expands only if the variable exists // making it even rarer and harder to diagnose when // it happens - arg = arg.Replace("\"", "\"\""); + arg = arg.Replace(@"""", @""""""); // Also fix the case where the argument ends with a slash if (arg[arg.Length - 1] == '\\') - arg += "\\"; + arg += @"\"; } // Check that all characters are in the safe set if (COMMANDLINE_SAFE.Match(arg).Length != arg.Length) - return "\"" + arg + "\""; + return @"""" + arg + @""""; else return arg; } diff --git a/Duplicati/Server/WebServer/RESTMethods/Licenses.cs b/Duplicati/Server/WebServer/RESTMethods/Licenses.cs index 893a7ae9d..201797665 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Licenses.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Licenses.cs @@ -22,7 +22,7 @@ namespace Duplicati.Server.WebServer.RESTMethods { public void GET(string key, RequestInfo info) { - var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), "licenses"); + var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.GetEntryAssembly().Location), "licenses"); info.OutputOK(Duplicati.License.LicenseReader.ReadLicenses(path)); } }