Merge branch 'master' into feature/issue5919
This commit is contained in:
@@ -275,7 +275,7 @@ public static class Program
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
var target = new ControllerMultiLogTarget(new ConsoleLogDestination(), LogMessageType.Information, null);
|
||||
var target = new ControllerMultiLogTarget(new ConsoleLogDestination(), LogMessageType.Information, null, null);
|
||||
using (Log.StartScope(target))
|
||||
{
|
||||
if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(agentConfig.WindowsEventLog))
|
||||
|
||||
@@ -150,7 +150,7 @@ public class SMBBackend : IStreamingBackend, IFolderEnabledBackend
|
||||
|
||||
var writeBufferSizeConfig = options.GetValueOrDefault(WRITE_BUFFER_SIZE_OPTION);
|
||||
if (!string.IsNullOrWhiteSpace(writeBufferSizeConfig))
|
||||
writeBufferSize = int.TryParse(readBufferSizeConfig, out int value) ? value : null;
|
||||
writeBufferSize = int.TryParse(writeBufferSizeConfig, out int value) ? value : null;
|
||||
|
||||
// Normalize to 10KB minimum buffers size
|
||||
readBufferSize = readBufferSize < 1024 * 10 ? null : readBufferSize;
|
||||
|
||||
@@ -422,7 +422,7 @@ partial class BackendManager
|
||||
}
|
||||
|
||||
// Refresh DNS name if we fail to connect in order to prevent issues with incorrect DNS entries
|
||||
var dnsFailure = Library.Utility.Utility.FlattenException(ex).Any(x => x is System.Net.WebException wex && wex.Status == System.Net.WebExceptionStatus.NameResolutionFailure);
|
||||
var dnsFailure = Library.Utility.ExceptionExtensions.FlattenException(ex).Any(x => x is System.Net.WebException wex && wex.Status == System.Net.WebExceptionStatus.NameResolutionFailure);
|
||||
if (dnsFailure)
|
||||
{
|
||||
try
|
||||
@@ -443,7 +443,7 @@ partial class BackendManager
|
||||
var recovered = false;
|
||||
|
||||
// Check if this was a folder missing exception and we are allowed to autocreate folders
|
||||
if (!(anyDownloaded || anyUploaded) && context.Options.AutocreateFolders && Library.Utility.Utility.FlattenException(ex).Any(x => x is FolderMissingException))
|
||||
if (!(anyDownloaded || anyUploaded) && context.Options.AutocreateFolders && Library.Utility.ExceptionExtensions.FlattenException(ex).Any(x => x is FolderMissingException))
|
||||
{
|
||||
if (await TryCreateFolder().ConfigureAwait(false))
|
||||
recovered = true;
|
||||
|
||||
@@ -478,7 +478,7 @@ namespace Duplicati.Library.Main
|
||||
{
|
||||
OnOperationStarted?.Invoke(result);
|
||||
var resultSetter = result as ISetCommonOptions;
|
||||
using (var logTarget = new ControllerMultiLogTarget(result, Logging.LogMessageType.Information, null))
|
||||
using (var logTarget = new ControllerMultiLogTarget(result, Logging.LogMessageType.Information, null, m_options.SuppressWarningsFilter))
|
||||
using (Logging.Log.StartScope(logTarget, null))
|
||||
{
|
||||
logTarget.AddTarget(m_messageSink, m_options.ConsoleLoglevel, m_options.ConsoleLogFilter);
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
// 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
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -29,12 +31,25 @@ namespace Duplicati.Library.Main
|
||||
/// <summary>
|
||||
/// The log target handler for the controller, which sends to multiple log targets
|
||||
/// </summary>
|
||||
public class ControllerMultiLogTarget : Logging.ILogDestination, IDisposable
|
||||
public class ControllerMultiLogTarget : ILogDestination, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The target entry for the log target
|
||||
/// </summary>
|
||||
/// <param name="Target">The log target.</param>
|
||||
/// <param name="Level">The minimum log level to consider</param>
|
||||
/// <param name="Filter">The log filter.</param>
|
||||
private sealed record TargetEntry(ILogDestination Target, LogMessageType Level, Library.Utility.IFilter Filter);
|
||||
|
||||
/// <summary>
|
||||
/// The list of log targets to handle
|
||||
/// </summary>
|
||||
private readonly List<Tuple<ILogDestination, LogMessageType, Library.Utility.IFilter>> m_targets = new List<Tuple<ILogDestination, LogMessageType, Library.Utility.IFilter>>();
|
||||
private readonly List<TargetEntry> m_targets = new List<TargetEntry>();
|
||||
|
||||
/// <summary>
|
||||
/// The filter used to suppress warnings
|
||||
/// </summary>
|
||||
private readonly HashSet<string>? m_supressWarningFilter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="T:Duplicati.Library.Main.ControllerMultiLogTarget"/> class.
|
||||
@@ -42,8 +57,9 @@ namespace Duplicati.Library.Main
|
||||
/// <param name="target">The log target.</param>
|
||||
/// <param name="loglevel">The minimum log level to consider</param>
|
||||
/// <param name="filter">The log filter.</param>
|
||||
public ControllerMultiLogTarget(ILogDestination target, Logging.LogMessageType loglevel, Library.Utility.IFilter filter)
|
||||
public ControllerMultiLogTarget(ILogDestination target, Logging.LogMessageType loglevel, Library.Utility.IFilter? filter, HashSet<string>? supressWarningFilter)
|
||||
{
|
||||
m_supressWarningFilter = supressWarningFilter;
|
||||
AddTarget(target, loglevel, filter);
|
||||
}
|
||||
|
||||
@@ -53,12 +69,12 @@ namespace Duplicati.Library.Main
|
||||
/// <param name="target">The log target.</param>
|
||||
/// <param name="loglevel">The minimum log level to consider</param>
|
||||
/// <param name="filter">The log filter.</param>
|
||||
public void AddTarget(ILogDestination target, LogMessageType loglevel, Library.Utility.IFilter filter)
|
||||
public void AddTarget(ILogDestination target, LogMessageType loglevel, Library.Utility.IFilter? filter)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
m_targets.Add(new Tuple<ILogDestination, LogMessageType, Library.Utility.IFilter>(target, loglevel, filter ?? new Library.Utility.FilterExpression()));
|
||||
m_targets.Add(new TargetEntry(target, loglevel, filter ?? new Library.Utility.FilterExpression()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -73,7 +89,7 @@ namespace Duplicati.Library.Main
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var m in m_targets)
|
||||
(m.Item1 as IDisposable)?.Dispose();
|
||||
(m.Target as IDisposable)?.Dispose();
|
||||
m_targets.Clear();
|
||||
}
|
||||
|
||||
@@ -81,7 +97,7 @@ namespace Duplicati.Library.Main
|
||||
/// Gets the minimum log level of all the targets
|
||||
/// </summary>
|
||||
public LogMessageType MinimumLevel
|
||||
=> m_targets.Select(x => x.Item2).DefaultIfEmpty(LogMessageType.Error).Min();
|
||||
=> m_targets.Select(x => x.Level).DefaultIfEmpty(LogMessageType.Error).Min();
|
||||
|
||||
/// <summary>
|
||||
/// Writes the message to all the destinations.
|
||||
@@ -89,9 +105,13 @@ namespace Duplicati.Library.Main
|
||||
/// <param name="entry">Entry.</param>
|
||||
public void WriteMessage(LogEntry entry)
|
||||
{
|
||||
// If the message is a warning, check if we should suppress it
|
||||
if (m_supressWarningFilter != null && entry.Level == LogMessageType.Warning && m_supressWarningFilter.Contains(entry.Id))
|
||||
entry = new LogEntry(entry.Message, entry.Arguments, LogMessageType.Information, entry.Tag, entry.Id, entry.Exception);
|
||||
|
||||
foreach (var e in m_targets)
|
||||
{
|
||||
var found = e.Item3.Matches(entry.FilterTag, out var result, out var match);
|
||||
var found = e.Filter.Matches(entry.FilterTag, out var result, out var _);
|
||||
|
||||
// If there is a filter match, use that
|
||||
if (found)
|
||||
@@ -102,12 +122,12 @@ namespace Duplicati.Library.Main
|
||||
else
|
||||
{
|
||||
// Otherwise, filter by log-level
|
||||
if (entry.Level < e.Item2)
|
||||
if (entry.Level < e.Level)
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we get here, write the message
|
||||
e.Item1.WriteMessage(entry);
|
||||
e.Target.WriteMessage(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,8 +142,8 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
{
|
||||
if (ex.IsRetiredException())
|
||||
return;
|
||||
else
|
||||
Logging.Log.WriteWarningMessage(FILELOGTAG, "PathProcessingFailed", ex, "Failed to process path: {0}", e.Entry.Path);
|
||||
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "FileProcessingFailed", e.Entry.Path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -96,7 +96,6 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
if (ignorenames != null && ignorenames.Length == 0)
|
||||
ignorenames = null;
|
||||
|
||||
|
||||
// Shared filter function with bound variables
|
||||
ValueTask<bool> FilterEntry(ISourceProviderEntry entry)
|
||||
=> SourceFileEntryFilter(entry, blacklistPaths, hardlinkPolicy, symlinkPolicy, hardlinkmap, fileAttributeFilter, enumeratefilter, ignorenames, mixinqueue, token);
|
||||
@@ -303,7 +302,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorEnumerate", ex, "Failed to enumerate path: {0}", e.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorEnumerate", e.Path, "Failed to enumerate path: {0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,7 +368,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorBlockDevice", ex, "Failed to process path: {0}", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorBlockDevice", entry.Path);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -384,7 +383,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorCharacterDevice", ex, "Failed to process path: {0}", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorCharacterDevice", entry.Path);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -444,7 +443,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorHardLink", ex, "Failed to process path: {0}", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorHardLink", entry.Path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -465,7 +464,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorIgnoreFile", ex, "Failed to process path: {0}", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorIgnoreFile", entry.Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,7 +479,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(FILTER_LOGTAG, "PathProcessingErrorAttributes", ex, "Failed to process path, using default attributes: {0}", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "PathProcessingErrorAttributes", entry.Path, "Failed to process path, using default attributes: {0}");
|
||||
}
|
||||
|
||||
// If we exclude files based on attributes, filter that
|
||||
@@ -511,7 +510,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteExplicitMessage(FILTER_LOGTAG, "SymlinkTargetReadError", ex, "Failed to read symlink target for path: {0}", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILTER_LOGTAG, "SymlinkTargetReadError", entry.Path, "Failed to read symlink target for path: {0}");
|
||||
}
|
||||
|
||||
if (symlinkTarget != null)
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteExplicitMessage(FILELOGTAG, "FailedToReadSize", ex, "Failed to read size of file: {0}", e.Entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "FailedToReadSize", e.Entry.Path, "Failed to read size on \"{0}\"");
|
||||
}
|
||||
|
||||
await stats.AddExaminedFile(filestatsize);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Duplicati.Library.Logging;
|
||||
using Duplicati.Library.Utility;
|
||||
|
||||
namespace Duplicati.Library.Main.Operation.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to log path exceptions in a common way
|
||||
/// </summary>
|
||||
public static class LogExceptionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs a path warning message, using exception detection for the appropriate message
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception to log</param>
|
||||
/// <param name="logtag">The log tag to use for the message</param>
|
||||
/// <param name="id">The id to use for the message</param>
|
||||
/// <param name="path">The path to use for the message</param>
|
||||
/// <param name="message">The message to log</param>
|
||||
public static void LogCommonWarning(Exception? ex, string logtag, string id, string path, string message = "Failed to process path: {0}")
|
||||
{
|
||||
if (ex.IsPermissionDeniedException())
|
||||
Log.WriteWarningMessage(logtag, "PermissionDenied", ex, "Excluding path due to permission denied: {0}", path);
|
||||
else if (ex.IsFileLockedException())
|
||||
Log.WriteWarningMessage(logtag, "FileLocked", ex, "Excluding path due to file locked: {0}", path);
|
||||
else if (ex.IsPathNotFoundException())
|
||||
Log.WriteWarningMessage(logtag, "PathNotFound", ex, "Excluding path due to path not found: {0}", path);
|
||||
else if (ex.IsPathTooLongException())
|
||||
Log.WriteWarningMessage(logtag, "PathTooLong", ex, "Excluding path due to path too long: {0}", path);
|
||||
else
|
||||
Log.WriteWarningMessage(logtag, id, ex, message, path);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(METALOGTAG, "TimestampReadFailed", ex, "Failed to read timestamp on \"{0}\"", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, METALOGTAG, "TimestampReadFailed", entry.Path, "Failed to read timestamp on \"{0}\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(METALOGTAG, "TimestampReadFailed", ex, "Failed to read timestamp on \"{0}\"", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, METALOGTAG, "TimestampReadFailed", entry.Path, "Failed to read timestamp on \"{0}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(METALOGTAG, "MetadataProcessFailed", ex, "Failed to process metadata for \"{0}\", storing empty metadata", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, METALOGTAG, "MetadataProcessFailed", entry.Path, "Failed to process metadata on \"{0}\", storing empty metadata");
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(FILELOGTAG, "TimestampReadFailed", ex, "Failed to read timestamp on \"{0}\"", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "TimestampReadFailed", entry.Path, "Failed to read timestamp on \"{0}\"");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -107,7 +107,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteVerboseMessage(FILELOGTAG, "FailedAttributeRead", "Failed to read attributes from {0}: {1}", entry.Path, ex.Message);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "AttributeReadFailed", entry.Path, "Failed to read attributes on \"{0}\"");
|
||||
}
|
||||
|
||||
// If we only have metadata, stop here
|
||||
@@ -166,8 +166,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
if (ex.IsRetiredException())
|
||||
continue;
|
||||
|
||||
Logging.Log.WriteWarningMessage(FILELOGTAG, "ProcessingMetadataFailed", ex,
|
||||
"Failed to process entry, path: {0}", entry.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "ProcessingMetadataFailed", entry.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// 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
|
||||
// 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 System;
|
||||
@@ -25,10 +25,8 @@ using Duplicati.Library.Main.Operation.Common;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Utility;
|
||||
using System.Linq;
|
||||
using Duplicati.Library.Interface;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Duplicati.Library.Main.Operation.Backup
|
||||
{
|
||||
@@ -87,7 +85,7 @@ namespace Duplicati.Library.Main.Operation.Backup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Log.WriteWarningMessage(FILELOGTAG, "FileLengthFailure", ex, "Failed to read file length for file {0}", e.Path);
|
||||
LogExceptionHelper.LogCommonWarning(ex, FILELOGTAG, "FileLengthFailure", e.Path, "Failed to read file length for \"{0}\"");
|
||||
}
|
||||
|
||||
if (e.IsMetadata && fslen > maxmetadatasize)
|
||||
|
||||
+274
-914
File diff suppressed because it is too large
Load Diff
@@ -123,6 +123,8 @@ namespace Duplicati.Library.Main.Strings
|
||||
public static string LoglevelLong { get { return LC.L(@"Specify the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
|
||||
public static string LoglevelShort { get { return LC.L(@"Log information level"); } }
|
||||
public static string LogLevelDeprecated(string option1, string option2) { return LC.L("Use the options --{0} and --{1} instead.", option1, option2); }
|
||||
public static string SuppresswarningsLong { get { return LC.L(@"Suppress warnings and log them as information instead. Use this if you need to silence specific warnings. This option accepts a comma separated list of warning IDs."); } }
|
||||
public static string SuppresswarningsShort { get { return LC.L(@"Suppress specific warnings"); } }
|
||||
public static string DisableautocreatefolderLong { get { return LC.L(@"If Duplicati detects that the target folder is missing, it will create it automatically. Activate this option to prevent automatic folder creation."); } }
|
||||
public static string DisableautocreatefolderShort { get { return LC.L(@"Disable automatic folder creation"); } }
|
||||
public static string VssexcludewritersLong { get { return LC.L(@"Use this option to exclude faulty writers from a snapshot. This is equivalent to the -wx flag of the vshadow.exe tool, except that it only accepts writer class GUIDs, and not component names or instance GUIDs. Multiple GUIDs must be separated with a semicolon, and most forms of GUIDs are allowed, including with and without curly braces."); } }
|
||||
@@ -289,10 +291,14 @@ namespace Duplicati.Library.Main.Strings
|
||||
public static string LogfileloglevelShort { get { return LC.L(@"Log file information level"); } }
|
||||
public static string LogfilelogfiltersLong(string delimiter) { return LC.L(@"This option accepts filters that removes or includes messages regardless of their log level. Multiple filters are supported by separating with {0}. Filters are matched against the log tag and assumed to be including, unless they start with '-'. Regular expressions are supported within hard braces. Example: ""+Path*{0}+*Mail*{0}-[.*DNS]"" ", delimiter); }
|
||||
public static string LogfilelogfiltersShort { get { return LC.L(@"Apply filters to the file log data"); } }
|
||||
public static string LogfilelogignoreLong(string option) { return LC.L(@"This is a simplified version of the --{0} option. It will ignore all log messages that have an ID in the list. The list is a comma separated list of log message ids.", option); }
|
||||
public static string LogfilelogignoreShort { get { return LC.L(@"Ignore log messages with the specified IDs"); } }
|
||||
public static string ConsoleloglevelLong { get { return LC.L(@"Specify the amount of log information to output to the console."); } }
|
||||
public static string ConsoleloglevelShort { get { return LC.L(@"Console information level"); } }
|
||||
public static string ConsolelogfiltersLong(string delimiter) { return LogfilelogfiltersLong(delimiter); }
|
||||
public static string ConsolelogfiltersShort { get { return LC.L(@"Apply filters to the console log data"); } }
|
||||
public static string ConsolelogignoreLong(string option) { return LogfilelogignoreLong(option); }
|
||||
public static string ConsolelogignoreShort { get { return LC.L(@"Ignore log messages with the specified IDs"); } }
|
||||
|
||||
public static string UsebackgroundiopriorityLong { get { return LC.L("This option instructs the operating system to set the current process to use the lowest IO priority level, which can make operations run slower but will interfere less with other operations running at the same time."); } }
|
||||
public static string UsebackgroundiopriorityShort { get { return LC.L("Set the process to use low IO priority"); } }
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace Duplicati.Server
|
||||
private RingBuffer<ILogWriteHandler.LiveLogEntry> m_buffer;
|
||||
|
||||
|
||||
private readonly ControllerMultiLogTarget m_target = new ControllerMultiLogTarget(null, LogMessageType.Warning, null);
|
||||
private readonly ControllerMultiLogTarget m_target = new ControllerMultiLogTarget(null, LogMessageType.Warning, null, null);
|
||||
private LogMessageType m_logLevel;
|
||||
|
||||
public LogWriteHandler()
|
||||
|
||||
@@ -47,7 +47,7 @@ public static class EnumerableExtensions
|
||||
/// <returns>A sequence that contains only the non-null elements from the input sequence.</returns>
|
||||
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : struct
|
||||
=> source.Where(x => x != null)
|
||||
.Select(x => x.Value);
|
||||
.Select(x => x!.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Filters a sequence of values to exclude null or whitespace elements.
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Duplicati.Library.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for exceptions
|
||||
/// </summary>
|
||||
public static class ExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Flattens an exception and its inner exceptions
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception to flatten</param>
|
||||
/// <returns>An enumerable of exceptions</returns>
|
||||
public static IEnumerable<Exception> FlattenException(Exception? ex)
|
||||
{
|
||||
if (ex == null)
|
||||
yield break;
|
||||
|
||||
yield return ex;
|
||||
|
||||
if (ex is AggregateException aex)
|
||||
foreach (var iex in aex.Flatten().InnerExceptions)
|
||||
foreach (var iex2 in FlattenException(iex))
|
||||
yield return iex2;
|
||||
|
||||
foreach (var iex in FlattenException(ex.InnerException))
|
||||
yield return iex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an exception is a stop, cancel or timeout exception
|
||||
/// </summary>
|
||||
/// <param name="ex">The operation to check</param>
|
||||
/// <returns><c>true</c> if the exception is a stop or cancel exception, <c>false</c> otherwise</returns>
|
||||
public static bool IsAbortOrCancelException(this Exception ex)
|
||||
=> ex is OperationCanceledException || ex is ThreadAbortException || ex is TaskCanceledException || ex is TimeoutException;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an exception is a stop exception
|
||||
/// </summary>
|
||||
/// <param name="ex">The operation to check</param>
|
||||
/// <returns><c>true</c> if the exception is a stop exception, <c>false</c> otherwise</returns>
|
||||
public static bool IsAbortException(this Exception ex)
|
||||
=> ex is OperationCanceledException || ex is ThreadAbortException;
|
||||
|
||||
/// <summary>
|
||||
/// List of Windows error codes that indicate a permission denied error
|
||||
/// </summary>
|
||||
private static readonly int[] WindowsPermissionDeniedCodes = {
|
||||
5, // ERROR_ACCESS_DENIED
|
||||
19, // ERROR_WRITE_PROTECT
|
||||
65, // ERROR_NETWORK_ACCESS_DENIED
|
||||
82, // ERROR_CANNOT_MAKE
|
||||
1314, // ERROR_PRIVILEGE_NOT_HELD
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// List of Windows error codes that indicate a file lock error
|
||||
/// </summary>
|
||||
private static readonly int[] WindowsFileLockCodes = {
|
||||
32, // ERROR_SHARING_VIOLATION
|
||||
33 // ERROR_LOCK_VIOLATION
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// List of Windows error codes that indicate a path not found error
|
||||
/// </summary>
|
||||
private static readonly int[] WindowsPathNotFoundCodes = {
|
||||
2, // ERROR_FILE_NOT_FOUND
|
||||
3, // ERROR_PATH_NOT_FOUND
|
||||
21, // ERROR_NOT_READY (e.g., missing drive)
|
||||
267 // ERROR_DIRECTORY
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// List of Windows error codes that indicate a path too long error
|
||||
/// </summary>
|
||||
private static readonly int[] WindowsPathTooLongCodes = {
|
||||
206, // ERROR_FILENAME_EXCED_RANGE
|
||||
3 // ERROR_PATH_NOT_FOUND (used when long path causes resolution failure)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// List of Posix error codes that indicate a permission denied error
|
||||
/// </summary>
|
||||
private static readonly int[] PosixPermissionErrnos = {
|
||||
1, // EPERM
|
||||
13, // EACCES
|
||||
30, // EROFS - Read-only file system
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// List of Posix error codes that indicate a file lock error
|
||||
/// </summary>
|
||||
private static readonly int[] PosixFileLockErrnos = {
|
||||
// Linux file lock errors are not always distinguishable
|
||||
11 // EAGAIN (on NFS), or temporary unavailable (used for lock contention in some cases)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// List of Posix error codes that indicate a path not found error
|
||||
/// </summary>
|
||||
private static readonly int[] PosixPathNotFoundErrnos = {
|
||||
2, // ENOENT - No such file or directory
|
||||
20 // ENOTDIR - A path component is not a directory
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// List of Posix error codes that indicate a path too long error
|
||||
/// </summary>
|
||||
private static readonly int[] PosixPathTooLongErrnos = {
|
||||
36, // ENAMETOOLONG - File name too long
|
||||
63 // ENAMETOOLONG (on some macOS versions)
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an exception is a permission denied error
|
||||
/// </summary>
|
||||
/// <param name="ex">The operation to check</param>
|
||||
/// <returns><c>true</c> if the exception is a permission denied error, <c>false</c> otherwise</returns>
|
||||
public static bool IsPermissionDeniedException(this Exception? ex)
|
||||
{
|
||||
if (ex is UnauthorizedAccessException)
|
||||
return true;
|
||||
|
||||
if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
|
||||
return WindowsPermissionDeniedCodes.Contains(win32Ex.NativeErrorCode);
|
||||
|
||||
if (ex is IOException ioEx)
|
||||
{
|
||||
if (ioEx.HResult == unchecked((int)0x80070005)) // E_ACCESSDENIED
|
||||
return true;
|
||||
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
||||
PosixPermissionErrnos.Contains(innerWin32.NativeErrorCode))
|
||||
return true;
|
||||
|
||||
var errno = ioEx.HResult & 0xFFFF;
|
||||
return PosixPermissionErrnos.Contains(errno);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an exception is a file lock error
|
||||
/// </summary>
|
||||
/// <param name="ex">The operation to check</param>
|
||||
/// <returns><c>true</c> if the exception is a file lock error, <c>false</c> otherwise</returns>
|
||||
public static bool IsFileLockedException(this Exception? ex)
|
||||
{
|
||||
if (ex is IOException ioEx)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// Check inner Win32Exception
|
||||
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
||||
WindowsFileLockCodes.Contains(innerWin32.NativeErrorCode))
|
||||
return true;
|
||||
|
||||
// Fallback to HRESULT
|
||||
var winCode = ioEx.HResult & 0xFFFF;
|
||||
return WindowsFileLockCodes.Contains(winCode);
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
||||
PosixFileLockErrnos.Contains(innerWin32.NativeErrorCode))
|
||||
return true;
|
||||
|
||||
var errno = ioEx.HResult & 0xFFFF;
|
||||
return PosixFileLockErrnos.Contains(errno);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an exception is a path not found error
|
||||
/// </summary>
|
||||
/// <param name="ex">The operation to check</param>
|
||||
/// <returns><c>true</c> if the exception is a path not found error, <c>false</c> otherwise</returns>
|
||||
public static bool IsPathNotFoundException(this Exception? ex)
|
||||
{
|
||||
if (ex is FileNotFoundException || ex is DirectoryNotFoundException)
|
||||
return true;
|
||||
|
||||
if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
|
||||
return WindowsPathNotFoundCodes.Contains(win32Ex.NativeErrorCode);
|
||||
|
||||
if (ex is IOException ioEx)
|
||||
{
|
||||
var code = ioEx.HResult & 0xFFFF;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
return WindowsPathNotFoundCodes.Contains(code);
|
||||
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
||||
PosixPathNotFoundErrnos.Contains(innerWin32.NativeErrorCode))
|
||||
return true;
|
||||
|
||||
return PosixPathNotFoundErrnos.Contains(code);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an exception is a path too long error
|
||||
/// </summary>
|
||||
/// <param name="ex">The operation to check</param>
|
||||
/// <returns><c>true</c> if the exception is a path too long error, <c>false</c> otherwise</returns>
|
||||
public static bool IsPathTooLongException(this Exception? ex)
|
||||
{
|
||||
if (ex is PathTooLongException)
|
||||
return true;
|
||||
|
||||
if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
|
||||
return WindowsPathTooLongCodes.Contains(win32Ex.NativeErrorCode);
|
||||
|
||||
if (ex is IOException ioEx)
|
||||
{
|
||||
var code = ioEx.HResult & 0xFFFF;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
return WindowsPathTooLongCodes.Contains(code);
|
||||
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (ioEx.InnerException is Win32Exception innerWin32 &&
|
||||
PosixPathTooLongErrnos.Contains(innerWin32.NativeErrorCode))
|
||||
return true;
|
||||
|
||||
return PosixPathTooLongErrnos.Contains(code);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,32 @@
|
||||
// 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
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Globalization;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Duplicati.Library.Utility
|
||||
{
|
||||
@@ -71,11 +74,11 @@ namespace Duplicati.Library.Utility
|
||||
/// <summary>
|
||||
/// The filter string
|
||||
/// </summary>
|
||||
public readonly string Filter;
|
||||
public readonly string? Filter;
|
||||
/// <summary>
|
||||
/// The regular expression version of the filter
|
||||
/// </summary>
|
||||
public readonly Regex Regexp;
|
||||
public readonly Regex? Regexp;
|
||||
|
||||
/// <summary>
|
||||
/// The single wildcard character (DOS style)
|
||||
@@ -104,7 +107,7 @@ namespace Duplicati.Library.Utility
|
||||
/// Initializes a new instance of the <see cref="T:Duplicati.Library.Utility.FilterExpression.FilterEntry"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filter string to use.</param>
|
||||
public FilterEntry(string filter)
|
||||
public FilterEntry(string? filter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter))
|
||||
{
|
||||
@@ -155,8 +158,7 @@ namespace Duplicati.Library.Utility
|
||||
private static Regex GetFilterGroupRegex(string filterGroupName)
|
||||
{
|
||||
FilterGroup filterGroup = FilterGroups.ParseFilterList(filterGroupName, FilterGroup.None);
|
||||
Regex result;
|
||||
if (FilterEntry.filterGroupRegexCache.TryGetValue(filterGroup, out result))
|
||||
if (FilterEntry.filterGroupRegexCache.TryGetValue(filterGroup, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
@@ -290,17 +292,17 @@ namespace Duplicati.Library.Utility
|
||||
case FilterType.Simple:
|
||||
return string.Equals(this.Filter, path, Library.Utility.Utility.ClientFilenameStringComparison);
|
||||
case FilterType.Wildcard:
|
||||
return IsWildcardMatch(!Utility.IsFSCaseSensitive ? path.ToUpper(CultureInfo.InvariantCulture) : path, this.Filter);
|
||||
return IsWildcardMatch(!Utility.IsFSCaseSensitive ? path.ToUpper(CultureInfo.InvariantCulture) : path, this.Filter!);
|
||||
case FilterType.Regexp:
|
||||
case FilterType.Group:
|
||||
var m = this.Regexp.Match(path);
|
||||
var m = this.Regexp!.Match(path);
|
||||
return m.Success && m.Length == path.Length;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
public override string? ToString()
|
||||
{
|
||||
switch (this.Type)
|
||||
{
|
||||
@@ -319,7 +321,7 @@ namespace Duplicati.Library.Utility
|
||||
/// <summary>
|
||||
/// The internal list of expressions
|
||||
/// </summary>
|
||||
private readonly List<FilterEntry> m_filters;
|
||||
private readonly List<FilterEntry>? m_filters;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the filter
|
||||
@@ -354,7 +356,7 @@ namespace Duplicati.Library.Utility
|
||||
/// </summary>
|
||||
/// <param name="result">The match result</param>
|
||||
/// <param name="match">The filter that matched</param>
|
||||
public bool Matches(string path, out bool result, out IFilter match)
|
||||
public bool Matches(string path, out bool result, out IFilter? match)
|
||||
{
|
||||
result = false;
|
||||
if (this.Type == FilterType.Empty)
|
||||
@@ -363,7 +365,7 @@ namespace Duplicati.Library.Utility
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_filters.Any(x => x.Matches(path)))
|
||||
if (m_filters != null && m_filters.Any(x => x.Matches(path)))
|
||||
{
|
||||
match = this;
|
||||
result = this.Result;
|
||||
@@ -385,7 +387,7 @@ namespace Duplicati.Library.Utility
|
||||
/// Creates a new <see cref="FilterExpression"/> instance, representing an empty filter.
|
||||
/// </summary>
|
||||
public FilterExpression()
|
||||
: this((IEnumerable<string>)null, true)
|
||||
: this((IEnumerable<string>?)null, true)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -403,7 +405,7 @@ namespace Duplicati.Library.Utility
|
||||
/// </summary>
|
||||
/// <param name="filter">The filter string that represents the filter</param>
|
||||
/// <param name="result">Return value of <see cref="Matches(string,out bool,out IFilter)"/> in case of match</param>
|
||||
public FilterExpression(IEnumerable<string> filter, bool result = true)
|
||||
public FilterExpression(IEnumerable<string>? filter, bool result = true)
|
||||
{
|
||||
this.Result = result;
|
||||
|
||||
@@ -414,10 +416,9 @@ namespace Duplicati.Library.Utility
|
||||
}
|
||||
|
||||
m_filters = Compact(
|
||||
(from n in filter
|
||||
let nx = new FilterEntry(n)
|
||||
where nx.Type != FilterType.Empty
|
||||
select nx)
|
||||
filter
|
||||
.Select(x => new FilterEntry(x))
|
||||
.Where(x => x.Type != FilterType.Empty)
|
||||
);
|
||||
|
||||
if (m_filters.Count == 0)
|
||||
@@ -426,7 +427,7 @@ namespace Duplicati.Library.Utility
|
||||
this.Type = m_filters.Max((a) => a.Type);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> Expand(string filter)
|
||||
private static IEnumerable<string>? Expand(string filter)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filter))
|
||||
return null;
|
||||
@@ -459,7 +460,7 @@ namespace Duplicati.Library.Utility
|
||||
// since that would make their names much more difficult to interpret on the command line.
|
||||
if (f.Type == FilterType.Simple || f.Type == FilterType.Wildcard || f.Type == FilterType.Group)
|
||||
r.Add(f);
|
||||
else if (f.Type != FilterType.Empty)
|
||||
else if (f.Type != FilterType.Empty && f.Regexp != null)
|
||||
{
|
||||
combined.Append(f.Regexp.ToString());
|
||||
first = true;
|
||||
@@ -518,12 +519,12 @@ namespace Duplicati.Library.Utility
|
||||
/// <param name="filter">The filter to examine</param>
|
||||
/// <param name="includes">True if the filter contains includes, false otherwise.</param>
|
||||
/// <param name="excludes">True if the filter contains excludes, false otherwise.</param>
|
||||
public static void AnalyzeFilters(IFilter filter, out bool includes, out bool excludes)
|
||||
public static void AnalyzeFilters(IFilter? filter, out bool includes, out bool excludes)
|
||||
{
|
||||
includes = false;
|
||||
excludes = false;
|
||||
|
||||
Tuple<bool, bool> cacheLookup = null;
|
||||
Tuple<bool, bool>? cacheLookup = null;
|
||||
|
||||
// Check for cached results
|
||||
if (filter != null)
|
||||
@@ -537,7 +538,7 @@ namespace Duplicati.Library.Utility
|
||||
// Figure out what components are in the filter
|
||||
if (cacheLookup == null)
|
||||
{
|
||||
var q = new Queue<IFilter>();
|
||||
var q = new Queue<IFilter?>();
|
||||
q.Enqueue(filter);
|
||||
|
||||
while (q.Count > 0)
|
||||
@@ -564,7 +565,8 @@ namespace Duplicati.Library.Utility
|
||||
{
|
||||
if (_matchFallbackLookup.Count > 10)
|
||||
_matchFallbackLookup.Remove(_matchFallbackLookup.Keys.Skip(new Random().Next(0, _matchFallbackLookup.Count)).First());
|
||||
_matchFallbackLookup[filter] = new Tuple<bool, bool>(includes, excludes);
|
||||
if (filter != null)
|
||||
_matchFallbackLookup[filter] = new Tuple<bool, bool>(includes, excludes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,7 +577,7 @@ namespace Duplicati.Library.Utility
|
||||
/// <param name="filter">The filter to evaluate</param>
|
||||
/// <param name="path">The path to evaluate</param>
|
||||
/// <param name="match">The filter that matched</param>
|
||||
public static bool Matches(IFilter filter, string path, out IFilter match)
|
||||
public static bool Matches(IFilter filter, string path, out IFilter? match)
|
||||
{
|
||||
if (filter == null || filter.Empty)
|
||||
{
|
||||
@@ -583,14 +585,11 @@ namespace Duplicati.Library.Utility
|
||||
return true;
|
||||
}
|
||||
|
||||
bool result;
|
||||
if (filter.Matches(path, out result, out match))
|
||||
if (filter.Matches(path, out bool result, out match))
|
||||
return result;
|
||||
|
||||
bool includes;
|
||||
bool excludes;
|
||||
|
||||
AnalyzeFilters(filter, out includes, out excludes);
|
||||
AnalyzeFilters(filter, out bool includes, out bool excludes);
|
||||
match = null;
|
||||
|
||||
// We have only include filters, we exclude files by default
|
||||
@@ -611,16 +610,16 @@ namespace Duplicati.Library.Utility
|
||||
/// </summary>
|
||||
/// <param name="first">First.</param>
|
||||
/// <param name="second">Second.</param>
|
||||
public static FilterExpression Combine(FilterExpression first, FilterExpression second)
|
||||
public static FilterExpression? Combine(FilterExpression? first, FilterExpression? second)
|
||||
{
|
||||
if (first == null || first.Empty)
|
||||
if (first?.m_filters == null || first.Empty)
|
||||
return second;
|
||||
if (second == null || second.Empty)
|
||||
if (second?.m_filters == null || second.Empty)
|
||||
return first;
|
||||
|
||||
if (first.Result != second.Result)
|
||||
throw new ArgumentException("Both filters must have the same result property");
|
||||
return new FilterExpression(first.m_filters.Union(second.m_filters).Select(x => x.ToString()), first.Result);
|
||||
return new FilterExpression(first.m_filters.Union(second.m_filters).Select(x => x.ToString()).WhereNotNull(), first.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -628,7 +627,7 @@ namespace Duplicati.Library.Utility
|
||||
/// </summary>
|
||||
/// <param name="first">First.</param>
|
||||
/// <param name="second">Second.</param>
|
||||
public static IFilter Combine(IFilter first, IFilter second)
|
||||
public static IFilter? Combine(IFilter? first, IFilter? second)
|
||||
{
|
||||
if (second == null || second.Empty)
|
||||
return first;
|
||||
@@ -663,15 +662,13 @@ namespace Duplicati.Library.Utility
|
||||
/// Serializes the filter instance into a list of strings
|
||||
/// that can be passed to the deserialize method
|
||||
/// </summary>
|
||||
public string[] Serialize()
|
||||
[return: NotNullIfNotNull("m_filters")]
|
||||
public string[]? Serialize()
|
||||
{
|
||||
if (this.Empty)
|
||||
if (this.Empty || m_filters == null)
|
||||
return null;
|
||||
|
||||
return
|
||||
(from n in m_filters
|
||||
select $"{(this.Result ? "+" : "-")}{n.ToString()}"
|
||||
).ToArray();
|
||||
return m_filters.Select(n => $"{(this.Result ? "+" : "-")}{n.ToString()}").ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -692,7 +689,7 @@ namespace Duplicati.Library.Utility
|
||||
var f = work.Pop();
|
||||
|
||||
if (f is FilterExpression expression)
|
||||
res = res.Union(expression.Serialize());
|
||||
res = res.Union(expression.Serialize() ?? []);
|
||||
else if (f is JoinedFilterExpression filterExpression)
|
||||
{
|
||||
work.Push(filterExpression.Second);
|
||||
@@ -710,12 +707,12 @@ namespace Duplicati.Library.Utility
|
||||
/// prefixed with either minus or plus
|
||||
/// </summary>
|
||||
/// <param name="filters">The filters to deserialize from.</param>
|
||||
public static IFilter Deserialize(string[] filters)
|
||||
public static IFilter? Deserialize(string[] filters)
|
||||
{
|
||||
if (filters == null || filters.Length == 0)
|
||||
return null;
|
||||
|
||||
IFilter res = null;
|
||||
IFilter? res = null;
|
||||
foreach (var n in filters)
|
||||
{
|
||||
bool include;
|
||||
@@ -737,7 +734,7 @@ namespace Duplicati.Library.Utility
|
||||
/// </summary>
|
||||
/// <returns>The log filter.</returns>
|
||||
/// <param name="value">The filter string to parse.</param>
|
||||
public static IFilter ParseLogFilter(string value)
|
||||
public static IFilter ParseLogFilter(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return new FilterExpression();
|
||||
@@ -745,7 +742,8 @@ namespace Duplicati.Library.Utility
|
||||
return value
|
||||
.Split(new char[] { System.IO.Path.PathSeparator, ':', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(StringToIFilter)
|
||||
.Aggregate(FilterExpression.Combine);
|
||||
.Cast<IFilter>()
|
||||
.Aggregate((a, b) => FilterExpression.Combine(a, b)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -789,8 +789,33 @@ namespace Duplicati.Library.Utility
|
||||
/// <param name="default">default value</param>
|
||||
/// <returns></returns>
|
||||
public static int ParseIntOption(IReadOnlyDictionary<string, string?> options, string value, int @default)
|
||||
=> options.TryGetValue(value, out var opt) && int.TryParse(opt ?? string.Empty, out var result) ? result : @default;
|
||||
|
||||
/// <summary>
|
||||
/// Parses an option with long value, returning the default value if the option is not found or cannot be parsed
|
||||
/// </summary>
|
||||
/// <param name="options">The set of options to look for the setting in</param>
|
||||
/// <param name="value">The value to look for in the settings</param>
|
||||
/// <param name="default">default value</param>
|
||||
/// <returns></returns>
|
||||
public static long ParseLongOption(IReadOnlyDictionary<string, string?> options, string value, long @default)
|
||||
=> options.TryGetValue(value, out var opt) && long.TryParse(opt ?? string.Empty, out var result) ? result : @default;
|
||||
|
||||
/// <summary>
|
||||
/// Parses a size option from the option set, returning the default value if the option is not found or cannot be parsed
|
||||
/// </summary>
|
||||
/// <param name="options">The set of options to look for the setting in</param>
|
||||
/// <param name="value">The value to look for in the settings</param>
|
||||
/// <param name="defaultMultiplier">Multiplier to use if the value does not have a multiplier</param>
|
||||
/// <param name="default">The default value to return if there are no matches.</param>
|
||||
/// <returns>The parsed or default size value.</returns>
|
||||
public static long ParseSizeOption(IReadOnlyDictionary<string, string?> options, string value, string defaultMultiplier, string @default)
|
||||
{
|
||||
return options.TryGetValue(value, out var opt) && int.TryParse(opt ?? string.Empty, out var result) ? result : @default;
|
||||
var opt = options.GetValueOrDefault(value);
|
||||
if (string.IsNullOrWhiteSpace(opt))
|
||||
opt = @default;
|
||||
|
||||
return Sizeparser.ParseSize(opt, defaultMultiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1569,26 +1594,7 @@ namespace Duplicati.Library.Utility
|
||||
public static string IpVersionCompatibleLoopback =>
|
||||
HasIPv4Loopback ? IPAddress.Loopback.ToString() : $"[{IPAddress.IPv6Loopback.ToString()}]";
|
||||
|
||||
/// <summary>
|
||||
/// Flattens an exception and its inner exceptions
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception to flatten</param>
|
||||
/// <returns>An enumerable of exceptions</returns>
|
||||
public static IEnumerable<Exception> FlattenException(Exception? ex)
|
||||
{
|
||||
if (ex == null)
|
||||
yield break;
|
||||
|
||||
yield return ex;
|
||||
|
||||
if (ex is AggregateException aex)
|
||||
foreach (var iex in aex.Flatten().InnerExceptions)
|
||||
foreach (var iex2 in FlattenException(iex))
|
||||
yield return iex2;
|
||||
|
||||
foreach (var iex in FlattenException(ex.InnerException))
|
||||
yield return iex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Guesses the URL scheme and returns it
|
||||
@@ -1639,26 +1645,6 @@ namespace Duplicati.Library.Utility
|
||||
return sanitizedUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an exception is a stop, cancel or timeout exception
|
||||
/// </summary>
|
||||
/// <param name="ex">The operation to check</param>
|
||||
/// <returns><c>true</c> if the exception is a stop or cancel exception, <c>false</c> otherwise</returns>
|
||||
public static bool IsAbortOrCancelException(this Exception ex)
|
||||
{
|
||||
return ex is OperationCanceledException || ex is ThreadAbortException || ex is TaskCanceledException || ex is TimeoutException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an exception is a stop exception
|
||||
/// </summary>
|
||||
/// <param name="ex">The operation to check</param>
|
||||
/// <returns><c>true</c> if the exception is a stop exception, <c>false</c> otherwise</returns>
|
||||
public static bool IsAbortException(this Exception ex)
|
||||
{
|
||||
return ex is OperationCanceledException || ex is ThreadAbortException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the string using the invariant culture
|
||||
/// </summary>
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace Duplicati.UnitTest
|
||||
public Task GetAsync(string remotename, string filename, CancellationToken cancellationToken)
|
||||
=> backend.GetAsync(remotename, filename, cancellationToken);
|
||||
public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken)
|
||||
=> GetDNSNamesAsync(cancelToken);
|
||||
=> backend.GetDNSNamesAsync(cancelToken);
|
||||
public IAsyncEnumerable<IFileEntry> ListAsync(CancellationToken cancellationToken)
|
||||
=> ListModifier(backend.ListAsync(cancellationToken));
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Duplicati.UnitTest
|
||||
public Task GetAsync(string remotename, string filename, CancellationToken cancellationToken)
|
||||
=> backend.GetAsync(remotename, filename, cancellationToken);
|
||||
public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken)
|
||||
=> GetDNSNamesAsync(cancelToken);
|
||||
=> backend.GetDNSNamesAsync(cancelToken);
|
||||
public IAsyncEnumerable<IFileEntry> ListAsync(CancellationToken cancellationToken)
|
||||
=> Sorter(backend.ListAsync(cancellationToken).ToBlockingEnumerable()).ToAsyncEnumerable();
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Duplicati.UnitTest
|
||||
public Task GetAsync(string remotename, string filename, CancellationToken cancellationToken)
|
||||
=> backend.GetAsync(remotename, filename, cancellationToken);
|
||||
public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken)
|
||||
=> GetDNSNamesAsync(cancelToken);
|
||||
=> backend.GetDNSNamesAsync(cancelToken);
|
||||
public IAsyncEnumerable<IFileEntry> ListAsync(CancellationToken cancellationToken)
|
||||
=> backend.ListAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -1016,7 +1016,9 @@ public static partial class Command
|
||||
Directory.Delete(tmpbuild, true);
|
||||
Directory.CreateDirectory(tmpbuild);
|
||||
|
||||
var tarsrc = Path.Combine(tmpbuild, $"duplicati-{rtcfg.ReleaseInfo.Version}");
|
||||
var servicename = target.Interface == InterfaceType.Agent ? "-agent" : "";
|
||||
|
||||
var tarsrc = Path.Combine(tmpbuild, $"duplicati{servicename}-{rtcfg.ReleaseInfo.Version}");
|
||||
EnvHelper.CopyDirectory(Path.Combine(buildRoot, target.BuildTargetString), tarsrc, recursive: true);
|
||||
|
||||
await PackageSupport.InstallPackageIdentifier(tarsrc, target);
|
||||
@@ -1090,7 +1092,7 @@ public static partial class Command
|
||||
.Replace("%BUILDVERSION%", rtcfg.ReleaseInfo.Version.ToString())
|
||||
.Replace("%BUILDTAG%", rtcfg.ReleaseInfo.Channel.ToString().ToLowerInvariant())
|
||||
.Replace("%VERSION%", rtcfg.ReleaseInfo.Version.ToString())
|
||||
.Replace("%SERVICENAME%", target.Interface == InterfaceType.Agent ? "-agent" : "")
|
||||
.Replace("%SERVICENAME%", servicename)
|
||||
.Replace("%PROVIDES%", string.Join("\n", executables.Select(x => $"Provides:\t{x}")))
|
||||
.Replace("%DEPENDS%", string.Join("\n",
|
||||
(target.Interface == InterfaceType.GUI
|
||||
|
||||
@@ -10,8 +10,8 @@ Version: %VERSION%
|
||||
Description: Backup client for encrypted online backups
|
||||
Duplicati is a free open-source backup client that securely stores encrypted, incremental,
|
||||
compressed backups on cloud storage services and remote file servers. It
|
||||
supports targets like Amazon S3, Windows Live SkyDrive, Rackspace Cloud Files
|
||||
or WebDAV, SSH, FTP (and many more).
|
||||
supports targets like Amazon S3, Backblaze B2, Microsoft OneDrive,
|
||||
WebDAV, SSH, FTP (and many more).
|
||||
.
|
||||
Duplicati has built-in AES-256 encryption and backups be can signed using GNU
|
||||
Privacy Guard. A built-in scheduler makes sure that backups are always
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# Set up some defaults
|
||||
%global namer duplicati
|
||||
%global installname %{namer}%SERVICENAME%
|
||||
%global debug_package %{nil}
|
||||
%global alphatag .git
|
||||
%global _builddate %BUILDDATE%
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
# All lines starting with #GUI_ONLY# are removed in CLI builds
|
||||
|
||||
Name: %{namer}
|
||||
Name: %{installname}
|
||||
Version: %{_buildversion}
|
||||
Release: %{_buildtag}
|
||||
Icon: duplicati.xpm
|
||||
@@ -27,11 +28,11 @@ URL: https://duplicati.com
|
||||
Source0: duplicati-%{_buildversion}.tar.bz2
|
||||
Source1: %{namer}-install-recursive.sh
|
||||
Source2: %{namer}-install-binaries.sh
|
||||
Source3: %{namer}%SERVICENAME%.service
|
||||
Source4: %{namer}%SERVICENAME%.default
|
||||
Source3: %{installname}.service
|
||||
Source4: %{installname}.default
|
||||
#GUI_ONLY#Source5: %{namer}.png
|
||||
#GUI_ONLY#Source6: %{namer}.desktop
|
||||
#AGENT_ONLY#Source5: %{namer}%SERVICENAME%.preset
|
||||
#AGENT_ONLY#Source5: %{installname}.preset
|
||||
|
||||
#GUI_ONLY#BuildRequires: desktop-file-utils
|
||||
BuildRequires: systemd
|
||||
@@ -44,8 +45,8 @@ Requires: bash
|
||||
%description
|
||||
Duplicati is a free backup client that securely stores encrypted,
|
||||
incremental, compressed backups on cloud storage services and remote file
|
||||
servers. It supports targets like Amazon S3, Windows Live SkyDrive,
|
||||
Rackspace Cloud Files or WebDAV, SSH, FTP (and many more).
|
||||
servers. It supports targets like Amazon S3, Backblaze B2,
|
||||
Microsoft OneDrive, WebDAV, SSH, FTP (and many more).
|
||||
|
||||
Duplicati has built-in AES-256 encryption and backups be can signed using
|
||||
GNU Privacy Guard. A built-in scheduler makes sure that backups are always
|
||||
@@ -54,7 +55,7 @@ tweaks like filters, deletion rules, transfer and bandwidth options to run
|
||||
backups for specific purposes.
|
||||
|
||||
%prep
|
||||
%setup -q -n %{namer}-%{_buildversion}
|
||||
%setup -q -n %{installname}-%{_buildversion}
|
||||
|
||||
%build
|
||||
|
||||
@@ -64,10 +65,10 @@ backups for specific purposes.
|
||||
%install
|
||||
|
||||
#GUI_ONLY#install -d %{buildroot}%{_datadir}/pixmaps/
|
||||
install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/
|
||||
install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/licenses/
|
||||
install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/webroot/
|
||||
install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/lvm-scripts/
|
||||
install -d %{buildroot}%{_exec_prefix}/lib/%{installname}/
|
||||
install -d %{buildroot}%{_exec_prefix}/lib/%{installname}/licenses/
|
||||
install -d %{buildroot}%{_exec_prefix}/lib/%{installname}/webroot/
|
||||
install -d %{buildroot}%{_exec_prefix}/lib/%{installname}/lvm-scripts/
|
||||
install -d %{buildroot}%{_exec_prefix}/bin/
|
||||
install -d %{buildroot}%{_unitdir}
|
||||
install -d %{buildroot}%{_sysconfdir}/sysconfig/
|
||||
@@ -77,35 +78,35 @@ install -d %{buildroot}%{_sysconfdir}/sysconfig/
|
||||
find . -type f -name ._\* | xargs rm -rf
|
||||
|
||||
# Install all files, but the list is too long to be in the script itself :/
|
||||
/bin/bash %{_topdir}/SOURCES/%{namer}-install-recursive.sh "." "%{buildroot}%{_exec_prefix}/lib/%{namer}/"
|
||||
/bin/bash %{_topdir}/SOURCES/%{namer}-install-recursive.sh "." "%{buildroot}%{_exec_prefix}/lib/%{installname}/"
|
||||
|
||||
# Move the icon in to place
|
||||
#GUI_ONLY#install -p %{_topdir}/SOURCES/%{namer}.png %{buildroot}%{_datadir}/pixmaps/
|
||||
|
||||
# Fix executable permissions and install symlinks
|
||||
/bin/bash %{_topdir}/SOURCES/%{namer}-install-binaries.sh "%{buildroot}" "%{_exec_prefix}" "%{namer}"
|
||||
/bin/bash %{_topdir}/SOURCES/%{namer}-install-binaries.sh "%{buildroot}" "%{_exec_prefix}" "%{installname}"
|
||||
|
||||
#GUI_ONLY#desktop-file-install %{_topdir}/SOURCES/%{namer}.desktop
|
||||
|
||||
# Install the service:
|
||||
install -p -D -m 644 %{_topdir}/SOURCES/%{namer}%SERVICENAME%.service %{buildroot}%{_unitdir}
|
||||
install -p -D -m 644 %{_topdir}/SOURCES/%{namer}%SERVICENAME%.default %{buildroot}%{_sysconfdir}/sysconfig/%{namer}
|
||||
|
||||
install -p -D -m 644 %{_topdir}/SOURCES/%{namer}%SERVICENAME%.default %{buildroot}%{_sysconfdir}/sysconfig/%{installname}
|
||||
#AGENT_ONLY#install -p -D -m 644 %{_topdir}/SOURCES/%{installname}.preset %{buildroot}%{_presetdir}/50-%{installname}.preset
|
||||
|
||||
%post
|
||||
#GUI_ONLY#/bin/touch --no-create %{_datadir}/icons/hicolor || :
|
||||
#GUI_ONLY#%{_bindir}/gtk-update-icon-cache \
|
||||
#GUI_ONLY# --quiet %{_datadir}/icons/hicolor 2> /dev/null|| :
|
||||
%systemd_post %{namer}%SERVICENAME%.service
|
||||
#AGENT_ONLY#%{__install} -Dm644 %{namer}%SERVICENAME%.preset %{buildroot}%{_presetdir}/50-%{namer}%SERVICENAME%.preset
|
||||
%systemd_post %{installname}.service
|
||||
|
||||
%preun
|
||||
%systemd_preun %{namer}%SERVICENAME%.service
|
||||
%systemd_preun %{installname}.service
|
||||
|
||||
%postun
|
||||
#GUI_ONLY#/bin/touch --no-create %{_datadir}/icons/hicolor || :
|
||||
#GUI_ONLY#%{_bindir}/gtk-update-icon-cache \
|
||||
#GUI_ONLY# --quiet %{_datadir}/icons/hicolor 2> /dev/null|| :
|
||||
%systemd_postun_with_restart %{namer}%SERVICENAME%.service
|
||||
%systemd_postun_with_restart %{installname}.service
|
||||
|
||||
%posttrans
|
||||
#GUI_ONLY#/usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :
|
||||
@@ -116,10 +117,14 @@ install -p -D -m 644 %{_topdir}/SOURCES/%{namer}%SERVICENAME%.default %{buildroo
|
||||
#GUI_ONLY#%{_datadir}/*/*
|
||||
%{_exec_prefix}/lib/*
|
||||
%{_exec_prefix}/bin/*
|
||||
%{_sysconfdir}/sysconfig/*
|
||||
|
||||
%config(noreplace) %{_sysconfdir}/sysconfig/%{installname}
|
||||
#AGENT_ONLY#%{_presetdir}/50-%{installname}.preset
|
||||
|
||||
%changelog
|
||||
* Mon May 19 2025 Kenneth Skovhede <kenneth@duplicati.com> - 2.0.0-0.20250519.git
|
||||
- Fixed config files not being overwritten
|
||||
- Moved agent files into separate folder to allow cli and agent to co-exist
|
||||
|
||||
* Wed May 22 2024 Kenneth Skovhede <kenneth@duplicati.com> - 2.0.0-0.20240522.git
|
||||
- Split for CLI and GUI package
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ destination will be verified before being overwritten (if they seemingly match).
|
||||
log_level_enum = log_level_parsed ? log_level_enum : Duplicati.Library.Logging.LogMessageType.Information;
|
||||
|
||||
using var console_sink = new Duplicati.CommandLine.ConsoleOutput(Console.Out, global_options);
|
||||
using var multi_sink = new Duplicati.Library.Main.ControllerMultiLogTarget(console_sink, log_level_enum, null);
|
||||
using var multi_sink = new Duplicati.Library.Main.ControllerMultiLogTarget(console_sink, log_level_enum, null, null);
|
||||
|
||||
// Parse the log file
|
||||
// The log file sink doesn't have to be disposed, as the multi_sink will take care of it
|
||||
|
||||
Reference in New Issue
Block a user