From 80ce9935260029b222a9d4fa80a0bf10ac934605 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 16 Apr 2025 19:05:51 +0200 Subject: [PATCH 01/12] Simple fix to prevent uploading dlist on failures --- Duplicati/Library/Main/Operation/BackupHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Duplicati/Library/Main/Operation/BackupHandler.cs b/Duplicati/Library/Main/Operation/BackupHandler.cs index 226b265b8..03f8af50b 100644 --- a/Duplicati/Library/Main/Operation/BackupHandler.cs +++ b/Duplicati/Library/Main/Operation/BackupHandler.cs @@ -709,6 +709,8 @@ namespace Duplicati.Library.Main.Operation using (new Logging.Timer(LOGTAG, "VerifyConsistency", "VerifyConsistency")) await db.VerifyConsistencyAsync(m_options.Blocksize, m_options.BlockhashSize, false); + await FlushBackend(m_database, null, m_result, backendManager).ConfigureAwait(false); + // Send the actual filelist await Backup.UploadRealFilelist.Run(m_result, db, backendManager, m_options, filesetvolume, filesetid, m_result.TaskControl, lastTempVolumeIncomplete); From 5511ed6a46072af20ba636e655f56e7583cf76c4 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 22 Apr 2025 13:08:36 +0200 Subject: [PATCH 02/12] Added support for additional libicu versions --- ReleaseBuilder/Build/Command.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReleaseBuilder/Build/Command.cs b/ReleaseBuilder/Build/Command.cs index d9ee5c90e..ec7406c67 100644 --- a/ReleaseBuilder/Build/Command.cs +++ b/ReleaseBuilder/Build/Command.cs @@ -99,7 +99,7 @@ public static partial class Command /// The supported versions of libicu for Debian /// private static string DebianLibIcuVersions => "libicu | " + string.Join(" | ", - "74;72;71;70;69;68;67;66;65;63;60;57;55;52" + "78;77;76;75;74;73;72;71;70;69;68;67;66;65;63;60;57;55;52" .Split(";", StringSplitOptions.RemoveEmptyEntries) .Select(x => $"libicu{x}")); From 7be96780802adcd6866fe91463200b532af14eb2 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 11:48:35 +0200 Subject: [PATCH 03/12] Fixed handling promotion of pending upload to actual filelist --- Duplicati/Library/Main/Operation/BackupHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Duplicati/Library/Main/Operation/BackupHandler.cs b/Duplicati/Library/Main/Operation/BackupHandler.cs index 03f8af50b..636472463 100644 --- a/Duplicati/Library/Main/Operation/BackupHandler.cs +++ b/Duplicati/Library/Main/Operation/BackupHandler.cs @@ -375,7 +375,8 @@ namespace Duplicati.Library.Main.Operation throw; } - return new PreBackupVerifyResult(database, lastTempFilelist); + // Return the updated version of the fileset, in case it was actually complete + return new PreBackupVerifyResult(database, database.GetLastIncompleteFilesetVolume(null)); } catch { From 3ed195d8218a06dea8d16e40be8d14e5f0a2215c Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 11:49:06 +0200 Subject: [PATCH 04/12] Updated tests --- Duplicati/UnitTest/DisruptionTests.cs | 45 +++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/Duplicati/UnitTest/DisruptionTests.cs b/Duplicati/UnitTest/DisruptionTests.cs index 2f10b2371..2e106e24a 100644 --- a/Duplicati/UnitTest/DisruptionTests.cs +++ b/Duplicati/UnitTest/DisruptionTests.cs @@ -898,12 +898,15 @@ namespace Duplicati.UnitTest using (var c = new Controller("file://" + TARGETFOLDER, testopts.Expand(new { full_remote_verification = true }), null)) TestUtils.AssertResults(c.Test(long.MaxValue)); + // If we fail on the dlist after the upload, it is promoted to a regular backup + var expectedFilesets = before || failOnLastDblock ? 2 : 1; + // Make a new backup, should continue as normal using (var c = new Controller("file://" + TARGETFOLDER, testopts, null)) { var backupResults = c.Backup(new string[] { DATAFOLDER }); TestUtils.AssertResults(backupResults); - Assert.AreEqual(2, c.List().Filesets.Count()); + Assert.AreEqual(expectedFilesets, c.List().Filesets.Count()); } // Test that we can recreate @@ -916,12 +919,12 @@ namespace Duplicati.UnitTest using (var c = new Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Repair()); - // Check that we have 2 versions + // Check that we have the correct versions using (var c = new Controller("file://" + TARGETFOLDER, testopts, null)) { var listResults = c.List(); TestUtils.AssertResults(listResults); - Assert.AreEqual(2, listResults.Filesets.Count()); + Assert.AreEqual(expectedFilesets, listResults.Filesets.Count()); } } @@ -984,12 +987,15 @@ namespace Duplicati.UnitTest // Make spacing for the next backup Thread.Sleep(3000); + // If we fail on the dlist after the upload, it is promoted to a regular backup + var expectedFilesets = before || failOnLastDblock ? 2 : 1; + // Make a new backup, should continue as normal using (var c = new Controller("file://" + TARGETFOLDER, testopts, null)) { IBackupResults backupResults = c.Backup(new string[] { DATAFOLDER }); TestUtils.AssertResults(backupResults); - Assert.AreEqual(2, c.List().Filesets.Count()); + Assert.AreEqual(expectedFilesets, c.List().Filesets.Count()); } // Verify that all is in order @@ -1011,7 +1017,7 @@ namespace Duplicati.UnitTest { var listResults = c.List(); TestUtils.AssertResults(listResults); - Assert.AreEqual(2, listResults.Filesets.Count()); + Assert.AreEqual(expectedFilesets, listResults.Filesets.Count()); } } @@ -1066,12 +1072,39 @@ namespace Duplicati.UnitTest [TestCase(false, true, 3, false)] [TestCase(true, true, 3, true)] [TestCase(false, true, 3, true)] + [TestCase(false, false, 3, true)] + [TestCase(true, false, 3, true)] public void TestMultiExceptionOnDlist(bool before, bool modifyInBetween, int runs, bool withBase) { var testopts = TestOptions; testopts["number-of-retries"] = "0"; testopts["dblock-size"] = "10mb"; - var expectedFilesets = withBase ? 3 : 2; + // We always have at least 1 backup at the end + var expectedFilesets = 1; + // If the files are modified, this will create multiple versions + if (modifyInBetween) + { + // If we prevent the file from being uploaded, + // it will be a temporary file, and a new synthetic file + // will be attempted, but that fails as well + // If it is after, then the upload completes, + // and it will be promoted to a regular uploaded backup + expectedFilesets += before ? 1 : runs; + + // If we modify with a base, then that creates a version as well + if (withBase) + expectedFilesets += 1; + } + else if (before && !withBase) + { + // Because an incomplete fileset is treated as a partial fileset, + // the final non-interrupted backup will see the syntheticly + // created fileset as a partial fileset, and create a new non-partial + // fileset, even though there are no changes between the two + // This does not happen with a base, because it simply does not + // attempt to upload the failing filesets + expectedFilesets += 1; + } if (withBase) { From e6ba6098d7dc24b8bab2c93751268d74288859d5 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 12:11:16 +0200 Subject: [PATCH 05/12] Implemented immediate transaction commits for almost all uploads. This ensures that files cannot appear on the remote destination, despite terminating the host program. This fixes #4485 There is still work needed to handle the repair and compact operations so they use transactions and can commit changes during transfer progress. --- .../Backend/BackendManager.PutOperation.cs | 26 ++++++++++++++-- .../Library/Main/Backend/BackendManager.cs | 31 +++++++++++++++---- Duplicati/Library/Main/IBackendManager.cs | 12 ++++++- .../Operation/Backup/DataBlockProcessor.cs | 2 +- .../Backup/RecreateMissingIndexFiles.cs | 2 +- .../Operation/Backup/SpillCollectorProcess.cs | 2 +- .../Operation/Backup/UploadRealFilelist.cs | 2 +- .../Backup/UploadSyntheticFilelist.cs | 3 +- .../Main/Operation/Common/DatabaseCommon.cs | 10 ++++++ .../Library/Main/Operation/CompactHandler.cs | 16 +++++++--- .../Main/Operation/PurgeFilesHandler.cs | 6 +++- .../Library/Main/Operation/RepairHandler.cs | 12 ++++--- 12 files changed, 100 insertions(+), 24 deletions(-) diff --git a/Duplicati/Library/Main/Backend/BackendManager.PutOperation.cs b/Duplicati/Library/Main/Backend/BackendManager.PutOperation.cs index a63d5f07a..7b8c3a240 100644 --- a/Duplicati/Library/Main/Backend/BackendManager.PutOperation.cs +++ b/Duplicati/Library/Main/Backend/BackendManager.PutOperation.cs @@ -75,6 +75,10 @@ partial class BackendManager /// public required bool TrackedInDb { get; init; } /// + /// A callback that is invoked when the database needs to be updated + /// + public required Func OnDbUpdate { get; init; } + /// /// The encryption and hashing task, the return value indicates if the hash and size was updated /// private Task<(string Hash, long Size)>? encryptionAndHashingTask; @@ -94,6 +98,10 @@ partial class BackendManager /// A callback that is invoked when the index volume is finished /// public required Action? IndexVolumeFinishedCallback { get; init; } + /// + /// The default callback for database updates (no-op) + /// + public static Func OnDbUpdateDefault = () => Task.CompletedTask; /// /// Creates a new put operation @@ -201,12 +209,16 @@ partial class BackendManager // On first upload attempt, calculate the hash and size if (operationState == OperationState.None && TrackedInDb) + { Context.Database.LogRemoteVolumeUpdated(RemoteFilename, RemoteVolumeState.Uploading, size, hash); + await OnDbUpdate().ConfigureAwait(false); + } // First attempt here, finish the index file now that all information is known if (OriginalIndexFile != null && indexOperation == null) { Context.Database.LogRemoteVolumeUpdated(OriginalIndexFile.RemoteFilename, RemoteVolumeState.Uploading, -1, null); + await OnDbUpdate().ConfigureAwait(false); // Prepare an upload operation for the index file var req2 = new PutOperation(OriginalIndexFile.RemoteFilename, Context, false, cancelToken) @@ -215,9 +227,11 @@ partial class BackendManager TrackedInDb = TrackedInDb, Unencrypted = Unencrypted, IndexVolumeFinishedCallback = null, - OriginalIndexFile = null + OriginalIndexFile = null, + OnDbUpdate = OnDbUpdate }; + OriginalIndexFile.FinishVolume(hash, size); IndexVolumeFinishedCallback?.Invoke(); OriginalIndexFile.Close(); @@ -226,8 +240,11 @@ partial class BackendManager } // If we have previously attempted to upload, we need to rename the file - if (operationState == OperationState.Uploading) + if (operationState == OperationState.Uploading && TrackedInDb) + { RenameFileAfterError(size); + await OnDbUpdate().ConfigureAwait(false); + } // Flag for next attempt, if any operationState = OperationState.Uploading; @@ -263,6 +280,8 @@ partial class BackendManager { Context.Database.LogRemoteOperation("put", RemoteFilename, JsonConvert.SerializeObject(new { Size = Size, Hash = Hash })); Context.Statwriter.SendEvent(BackendActionType.Put, BackendEventType.Started, RemoteFilename, Size); + if (TrackedInDb) + await OnDbUpdate().ConfigureAwait(false); var begin = DateTime.Now; @@ -280,7 +299,10 @@ partial class BackendManager Logging.Log.WriteProfilingMessage(LOGTAG, "UploadSpeed", "Uploaded {0} in {1}, {2}/s", Library.Utility.Utility.FormatSizeString(Size), duration, Library.Utility.Utility.FormatSizeString((long)(Size / duration.TotalSeconds))); if (TrackedInDb) + { Context.Database.LogRemoteVolumeUpdated(RemoteFilename, RemoteVolumeState.Uploaded, Size, Hash); + await OnDbUpdate().ConfigureAwait(false); + } Context.Statwriter.SendEvent(BackendActionType.Put, BackendEventType.Completed, RemoteFilename, Size); diff --git a/Duplicati/Library/Main/Backend/BackendManager.cs b/Duplicati/Library/Main/Backend/BackendManager.cs index d6647e48a..0f9c40b15 100644 --- a/Duplicati/Library/Main/Backend/BackendManager.cs +++ b/Duplicati/Library/Main/Backend/BackendManager.cs @@ -249,9 +249,10 @@ internal partial class BackendManager : IBackendManager /// The index volume to upload, if any /// The callback to call when the index volume is finished /// True if the operation should wait for the file to actually be uploaded. If this argument is false, the task will complete once the operation is queued + /// The callback to call when the database is updated /// The cancellation token /// An awaitable task - public async Task PutAsync(VolumeWriterBase volume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, CancellationToken cancelToken) + public async Task PutAsync(VolumeWriterBase volume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, Func? onDbUpdate, CancellationToken cancelToken) { volume.Close(); @@ -261,7 +262,8 @@ internal partial class BackendManager : IBackendManager OriginalIndexFile = indexVolume, Unencrypted = false, TrackedInDb = true, - IndexVolumeFinishedCallback = indexVolumeFinished + IndexVolumeFinishedCallback = indexVolumeFinished, + OnDbUpdate = onDbUpdate ?? PutOperation.OnDbUpdateDefault, }; // Prepare encryption @@ -285,7 +287,8 @@ internal partial class BackendManager : IBackendManager Unencrypted = true, // Avoid encrypting TrackedInDb = false, // Not tracked OriginalIndexFile = null, - IndexVolumeFinishedCallback = null + IndexVolumeFinishedCallback = null, + OnDbUpdate = PutOperation.OnDbUpdateDefault, }; // Sets the task as already completed @@ -294,20 +297,35 @@ internal partial class BackendManager : IBackendManager await op.GetResult().ConfigureAwait(false); } + /// + /// Flushes the database messages to the database + /// + /// The database to write to + /// The transaction to use + /// The cancellation token + /// + public Task FlushPendingMessagesAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken) + { + context.Database.FlushPendingMessages(database, transaction); + return Task.CompletedTask; + } + /// /// Waits for the backend queue to be empty and flushes the database messages /// + /// The database to write to + /// The transaction to use /// The cancellation token /// An awaitable task public async Task WaitForEmptyAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken) { - context.Database.FlushPendingMessages(database, transaction); + await FlushPendingMessagesAsync(database, transaction, cancellationToken).ConfigureAwait(false); var op = new WaitForEmptyOperation(context, cancellationToken); await QueueTask(op).ConfigureAwait(false); await op.GetResult().ConfigureAwait(false); - context.Database.FlushPendingMessages(database, transaction); + await FlushPendingMessagesAsync(database, transaction, cancellationToken).ConfigureAwait(false); } /// @@ -318,7 +336,8 @@ internal partial class BackendManager : IBackendManager public async Task StopRunnerAndFlushMessages(LocalDatabase database, IDbTransaction? transaction) { await requestChannel.RetireAsync().ConfigureAwait(false); - context.Database.FlushPendingMessages(database, transaction); + await FlushPendingMessagesAsync(database, transaction, CancellationToken.None).ConfigureAwait(false); + if (queueRunner.IsFaulted) Logging.Log.WriteWarningMessage(LOGTAG, "BackendManagerShutdown", queueRunner.Exception, "Backend manager queue runner crashed"); } diff --git a/Duplicati/Library/Main/IBackendManager.cs b/Duplicati/Library/Main/IBackendManager.cs index c9cd0372c..13e589d35 100644 --- a/Duplicati/Library/Main/IBackendManager.cs +++ b/Duplicati/Library/Main/IBackendManager.cs @@ -25,9 +25,10 @@ internal interface IBackendManager : IDisposable /// The index volume to upload, if any /// The action to call when the index volume is finished /// Whether to wait for the upload to complete + /// The action to call when the database should be updated /// The cancellation token /// An awaitable task - Task PutAsync(VolumeWriterBase blockVolume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, CancellationToken cancelToken); + Task PutAsync(VolumeWriterBase blockVolume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, Func? onDbUpdate, CancellationToken cancelToken); /// /// Uploads a file to the backend without encryption @@ -128,4 +129,13 @@ internal interface IBackendManager : IDisposable /// long LastWriteSize { get; } + /// + /// Flushes the database messages to the database + /// + /// The database to write to + /// The transaction to use + /// The cancellation token + /// + Task FlushPendingMessagesAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken); + } diff --git a/Duplicati/Library/Main/Operation/Backup/DataBlockProcessor.cs b/Duplicati/Library/Main/Operation/Backup/DataBlockProcessor.cs index 670f3a809..b6ebd49a5 100644 --- a/Duplicati/Library/Main/Operation/Backup/DataBlockProcessor.cs +++ b/Duplicati/Library/Main/Operation/Backup/DataBlockProcessor.cs @@ -143,7 +143,7 @@ namespace Duplicati.Library.Main.Operation.Backup blockvolume = null; indexvolume = null; - await backendManager.PutAsync(blockVolumeCopy, indexVolumeCopy, null, false, taskreader.ProgressToken); + await backendManager.PutAsync(blockVolumeCopy, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken); } diff --git a/Duplicati/Library/Main/Operation/Backup/RecreateMissingIndexFiles.cs b/Duplicati/Library/Main/Operation/Backup/RecreateMissingIndexFiles.cs index 46e80a80a..88be0f140 100644 --- a/Duplicati/Library/Main/Operation/Backup/RecreateMissingIndexFiles.cs +++ b/Duplicati/Library/Main/Operation/Backup/RecreateMissingIndexFiles.cs @@ -47,7 +47,7 @@ internal static class RecreateMissingIndexFiles return; await database.UpdateRemoteVolumeAsync(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null).ConfigureAwait(false); - await backendManager.PutAsync(w, null, null, false, taskreader.ProgressToken).ConfigureAwait(false); + await backendManager.PutAsync(w, null, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false); } } } diff --git a/Duplicati/Library/Main/Operation/Backup/SpillCollectorProcess.cs b/Duplicati/Library/Main/Operation/Backup/SpillCollectorProcess.cs index c391f7b21..06b6326cf 100644 --- a/Duplicati/Library/Main/Operation/Backup/SpillCollectorProcess.cs +++ b/Duplicati/Library/Main/Operation/Backup/SpillCollectorProcess.cs @@ -172,7 +172,7 @@ namespace Duplicati.Library.Main.Operation.Backup await database.AddIndexBlockLinkAsync(indexVolumeCopy.VolumeID, target.BlockVolume.VolumeID).ConfigureAwait(false); } - await backendManager.PutAsync(target.BlockVolume, indexVolumeCopy, null, false, taskreader.ProgressToken).ConfigureAwait(false); + await backendManager.PutAsync(target.BlockVolume, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false); } } } diff --git a/Duplicati/Library/Main/Operation/Backup/UploadRealFilelist.cs b/Duplicati/Library/Main/Operation/Backup/UploadRealFilelist.cs index 3d8431f62..aa8a85d9c 100644 --- a/Duplicati/Library/Main/Operation/Backup/UploadRealFilelist.cs +++ b/Duplicati/Library/Main/Operation/Backup/UploadRealFilelist.cs @@ -67,7 +67,7 @@ internal static class UploadRealFilelist await db.UpdateRemoteVolumeAsync(filesetvolume.RemoteFilename, RemoteVolumeState.Uploading, -1, null).ConfigureAwait(false); await db.CommitTransactionAsync("CommitUpdateRemoteVolume").ConfigureAwait(false); - await backendManager.PutAsync(filesetvolume, null, null, false, taskreader.ProgressToken).ConfigureAwait(false); + await backendManager.PutAsync(filesetvolume, null, null, false, () => db.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false); } } else diff --git a/Duplicati/Library/Main/Operation/Backup/UploadSyntheticFilelist.cs b/Duplicati/Library/Main/Operation/Backup/UploadSyntheticFilelist.cs index 7efa1b13e..76b80f852 100644 --- a/Duplicati/Library/Main/Operation/Backup/UploadSyntheticFilelist.cs +++ b/Duplicati/Library/Main/Operation/Backup/UploadSyntheticFilelist.cs @@ -115,8 +115,7 @@ namespace Duplicati.Library.Main.Operation.Backup if (!string.IsNullOrWhiteSpace(lastTempFilelist.Name) && (lastTempFilelist.State == RemoteVolumeState.Uploading || lastTempFilelist.State == RemoteVolumeState.Temporary)) await database.UpdateRemoteVolumeAsync(lastTempFilelist.Name, RemoteVolumeState.Deleting, -1, null); await database.CommitTransactionAsync("CommitUpdateFilelistVolume"); - - await backendManager.PutAsync(fsw, null, null, false, taskreader.ProgressToken); + await backendManager.PutAsync(fsw, null, null, true, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken); } catch { diff --git a/Duplicati/Library/Main/Operation/Common/DatabaseCommon.cs b/Duplicati/Library/Main/Operation/Common/DatabaseCommon.cs index d7acefa5a..7a3a20a9a 100644 --- a/Duplicati/Library/Main/Operation/Common/DatabaseCommon.cs +++ b/Duplicati/Library/Main/Operation/Common/DatabaseCommon.cs @@ -21,6 +21,7 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Duplicati.Library.Main.Database; @@ -66,6 +67,15 @@ namespace Duplicati.Library.Main.Operation.Common return RunOnMain(() => m_db.UpdateRemoteVolume(name, state, size, hash, suppressCleanup, deleteGraceTime, GetTransaction())); } + public async Task FlushBackendMessagesAndCommitAsync(IBackendManager backendManager) + { + await FlushPendingMessagesAsync(backendManager).ConfigureAwait(false); + await CommitTransactionAsync("FlushBackendMessagesAndCommitAsync").ConfigureAwait(false); + } + + public Task FlushPendingMessagesAsync(IBackendManager backendManager) + => RunOnMain(() => backendManager.FlushPendingMessagesAsync(m_db, GetTransaction(), CancellationToken.None).ConfigureAwait(false)); + public Task CommitTransactionAsync(string message, bool restart = true) { return RunOnMain(() => diff --git a/Duplicati/Library/Main/Operation/CompactHandler.cs b/Duplicati/Library/Main/Operation/CompactHandler.cs index 6a05d3de5..f975c3eb4 100644 --- a/Duplicati/Library/Main/Operation/CompactHandler.cs +++ b/Duplicati/Library/Main/Operation/CompactHandler.cs @@ -183,7 +183,7 @@ namespace Duplicati.Library.Main.Operation if (newvol.Filesize > (m_options.VolumeSize - m_options.Blocksize)) { - await FinishVolumeAndUpload(db, backendManager, newvol, newvolindex, uploadedVolumes); + await FinishVolumeAndUpload(db, backendManager, newvol, newvolindex, uploadedVolumes, rtr); newvol = new BlockVolumeWriter(m_options); newvol.VolumeID = db.RegisterRemoteVolume(newvol.RemoteFilename, RemoteVolumeType.Blocks, RemoteVolumeState.Temporary, rtr.Transaction); @@ -215,7 +215,7 @@ namespace Duplicati.Library.Main.Operation if (blocksInVolume > 0) { - await FinishVolumeAndUpload(db, backendManager, newvol, newvolindex, uploadedVolumes).ConfigureAwait(false); + await FinishVolumeAndUpload(db, backendManager, newvol, newvolindex, uploadedVolumes, rtr).ConfigureAwait(false); } else { @@ -323,7 +323,7 @@ namespace Duplicati.Library.Main.Operation yield return d; } - private async Task FinishVolumeAndUpload(LocalDeleteDatabase db, IBackendManager backendManager, BlockVolumeWriter newvol, IndexVolumeWriter newvolindex, List> uploadedVolumes) + private async Task FinishVolumeAndUpload(LocalDeleteDatabase db, IBackendManager backendManager, BlockVolumeWriter newvol, IndexVolumeWriter newvolindex, List> uploadedVolumes, ReusableTransaction rtr) { Action indexVolumeFinished = () => { @@ -339,8 +339,16 @@ namespace Duplicati.Library.Main.Operation uploadedVolumes.Add(new KeyValuePair(newvol.RemoteFilename, newvol.Filesize)); if (newvolindex != null) uploadedVolumes.Add(new KeyValuePair(newvolindex.RemoteFilename, newvolindex.Filesize)); + + // TODO: The upload here does not flush the database messages, + // and this can leave the database in a state where it does not know of the remote file + // To fix it, we need thread-safe access to the daabase and transaction + // Once fixed, we can perhaps let he backend manager simply call the database directly if (!m_options.Dryrun) - await backendManager.PutAsync(newvol, newvolindex, indexVolumeFinished, false, m_result.TaskControl.ProgressToken).ConfigureAwait(false); + { + rtr.Commit("CommitUpload"); + await backendManager.PutAsync(newvol, newvolindex, indexVolumeFinished, false, null, m_result.TaskControl.ProgressToken).ConfigureAwait(false); + } else Logging.Log.WriteDryrunMessage(LOGTAG, "WouldUploadGeneratedBlockset", "Would upload generated blockset of size {0}", Library.Utility.Utility.FormatSizeString(newvol.Filesize)); } diff --git a/Duplicati/Library/Main/Operation/PurgeFilesHandler.cs b/Duplicati/Library/Main/Operation/PurgeFilesHandler.cs index 0748482b8..cc873661e 100644 --- a/Duplicati/Library/Main/Operation/PurgeFilesHandler.cs +++ b/Duplicati/Library/Main/Operation/PurgeFilesHandler.cs @@ -213,7 +213,11 @@ namespace Duplicati.Library.Main.Operation db.UpdateRemoteVolume(f.Key, RemoteVolumeState.Deleting, f.Value, null, tr); tr.Commit(); - await backendManager.PutAsync(vol, null, null, true, m_result.TaskControl.ProgressToken).ConfigureAwait(false); + + await backendManager.PutAsync(vol, null, null, true, async () => + { + await backendManager.FlushPendingMessagesAsync(db, null, m_result.TaskControl.ProgressToken).ConfigureAwait(false); + }, m_result.TaskControl.ProgressToken).ConfigureAwait(false); await backendManager.DeleteAsync(prevfilename, -1, true, m_result.TaskControl.ProgressToken).ConfigureAwait(false); await backendManager.WaitForEmptyAsync(db, null, m_result.TaskControl.ProgressToken).ConfigureAwait(false); } diff --git a/Duplicati/Library/Main/Operation/RepairHandler.cs b/Duplicati/Library/Main/Operation/RepairHandler.cs index d28cbd44e..3a967e007 100644 --- a/Duplicati/Library/Main/Operation/RepairHandler.cs +++ b/Duplicati/Library/Main/Operation/RepairHandler.cs @@ -350,7 +350,8 @@ namespace Duplicati.Library.Main.Operation fsw.VolumeID = db.RegisterRemoteVolume(fsw.RemoteFilename, RemoteVolumeType.Files, -1, RemoteVolumeState.Temporary); db.LinkFilesetToVolume(filesetId, fsw.VolumeID, null); - await backendManager.PutAsync(fsw, null, null, false, cancellationToken).ConfigureAwait(false); + // TODO: Rewrite to use transactions and flush db messages as needed + await backendManager.PutAsync(fsw, null, null, false, null, cancellationToken).ConfigureAwait(false); } if (anyDlistUploads) @@ -415,7 +416,8 @@ namespace Duplicati.Library.Main.Operation else { db.UpdateRemoteVolume(volumeWriter.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null); - await backendManager.PutAsync(volumeWriter, null, null, false, cancellationToken).ConfigureAwait(false); + // TODO: Rewrite to use transactions and flush db messages as needed + await backendManager.PutAsync(volumeWriter, null, null, false, null, cancellationToken).ConfigureAwait(false); } } else if (n.Type == RemoteVolumeType.Index) @@ -456,7 +458,8 @@ namespace Duplicati.Library.Main.Operation else { db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null); - await backendManager.PutAsync(w, null, null, false, cancellationToken).ConfigureAwait(false); + // TODO: Rewrite to use transactions and flush db messages as needed + await backendManager.PutAsync(w, null, null, false, null, cancellationToken).ConfigureAwait(false); } } else if (n.Type == RemoteVolumeType.Blocks) @@ -552,7 +555,8 @@ namespace Duplicati.Library.Main.Operation else { db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null); - await backendManager.PutAsync(w, null, null, false, cancellationToken).ConfigureAwait(false); + // TODO: Rewrite to use transactions and flush db messages as needed + await backendManager.PutAsync(w, null, null, false, null, cancellationToken).ConfigureAwait(false); } } } From e8f85961d0666719c66bcab9532afed8dd5fa1bf Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 12:11:33 +0200 Subject: [PATCH 06/12] Include more changes before comitting the transaction --- Duplicati/Library/Main/Operation/Backup/DataBlockProcessor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Duplicati/Library/Main/Operation/Backup/DataBlockProcessor.cs b/Duplicati/Library/Main/Operation/Backup/DataBlockProcessor.cs index b6ebd49a5..01e4f123a 100644 --- a/Duplicati/Library/Main/Operation/Backup/DataBlockProcessor.cs +++ b/Duplicati/Library/Main/Operation/Backup/DataBlockProcessor.cs @@ -127,8 +127,6 @@ namespace Duplicati.Library.Main.Operation.Backup blockvolume.Close(); - await database.CommitTransactionAsync("CommitAddBlockToOutputFlush"); - IndexVolumeWriter indexVolumeCopy = null; if (indexvolume != null) { @@ -143,6 +141,7 @@ namespace Duplicati.Library.Main.Operation.Backup blockvolume = null; indexvolume = null; + await database.CommitTransactionAsync("CommitAddBlockToOutputFlush"); await backendManager.PutAsync(blockVolumeCopy, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken); } From d19e7f443032a637cab2ebda97cf45ed93dd81cb Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 12:12:38 +0200 Subject: [PATCH 07/12] More safeguards against getting an unknown remote file --- .../Library/Main/Operation/Backup/RecreateMissingIndexFiles.cs | 1 + Duplicati/Library/Main/Operation/Backup/SpillCollectorProcess.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Duplicati/Library/Main/Operation/Backup/RecreateMissingIndexFiles.cs b/Duplicati/Library/Main/Operation/Backup/RecreateMissingIndexFiles.cs index 88be0f140..608ff7b7e 100644 --- a/Duplicati/Library/Main/Operation/Backup/RecreateMissingIndexFiles.cs +++ b/Duplicati/Library/Main/Operation/Backup/RecreateMissingIndexFiles.cs @@ -47,6 +47,7 @@ internal static class RecreateMissingIndexFiles return; await database.UpdateRemoteVolumeAsync(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null).ConfigureAwait(false); + await database.CommitTransactionAsync("RecreateMissingIndexFile").ConfigureAwait(false); await backendManager.PutAsync(w, null, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false); } } diff --git a/Duplicati/Library/Main/Operation/Backup/SpillCollectorProcess.cs b/Duplicati/Library/Main/Operation/Backup/SpillCollectorProcess.cs index 06b6326cf..46d7fb02c 100644 --- a/Duplicati/Library/Main/Operation/Backup/SpillCollectorProcess.cs +++ b/Duplicati/Library/Main/Operation/Backup/SpillCollectorProcess.cs @@ -172,6 +172,7 @@ namespace Duplicati.Library.Main.Operation.Backup await database.AddIndexBlockLinkAsync(indexVolumeCopy.VolumeID, target.BlockVolume.VolumeID).ConfigureAwait(false); } + await database.CommitTransactionAsync("UploadSpillVolume").ConfigureAwait(false); await backendManager.PutAsync(target.BlockVolume, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false); } } From 66862043b662c387b152e9c66f122c16c5d88507 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 13:44:46 +0200 Subject: [PATCH 08/12] Fixed a case where the transactions were not used, despite being active --- .../Library/Main/Backend/BackendManager.cs | 18 +++++++++++++----- Duplicati/Library/Main/IBackendManager.cs | 7 +++++++ .../Library/Main/Operation/BackupHandler.cs | 10 ++++++---- .../Main/Operation/Common/DatabaseCommon.cs | 4 ++-- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Duplicati/Library/Main/Backend/BackendManager.cs b/Duplicati/Library/Main/Backend/BackendManager.cs index 0f9c40b15..20ded2a5f 100644 --- a/Duplicati/Library/Main/Backend/BackendManager.cs +++ b/Duplicati/Library/Main/Backend/BackendManager.cs @@ -310,6 +310,18 @@ internal partial class BackendManager : IBackendManager return Task.CompletedTask; } + /// + /// Waits for the backend queue to be empty + /// + /// The cancellation token + /// An awaitable task + public async Task WaitForEmptyAsync(CancellationToken cancellationToken) + { + var op = new WaitForEmptyOperation(context, cancellationToken); + await QueueTask(op).ConfigureAwait(false); + await op.GetResult().ConfigureAwait(false); + } + /// /// Waits for the backend queue to be empty and flushes the database messages /// @@ -320,11 +332,7 @@ internal partial class BackendManager : IBackendManager public async Task WaitForEmptyAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken) { await FlushPendingMessagesAsync(database, transaction, cancellationToken).ConfigureAwait(false); - - var op = new WaitForEmptyOperation(context, cancellationToken); - await QueueTask(op).ConfigureAwait(false); - await op.GetResult().ConfigureAwait(false); - + await WaitForEmptyAsync(cancellationToken).ConfigureAwait(false); await FlushPendingMessagesAsync(database, transaction, cancellationToken).ConfigureAwait(false); } diff --git a/Duplicati/Library/Main/IBackendManager.cs b/Duplicati/Library/Main/IBackendManager.cs index 13e589d35..99c4104ba 100644 --- a/Duplicati/Library/Main/IBackendManager.cs +++ b/Duplicati/Library/Main/IBackendManager.cs @@ -39,6 +39,13 @@ internal interface IBackendManager : IDisposable /// An awaitable task Task PutVerificationFileAsync(string remotename, TempFile tempFile, CancellationToken cancelToken); + /// + /// Waits for the backend queue to be empty + /// + /// The cancellation token + /// An awaitable task + Task WaitForEmptyAsync(CancellationToken cancellationToken); + /// /// Waits for the backend queue to be empty and flushes any pending messages to the database /// diff --git a/Duplicati/Library/Main/Operation/BackupHandler.cs b/Duplicati/Library/Main/Operation/BackupHandler.cs index 636472463..322e0f1a2 100644 --- a/Duplicati/Library/Main/Operation/BackupHandler.cs +++ b/Duplicati/Library/Main/Operation/BackupHandler.cs @@ -595,14 +595,16 @@ namespace Duplicati.Library.Main.Operation return new AggregateException(ex.First().Message, ex); } - private static async Task FlushBackend(LocalDatabase database, IDbTransaction transaction, BackupResults result, IBackendManager backendManager) + private static async Task FlushBackend(Backup.BackupDatabase database, BackupResults result, IBackendManager backendManager) { // Wait for upload completion result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_WaitForUpload); try { - await backendManager.WaitForEmptyAsync(database, transaction, result.TaskControl.ProgressToken).ConfigureAwait(false); + await database.FlushBackendMessagesAndCommitAsync(backendManager).ConfigureAwait(false); + await backendManager.WaitForEmptyAsync(result.TaskControl.ProgressToken).ConfigureAwait(false); + await database.FlushBackendMessagesAndCommitAsync(backendManager).ConfigureAwait(false); // Grab the size of the last uploaded volume return backendManager.LastWriteSize; } @@ -710,14 +712,14 @@ namespace Duplicati.Library.Main.Operation using (new Logging.Timer(LOGTAG, "VerifyConsistency", "VerifyConsistency")) await db.VerifyConsistencyAsync(m_options.Blocksize, m_options.BlockhashSize, false); - await FlushBackend(m_database, null, m_result, backendManager).ConfigureAwait(false); + await FlushBackend(db, m_result, backendManager).ConfigureAwait(false); // Send the actual filelist await Backup.UploadRealFilelist.Run(m_result, db, backendManager, m_options, filesetvolume, filesetid, m_result.TaskControl, lastTempVolumeIncomplete); // Wait for upload completion m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_WaitForUpload); - var lastVolumeSize = await FlushBackend(m_database, null, m_result, backendManager).ConfigureAwait(false); + var lastVolumeSize = await FlushBackend(db, m_result, backendManager).ConfigureAwait(false); if (!m_options.Dryrun) database.TerminatedWithActiveUploads = false; diff --git a/Duplicati/Library/Main/Operation/Common/DatabaseCommon.cs b/Duplicati/Library/Main/Operation/Common/DatabaseCommon.cs index 7a3a20a9a..0d9d8d8e3 100644 --- a/Duplicati/Library/Main/Operation/Common/DatabaseCommon.cs +++ b/Duplicati/Library/Main/Operation/Common/DatabaseCommon.cs @@ -69,11 +69,11 @@ namespace Duplicati.Library.Main.Operation.Common public async Task FlushBackendMessagesAndCommitAsync(IBackendManager backendManager) { - await FlushPendingMessagesAsync(backendManager).ConfigureAwait(false); + await FlushPendingBackendMessagesAsync(backendManager).ConfigureAwait(false); await CommitTransactionAsync("FlushBackendMessagesAndCommitAsync").ConfigureAwait(false); } - public Task FlushPendingMessagesAsync(IBackendManager backendManager) + private Task FlushPendingBackendMessagesAsync(IBackendManager backendManager) => RunOnMain(() => backendManager.FlushPendingMessagesAsync(m_db, GetTransaction(), CancellationToken.None).ConfigureAwait(false)); public Task CommitTransactionAsync(string message, bool restart = true) From 62ce6d00b80635d1b345571c7fdcbcdd344637df Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 14:54:48 +0200 Subject: [PATCH 09/12] Rename LICENSE.txt to LICENSE --- LICENSE.txt => LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE.txt => LICENSE (100%) diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE From 337a094db112102db0e7abda53df201245fbece7 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 15:02:01 +0200 Subject: [PATCH 10/12] Updated README This removes a bit of cruft from the files and updates a few incorrect links --- README.ja-JP.md | 19 +++---------------- README.md | 17 ++--------------- README.zh-CN.md | 27 ++++++--------------------- 3 files changed, 11 insertions(+), 52 deletions(-) diff --git a/README.ja-JP.md b/README.ja-JP.md index 1d24d280c..424936b57 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -4,20 +4,9 @@ 暗号化したバックアップを、クラウドストレージサービスで安全に保管しましょう! - - - - [![Open Collectiveでのサポーター](https://opencollective.com/duplicati/backers/badge.svg)](#backers) [![Open Collectiveでのスポンサー](https://opencollective.com/duplicati/sponsors/badge.svg)](#sponsors) [![Travis-CIでのビルドの状況](https://travis-ci.org/duplicati/duplicati.svg?branch=master)](https://travis-ci.org/duplicati/duplicati) [![カバレッジの状況](https://coveralls.io/repos/github/duplicati/duplicati/badge.svg?branch=HEAD)](https://coveralls.io/github/duplicati/duplicati?branch=HEAD) -[![ライセンス](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/duplicati/duplicati/blob/master/LICENSE.txt) +[![ライセンス](https://img.shields.io/github/license/duplicati/duplicati.svg)](https://github.com/duplicati/duplicati/blob/master/LICENSE) Duplicati は、フリー(自由)でオープンソースのバックアップ用クライアントです。圧縮し、暗号化した増分バックアップを、クラウドストレージサービスや遠隔のファイルサーバー上に安全に保存できます。Duplicati は、主に以下のサービスやソフトウェアで使うことができます。 @@ -27,9 +16,7 @@ Duplicati は MIT ライセンスで公開されており、Windows、OSX、Linu # ダウンロード -Duplicati 2.0 のベータ版が Duplicati の最新バージョンとなります。 - -[ここをクリックすると、Duplicati 2.0 のベータ版をダウンロードできます。](https://duplicati.com/download) +[ここをクリックすると、Duplicati のベータ版をダウンロードできます。](https://duplicati.com/download) ベータ版では、アップデートがある場合に自動的に通知を行い、1 クリック(またはターミナルでのコマンド入力)でアップグレードできます。 より新しい[テスト版に関しては、最新のリリースを確認](https://github.com/duplicati/duplicati/releases)するか、ソフトウェア上の画面またはコマンドラインで、別のアップデートチャンネルを選択してください。 @@ -40,7 +27,7 @@ Duplicati 2.0 のベータ版が Duplicati の最新バージョンとなりま Duplicati は、活発なコミュニティーによってサポートが行われています。コミュニティーには[フォーラム](https://forum.duplicati.com)からご参加ください。 -[Duplicati のマニュアル](https://docs.duplicati.com)もあります。マニュアルの作成や維持にぜひ[ご参加](https://github.com/kees-z/DuplicatiDocs)ください。 +[Duplicati のマニュアル](https://docs.duplicati.com)もあります。マニュアルの作成や維持にぜひ[ご参加](https://github.com/duplicati/documentation)ください。 # 機能 diff --git a/README.md b/README.md index 2e3cdbf56..55d6284d9 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,6 @@ Store securely encrypted backups on cloud storage services! - - - - [![Backers on Open Collective](https://opencollective.com/duplicati/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/duplicati/sponsors/badge.svg)](#sponsors) [![Build Status on Travis-CI](https://travis-ci.org/duplicati/duplicati.svg?branch=master)](https://travis-ci.org/duplicati/duplicati) [![Coverage Status](https://coveralls.io/repos/github/duplicati/duplicati/badge.svg?branch=HEAD)](https://coveralls.io/github/duplicati/duplicati?branch=HEAD) [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/duplicati/duplicati/blob/master/LICENSE.txt) @@ -24,13 +13,11 @@ Duplicati is a free, open-source backup client that securely stores encrypted, i    _Amazon S3, [IDrive e2](https://www.idrive.com/e2/duplicati "Using Duplicati with IDrive e2"), [Backblaze (B2)](https://www.backblaze.com/blog/duplicati-backups-cloud-storage/ "Duplicati with Backblaze B2 Cloud Storage"), Box, Dropbox, FTP, Google Cloud and Drive, MEGA, Microsoft Azure and OneDrive, Rackspace Cloud Files, OpenStack Storage (Swift), Sia, Storj DCS, SSH (SFTP), WebDAV, Tencent Cloud Object Storage (COS), Aliyun OSS, [and more!](https://docs.duplicati.com/backup-destinations/destination-overview)_ -Duplicati is licensed under the MIT license and is available for Windows, macOS, and Linux (.NET 4.7.1+ or Mono 5.10.0+ required). +Duplicati is licensed under the MIT license and is available for Windows, macOS, and Linux. # Download -The latest version of Duplicati is a beta version for the Duplicati 2.0 release. - -[Click here to download the latest Duplicati 2.0 beta release.](https://duplicati.com/download) +[Click here to download the latest Duplicati release.](https://duplicati.com/download) The beta release will automatically notify you of updates and allows you to upgrade with a single click (or command in the terminal). For even more [bleeding edge access, check the latest releases](https://github.com/duplicati/duplicati/releases) or choose another update channel in the UI or on the commandline. diff --git a/README.zh-CN.md b/README.zh-CN.md index d7aa179c0..ad9f8afb0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,38 +2,23 @@ [English](./README.md) | **中文** | [日本語](./README.ja-JP.md) -[中文官网](https://duplicati.cn) +[中文官网](https://duplicati.com) 在云存储服务上安全地存储加密备份! - - - - [![Open Collective 上的支持者](https://opencollective.com/duplicati/backers/badge.svg)](#backers) [![Open Collective 上的赞助商](https://opencollective.com/duplicati/sponsors/badge.svg)](#sponsors) [![Travis-CI 上的构建状态](https://travis-ci.org/duplicati/duplicati.svg?branch=master)](https://travis-ci.org/duplicati/duplicati) -[![AppVeyor 上的构建状态](https://ci.appveyor.com/api/projects/status/h8s5nau9mn311hq0/branch/master?svg=true)](https://ci.appveyor.com/project/kenkendk/duplicati/branch/master) -[![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=4870652)](https://www.bountysource.com/teams/duplicati?tracker_ids=4870652&utm_medium=shield&utm_campaign=TRACKER_BADGE) [![覆盖率状态](https://coveralls.io/repos/github/duplicati/duplicati/badge.svg?branch=HEAD)](https://coveralls.io/github/duplicati/duplicati?branch=HEAD) -[![许可](https://img.shields.io/github/license/duplicati/duplicati.svg)](https://github.com/duplicati/duplicati/blob/master/LICENSE.txt) +[![许可](https://img.shields.io/github/license/duplicati/duplicati.svg)](https://github.com/duplicati/duplicati/blob/master/LICENSE) Duplicati 是一个免费、开源的备份客户端,可以安全地将加密、增量、压缩的备份存储在云存储服务和远程文件服务器上。它与以下服务兼容: -   _亚马逊 S3、[IDrive e2](https://www.idrive.com/e2/duplicati "使用 Duplicati 与 IDrive e2")、[Backblaze (B2)](https://www.backblaze.com/blog/duplicati-backups-cloud-storage/ "Duplicati 与 Backblaze B2 云存储")、Box、Dropbox、FTP、Google Cloud 和 Drive、MEGA、Microsoft Azure 和 OneDrive、Rackspace Cloud Files、OpenStack Storage (Swift)、Sia、Storj DCS、SSH (SFTP)、WebDAV、阿里云对象存储(OSS)、百度网盘、阿里云盘、腾讯云对象存储 (COS)、[以及更多!](https://docs.duplicati.com/backup-destinations/destination-overview)_ +   _亚马逊 S3、[IDrive e2](https://www.idrive.com/e2/duplicati "使用 Duplicati 与 IDrive e2")、[Backblaze (B2)](https://www.backblaze.com/blog/duplicati-backups-cloud-storage/ "Duplicati 与 Backblaze B2 云存储")、Box、Dropbox、FTP、Google Cloud 和 Drive、MEGA、Microsoft Azure 和 OneDrive、Rackspace Cloud Files、OpenStack Storage (Swift)、Sia、Storj DCS、SSH (SFTP)、WebDAV、阿里云对象存储(OSS)、腾讯云对象存储 (COS)、[以及更多!](https://docs.duplicati.com/backup-destinations/destination-overview)_ -Duplicati 根据 LGPL 许可证授权,并可用于 Windows、OSX 和 Linux (.NET 4.7.1+ 或 Mono 5.10.0+ 需要)。 +Duplicati 根据 MIT 许可证授权,并可用于 Windows、macOS 和 Linux。 # 下载 -Duplicati 的最新版本是 Duplicati 2.0 发布的测试版。 - -[点击此处下载最新的 Duplicati 2.0 测试版。](https://duplicati.com/download) +[点击此处下载最新的 Duplicati 测试版。](https://duplicati.com/download) 测试版将自动通知您更新,并允许您通过单击(或在终端中的命令)升级。 要获取更多[前沿版本,查看最新发布](https://github.com/duplicati/duplicati/releases)或在 UI 或命令行中选择另一个更新渠道。 @@ -44,7 +29,7 @@ Duplicati 的最新版本是 Duplicati 2.0 发布的测试版。 Duplicati 由一个[活跃的社区支持,您可以通过我们的论坛与他们联系](https://forum.duplicati.com)。 -我们有一个很棒的 [Duplicati 手册](https://docs.duplicati.com),您也可以[为其做出贡献](https://github.com/kees-z/DuplicatiDocs)。 +我们有一个很棒的 [Duplicati 手册](https://docs.duplicati.com),您也可以[为其做出贡献](https://github.com/duplicati/documentation)。 # 功能 From eea290dbac3d8c398554d1c8a405db85ee066381 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 15:05:04 +0200 Subject: [PATCH 11/12] Updated license to use SPDX header --- BuildTools/LicenseUpdater/Fragments.cs | 37 ++++++++++--------- .../LicenseUpdater/LicenseUpdater.csproj | 2 +- LICENSE | 37 ++++++++++--------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/BuildTools/LicenseUpdater/Fragments.cs b/BuildTools/LicenseUpdater/Fragments.cs index a0e334314..aea78ac95 100755 --- a/BuildTools/LicenseUpdater/Fragments.cs +++ b/BuildTools/LicenseUpdater/Fragments.cs @@ -25,26 +25,29 @@ namespace LicenseUpdater; public static class Fragments { - public static string NEW_LICENSE = @"Copyright (C) YYYY, The Duplicati Team -https://duplicati.com, hello@duplicati.com + public static string NEW_LICENSE = @"SPDX-License-Identifier: MIT -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: +MIT License -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +Copyright (c) YYYY Duplicati contributors -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.".Replace("YYYY", DateTime.UtcNow.Year.ToString()); +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.".Replace("YYYY", DateTime.UtcNow.Year.ToString()); public static RegexOptions RE_OPTS = RegexOptions.Singleline | RegexOptions.IgnoreCase; diff --git a/BuildTools/LicenseUpdater/LicenseUpdater.csproj b/BuildTools/LicenseUpdater/LicenseUpdater.csproj index 0b3177572..0de69fdc0 100755 --- a/BuildTools/LicenseUpdater/LicenseUpdater.csproj +++ b/BuildTools/LicenseUpdater/LicenseUpdater.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 license_upgrader enable enable diff --git a/LICENSE b/LICENSE index 52fe4579c..9b249d383 100644 --- a/LICENSE +++ b/LICENSE @@ -1,20 +1,23 @@ -Copyright (C) 2025, The Duplicati Team -https://duplicati.com, hello@duplicati.com +SPDX-License-Identifier: MIT -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: +MIT License -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +Copyright (c) 2025 Duplicati contributors -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. +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. From e011ebe75471b3d465322d611bbae5dadcb91f43 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 23 Apr 2025 15:12:56 +0200 Subject: [PATCH 12/12] Removed SPDX header again --- BuildTools/LicenseUpdater/Fragments.cs | 4 +--- LICENSE | 2 -- README.md | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/BuildTools/LicenseUpdater/Fragments.cs b/BuildTools/LicenseUpdater/Fragments.cs index aea78ac95..92af02cb2 100755 --- a/BuildTools/LicenseUpdater/Fragments.cs +++ b/BuildTools/LicenseUpdater/Fragments.cs @@ -25,9 +25,7 @@ namespace LicenseUpdater; public static class Fragments { - public static string NEW_LICENSE = @"SPDX-License-Identifier: MIT - -MIT License + public static string NEW_LICENSE = @"MIT License Copyright (c) YYYY Duplicati contributors diff --git a/LICENSE b/LICENSE index 9b249d383..a76b68663 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,3 @@ -SPDX-License-Identifier: MIT - MIT License Copyright (c) 2025 Duplicati contributors diff --git a/README.md b/README.md index 55d6284d9..c2ab0bcf9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Store securely encrypted backups on cloud storage services! [![Backers on Open Collective](https://opencollective.com/duplicati/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/duplicati/sponsors/badge.svg)](#sponsors) [![Build Status on Travis-CI](https://travis-ci.org/duplicati/duplicati.svg?branch=master)](https://travis-ci.org/duplicati/duplicati) [![Coverage Status](https://coveralls.io/repos/github/duplicati/duplicati/badge.svg?branch=HEAD)](https://coveralls.io/github/duplicati/duplicati?branch=HEAD) -[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/duplicati/duplicati/blob/master/LICENSE.txt) +[![License](https://img.shields.io/github/license/duplicati/duplicati.svg)](https://github.com/duplicati/duplicati/blob/master/LICENSE) [![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Duplicati%20Guru-006BFF)](https://gurubase.io/g/duplicati) Duplicati is a free, open-source backup client that securely stores encrypted, incremental, and compressed backups on cloud storage services and remote file servers. It supports: