// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #nullable enable using System; using System.IO; using System.Threading.Tasks; using Duplicati.Library.Interface; using Duplicati.Library.Logging; using Duplicati.Library.Main.Database; namespace Duplicati.Library.Main.Operation { /// /// Handler for reading lock information from the backend and updating the database. /// internal class ReadLockInfoHandler { private static readonly string LOGTAG = Log.LogTagFromType(); private readonly Options m_options; private readonly ReadLockInfoResults m_result; /// /// Creates a new instance of the class. /// /// The options for the operation. /// The backend writer for logging. /// The cancellation token. public ReadLockInfoHandler(Options options, ReadLockInfoResults result) { m_options = options; m_result = result; } /// /// Runs the read lock info operation, updating the database with lock expiration times from the backend. /// /// The backend manager to use for reading lock info. /// A task that completes when the operation is finished. public async Task RunAsync(IBackendManager backendManager) { await RunAsync(backendManager, null).ConfigureAwait(false); } /// /// Runs the read lock info operation, updating the database with lock expiration times from the backend. /// /// The backend manager to use for reading lock info. /// An optional database override to use instead of creating a new one. /// A task that completes when the operation is finished. internal async Task RunAsync(IBackendManager backendManager, LocalLockDatabase? databaseOverride) { if (!backendManager.SupportsObjectLocking) throw new UserInformationException("Backend does not support object locking", "BackendDoesNotSupportLocking"); if (!File.Exists(m_options.Dbpath)) throw new UserInformationException(string.Format("Database file does not exist: {0}", m_options.Dbpath), "DatabaseFileDoesNotExist"); m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.ReadLockInfo_Running); var ownsDatabase = databaseOverride is null; await using var db = ownsDatabase ? await LocalLockDatabase.CreateAsync(m_options.Dbpath, null, m_result.TaskControl.ProgressToken).ConfigureAwait(false) : null; var database = databaseOverride ?? db!; long readCount = 0; var updatedCount = 0; var errorCount = 0; await foreach (var (name, lockExpiration) in database.GetRemoteVolumesWithLockExpiration(!m_options.RefreshLockInfoComplete, m_result.TaskControl.ProgressToken).ConfigureAwait(false)) { readCount++; try { var remoteLockExpiration = (await backendManager.GetObjectLockUntilAsync(name, m_result.TaskControl.ProgressToken).ConfigureAwait(false)) ?? DateTime.UnixEpoch; // Only update if the value has changed if (remoteLockExpiration != lockExpiration) { await database.UpdateRemoteVolumeLockExpiration(name, remoteLockExpiration, m_result.TaskControl.ProgressToken).ConfigureAwait(false); updatedCount++; Log.WriteVerboseMessage(LOGTAG, "UpdatedLockExpiration", "Updated lock expiration for {0} to {1}", name, remoteLockExpiration); } } catch (Exception ex) { errorCount++; Log.WriteWarningMessage(LOGTAG, "GetObjectLockFailed", ex, "Failed to get object lock for {0}: {1}", name, ex.Message); } } await backendManager.WaitForEmptyAsync(m_result.TaskControl.ProgressToken).ConfigureAwait(false); await backendManager.FlushPendingMessagesAsync(database, m_result.TaskControl.ProgressToken); await database.Transaction.CommitAsync(m_result.TaskControl.ProgressToken).ConfigureAwait(false); m_result.VolumesRead = readCount; m_result.VolumesUpdated = updatedCount; if (updatedCount > 0 || errorCount > 0) Log.WriteInformationMessage(LOGTAG, "ReadLockInfoComplete", "Read lock info complete: {0} updated, {1} errors", updatedCount, errorCount); } } }