Merge branch 'master' into feature/add-sqlite-cache-value
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -4,20 +4,9 @@
|
||||
|
||||
暗号化したバックアップを、クラウドストレージサービスで安全に保管しましょう!
|
||||
|
||||
<!---
|
||||
以下は現在機能していません…
|
||||
[](http://www.issuestats.com/github/duplicati/duplicati/)
|
||||
[](http://www.issuestats.com/github/duplicati/duplicati/)
|
||||
-->
|
||||
|
||||
<!--
|
||||
Gitterは削除済
|
||||
[](https://gitter.im/duplicati/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
-->
|
||||
|
||||
[](#backers) [](#sponsors) [](https://travis-ci.org/duplicati/duplicati)
|
||||
[](https://coveralls.io/github/duplicati/duplicati?branch=HEAD)
|
||||
[](https://github.com/duplicati/duplicati/blob/master/LICENSE.txt)
|
||||
[](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)ください。
|
||||
|
||||
# 機能
|
||||
|
||||
|
||||
@@ -4,33 +4,20 @@
|
||||
|
||||
Store securely encrypted backups on cloud storage services!
|
||||
|
||||
<!---
|
||||
These are currently not functional ...
|
||||
[](http://www.issuestats.com/github/duplicati/duplicati/)
|
||||
[](http://www.issuestats.com/github/duplicati/duplicati/)
|
||||
-->
|
||||
|
||||
<!--
|
||||
Removed Gitter
|
||||
[](https://gitter.im/duplicati/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
-->
|
||||
|
||||
[](#backers) [](#sponsors) [](https://travis-ci.org/duplicati/duplicati)
|
||||
[](https://coveralls.io/github/duplicati/duplicati?branch=HEAD)
|
||||
[](https://github.com/duplicati/duplicati/blob/master/LICENSE.txt)
|
||||
[](https://github.com/duplicati/duplicati/blob/master/LICENSE)
|
||||
[](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:
|
||||
|
||||
_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
@@ -2,38 +2,23 @@
|
||||
|
||||
[English](./README.md) | **中文** | [日本語](./README.ja-JP.md)
|
||||
|
||||
[中文官网](https://duplicati.cn)
|
||||
[中文官网](https://duplicati.com)
|
||||
|
||||
在云存储服务上安全地存储加密备份!
|
||||
|
||||
<!---
|
||||
以下功能目前不可用...
|
||||
[](http://www.issuestats.com/github/duplicati/duplicati/)
|
||||
[](http://www.issuestats.com/github/duplicati/duplicati/)
|
||||
-->
|
||||
|
||||
<!--
|
||||
已移除 Gitter
|
||||
[](https://gitter.im/duplicati/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
-->
|
||||
|
||||
[](#backers) [](#sponsors) [](https://travis-ci.org/duplicati/duplicati)
|
||||
[](https://ci.appveyor.com/project/kenkendk/duplicati/branch/master)
|
||||
[](https://www.bountysource.com/teams/duplicati?tracker_ids=4870652&utm_medium=shield&utm_campaign=TRACKER_BADGE)
|
||||
[](https://coveralls.io/github/duplicati/duplicati?branch=HEAD)
|
||||
[](https://github.com/duplicati/duplicati/blob/master/LICENSE.txt)
|
||||
[](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)。
|
||||
|
||||
# 功能
|
||||
|
||||
|
||||
@@ -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}"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user