Merge branch 'master' into feature/add-sqlite-cache-value

This commit is contained in:
Kenneth Skovhede
2025-04-23 15:25:00 +02:00
committed by GitHub
22 changed files with 224 additions and 133 deletions
+18 -17
View File
@@ -25,26 +25,27 @@ 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 = @"MIT License
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:
Copyright (c) YYYY Duplicati contributors
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of 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 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());
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;
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>license_upgrader</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -75,6 +75,10 @@ partial class BackendManager
/// </summary>
public required bool TrackedInDb { get; init; }
/// <summary>
/// A callback that is invoked when the database needs to be updated
/// </summary>
public required Func<Task> OnDbUpdate { get; init; }
/// <summary>
/// The encryption and hashing task, the return value indicates if the hash and size was updated
/// </summary>
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
/// </summary>
public required Action? IndexVolumeFinishedCallback { get; init; }
/// <summary>
/// The default callback for database updates (no-op)
/// </summary>
public static Func<Task> OnDbUpdateDefault = () => Task.CompletedTask;
/// <summary>
/// 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);
@@ -249,9 +249,10 @@ internal partial class BackendManager : IBackendManager
/// <param name="indexVolume">The index volume to upload, if any</param>
/// <param name="indexVolumeFinished">The callback to call when the index volume is finished</param>
/// <param name="waitForComplete">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</param>
/// <param name="onDbUpdate">The callback to call when the database is updated</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
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<Task>? 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,43 @@ internal partial class BackendManager : IBackendManager
await op.GetResult().ConfigureAwait(false);
}
/// <summary>
/// Flushes the database messages to the database
/// </summary>
/// <param name="database">The database to write to</param>
/// <param name="transaction">The transaction to use</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns></returns>
public Task FlushPendingMessagesAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken)
{
context.Database.FlushPendingMessages(database, transaction);
return Task.CompletedTask;
}
/// <summary>
/// Waits for the backend queue to be empty
/// </summary>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task WaitForEmptyAsync(CancellationToken cancellationToken)
{
var op = new WaitForEmptyOperation(context, cancellationToken);
await QueueTask(op).ConfigureAwait(false);
await op.GetResult().ConfigureAwait(false);
}
/// <summary>
/// Waits for the backend queue to be empty and flushes the database messages
/// </summary>
/// <param name="database">The database to write to</param>
/// <param name="transaction">The transaction to use</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task WaitForEmptyAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken)
{
context.Database.FlushPendingMessages(database, transaction);
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);
await WaitForEmptyAsync(cancellationToken).ConfigureAwait(false);
await FlushPendingMessagesAsync(database, transaction, cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -318,7 +344,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");
}
+18 -1
View File
@@ -25,9 +25,10 @@ internal interface IBackendManager : IDisposable
/// <param name="indexVolume">The index volume to upload, if any</param>
/// <param name="indexVolumeFinished">The action to call when the index volume is finished</param>
/// <param name="waitForComplete">Whether to wait for the upload to complete</param>
/// <param name="onDbUpdate">The action to call when the database should be updated</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
Task PutAsync(VolumeWriterBase blockVolume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, CancellationToken cancelToken);
Task PutAsync(VolumeWriterBase blockVolume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, Func<Task>? onDbUpdate, CancellationToken cancelToken);
/// <summary>
/// Uploads a file to the backend without encryption
@@ -38,6 +39,13 @@ internal interface IBackendManager : IDisposable
/// <returns>An awaitable task</returns>
Task PutVerificationFileAsync(string remotename, TempFile tempFile, CancellationToken cancelToken);
/// <summary>
/// Waits for the backend queue to be empty
/// </summary>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
Task WaitForEmptyAsync(CancellationToken cancellationToken);
/// <summary>
/// Waits for the backend queue to be empty and flushes any pending messages to the database
/// </summary>
@@ -128,4 +136,13 @@ internal interface IBackendManager : IDisposable
/// </summary>
long LastWriteSize { get; }
/// <summary>
/// Flushes the database messages to the database
/// </summary>
/// <param name="database">The database to write to</param>
/// <param name="transaction">The transaction to use</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns></returns>
Task FlushPendingMessagesAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken);
}
@@ -127,8 +127,6 @@ namespace Duplicati.Library.Main.Operation.Backup
blockvolume.Close();
await database.CommitTransactionAsync("CommitAddBlockToOutputFlush");
IndexVolumeWriter indexVolumeCopy = null;
if (indexvolume != null)
{
@@ -143,7 +141,8 @@ namespace Duplicati.Library.Main.Operation.Backup
blockvolume = null;
indexvolume = null;
await backendManager.PutAsync(blockVolumeCopy, indexVolumeCopy, null, false, taskreader.ProgressToken);
await database.CommitTransactionAsync("CommitAddBlockToOutputFlush");
await backendManager.PutAsync(blockVolumeCopy, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken);
}
@@ -47,7 +47,8 @@ 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 database.CommitTransactionAsync("RecreateMissingIndexFile").ConfigureAwait(false);
await backendManager.PutAsync(w, null, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false);
}
}
}
@@ -172,7 +172,8 @@ 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 database.CommitTransactionAsync("UploadSpillVolume").ConfigureAwait(false);
await backendManager.PutAsync(target.BlockVolume, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false);
}
}
}
@@ -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
@@ -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
{
@@ -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
{
@@ -594,14 +595,16 @@ namespace Duplicati.Library.Main.Operation
return new AggregateException(ex.First().Message, ex);
}
private static async Task<long> FlushBackend(LocalDatabase database, IDbTransaction transaction, BackupResults result, IBackendManager backendManager)
private static async Task<long> 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;
}
@@ -709,12 +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(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;
@@ -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 FlushPendingBackendMessagesAsync(backendManager).ConfigureAwait(false);
await CommitTransactionAsync("FlushBackendMessagesAndCommitAsync").ConfigureAwait(false);
}
private Task FlushPendingBackendMessagesAsync(IBackendManager backendManager)
=> RunOnMain(() => backendManager.FlushPendingMessagesAsync(m_db, GetTransaction(), CancellationToken.None).ConfigureAwait(false));
public Task CommitTransactionAsync(string message, bool restart = true)
{
return RunOnMain(() =>
@@ -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<KeyValuePair<string, long>> uploadedVolumes)
private async Task FinishVolumeAndUpload(LocalDeleteDatabase db, IBackendManager backendManager, BlockVolumeWriter newvol, IndexVolumeWriter newvolindex, List<KeyValuePair<string, long>> uploadedVolumes, ReusableTransaction rtr)
{
Action indexVolumeFinished = () =>
{
@@ -339,8 +339,16 @@ namespace Duplicati.Library.Main.Operation
uploadedVolumes.Add(new KeyValuePair<string, long>(newvol.RemoteFilename, newvol.Filesize));
if (newvolindex != null)
uploadedVolumes.Add(new KeyValuePair<string, long>(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));
}
@@ -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);
}
@@ -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);
}
}
}
+39 -6
View File
@@ -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)
{
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Duplicati contributors
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.
-20
View File
@@ -1,20 +0,0 @@
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.
+3 -16
View File
@@ -4,20 +4,9 @@
暗号化したバックアップを、クラウドストレージサービスで安全に保管しましょう!
<!---
以下は現在機能していません…
[![Issue Stats](http://www.issuestats.com/github/duplicati/duplicati/badge/pr)](http://www.issuestats.com/github/duplicati/duplicati/)
[![Issue Stats](http://www.issuestats.com/github/duplicati/duplicati/badge/issue)](http://www.issuestats.com/github/duplicati/duplicati/)
-->
<!--
Gitterは削除済
[![Join the chat at https://gitter.im/duplicati/Lobby](https://badges.gitter.im/duplicati/Lobby.svg)](https://gitter.im/duplicati/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-->
[![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)ください。
# 機能
+3 -16
View File
@@ -4,33 +4,20 @@
Store securely encrypted backups on cloud storage services!
<!---
These are currently not functional ...
[![Issue Stats](http://www.issuestats.com/github/duplicati/duplicati/badge/pr)](http://www.issuestats.com/github/duplicati/duplicati/)
[![Issue Stats](http://www.issuestats.com/github/duplicati/duplicati/badge/issue)](http://www.issuestats.com/github/duplicati/duplicati/)
-->
<!--
Removed Gitter
[![Join the chat at https://gitter.im/duplicati/Lobby](https://badges.gitter.im/duplicati/Lobby.svg)](https://gitter.im/duplicati/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-->
[![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:
&nbsp;&nbsp; _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.
+6 -21
View File
@@ -2,38 +2,23 @@
[English](./README.md) | **中文** | [日本語](./README.ja-JP.md)
[中文官网](https://duplicati.cn)
[中文官网](https://duplicati.com)
在云存储服务上安全地存储加密备份!
<!---
以下功能目前不可用...
[![问题统计](http://www.issuestats.com/github/duplicati/duplicati/badge/pr)](http://www.issuestats.com/github/duplicati/duplicati/)
[![问题统计](http://www.issuestats.com/github/duplicati/duplicati/badge/issue)](http://www.issuestats.com/github/duplicati/duplicati/)
-->
<!--
已移除 Gitter
[![在 https://gitter.im/duplicati/Lobby 聊天](https://badges.gitter.im/duplicati/Lobby.svg)](https://gitter.im/duplicati/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-->
[![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 是一个免费、开源的备份客户端,可以安全地将加密、增量、压缩的备份存储在云存储服务和远程文件服务器上。它与以下服务兼容:
&nbsp;&nbsp; _亚马逊 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)_
&nbsp;&nbsp; _亚马逊 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)。
# 功能
+1 -1
View File
@@ -99,7 +99,7 @@ public static partial class Command
/// The supported versions of libicu for Debian
/// </summary>
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}"));