// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using Duplicati.Library.Interface; using Duplicati.Library.Main.Database; using Duplicati.Library.Main.Volumes; using Duplicati.Library.Utility; namespace Duplicati.Library.Main.Operation { internal class RecreateDatabaseHandler : IDisposable { /// /// The tag used for logging /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(); private readonly string m_backendurl; private readonly Options m_options; private readonly RecreateDatabaseResults m_result; public delegate IEnumerable> NumberedFilterFilelistDelegate(IEnumerable filelist); public delegate void BlockVolumePostProcessor(string volumename,BlockVolumeReader reader); public RecreateDatabaseHandler(string backendurl, Options options, RecreateDatabaseResults result) { m_options = options; m_backendurl = backendurl; m_result = result; } /// /// Run the recreate procedure /// /// Path to the database that will be created /// A filter that can be used to disregard certain remote files, intended to be used to select a certain filelist /// Filters the files in a filelist to prevent downloading unwanted data /// A callback hook that can be used to work with downloaded block volumes, intended to be use to recover data blocks while processing blocklists public void Run(string path, Library.Utility.IFilter filter = null, NumberedFilterFilelistDelegate filelistfilter = null, BlockVolumePostProcessor blockprocessor = null) { if (System.IO.File.Exists(path)) throw new UserInformationException(string.Format("Cannot recreate database because file already exists: {0}", path), "RecreateTargetDatabaseExists"); using(var db = new LocalDatabase(path, "Recreate", true)) { m_result.SetDatabase(db); DoRun(db, false, filter, filelistfilter, blockprocessor); db.WriteResults(); } } /// /// Updates a database with new path information from a remote fileset /// /// A filter that can be used to disregard certain remote files, intended to be used to select a certain filelist /// Filters the files in a filelist to prevent downloading unwanted data /// A callback hook that can be used to work with downloaded block volumes, intended to be use to recover data blocks while processing blocklists public void RunUpdate(Library.Utility.IFilter filter = null, NumberedFilterFilelistDelegate filelistfilter = null, BlockVolumePostProcessor blockprocessor = null) { if (!m_options.RepairOnlyPaths) throw new UserInformationException(string.Format("Can only update with paths, try setting {0}", "--repair-only-paths"), "RepairUpdateRequiresPathsOnly"); using(var db = new LocalDatabase(m_options.Dbpath, "Recreate", true)) { m_result.SetDatabase(db); if (db.FindMatchingFilesets(m_options.Time, m_options.Version).Any()) throw new UserInformationException("The version(s) being updated to, already exists", "UpdateVersionAlreadyExists"); // Mark as incomplete db.PartiallyRecreated = true; Utility.UpdateOptionsFromDb(db, m_options, null); DoRun(db, true, filter, filelistfilter, blockprocessor); db.WriteResults(); } } /// /// Run the recreate procedure /// /// The database to restore into /// True if this is an update call, false otherwise /// A filter that can be used to disregard certain remote files, intended to be used to select a certain filelist /// Filters the files in a filelist to prevent downloading unwanted data /// A callback hook that can be used to work with downloaded block volumes, intended to be use to recover data blocks while processing blocklists internal void DoRun(LocalDatabase dbparent, bool updating, Library.Utility.IFilter filter = null, NumberedFilterFilelistDelegate filelistfilter = null, BlockVolumePostProcessor blockprocessor = null) { m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Recreate_Running); //We build a local database in steps. using(var restoredb = new LocalRecreateDatabase(dbparent, m_options)) using(var backend = new BackendManager(m_backendurl, m_options, m_result.BackendWriter, restoredb)) { restoredb.RepairInProgress = true; var expRecreateDb = false; // experimental recreate db code flag var autoDetectBlockSize = !(m_options.HasBlocksize && restoredb.GetDbOptions().ContainsKey("blocksize")); var volumeIds = new Dictionary(); if (string.Equals(Environment.GetEnvironmentVariable("EXPERIMENTAL_RECREATEDB_DUPLICATI") ?? string.Empty, "1")) { expRecreateDb = true; } var rawlist = backend.List(); //First step is to examine the remote storage to see what // kind of data we can find var remotefiles = (from x in rawlist let n = VolumeBase.ParseFilename(x) where n != null && n.Prefix == m_options.Prefix select n).ToArray(); //ToArray() ensures that we do not remote-request it multiple times if (remotefiles.Length == 0) { if (rawlist.Count == 0) throw new UserInformationException("No files were found at the remote location, perhaps the target url is incorrect?", "EmptyRemoteLocation"); else { var tmp = (from x in rawlist let n = VolumeBase.ParseFilename(x) where n != null select n.Prefix).ToArray(); var types = tmp.Distinct().ToArray(); if (tmp.Length == 0) throw new UserInformationException(string.Format("Found {0} files at the remote storage, but none that could be parsed", rawlist.Count), "EmptyRemoteLocation"); else if (types.Length == 1) throw new UserInformationException(string.Format("Found {0} parse-able files with the prefix {1}, did you forget to set the backup prefix?", tmp.Length, types[0]), "EmptyRemoteLocationWithPrefix"); else throw new UserInformationException(string.Format("Found {0} parse-able files (of {1} files) with different prefixes: {2}, did you forget to set the backup prefix?", tmp.Length, rawlist.Count, string.Join(", ", types)), "EmptyRemoteLocationWithPrefix"); } } //Then we select the filelist we should work with, // and create the filelist table to fit IEnumerable filelists = from n in remotefiles where n.FileType == RemoteVolumeType.Files orderby n.Time descending select n; if (!filelists.Any()) throw new UserInformationException("No filelists found on the remote destination", "EmptyRemoteLocation"); if (filelistfilter != null) filelists = filelistfilter(filelists).Select(x => x.Value).ToArray(); if (!filelists.Any()) throw new UserInformationException("No filelists", "NoMatchingRemoteFilelists"); // If we are updating, all files should be accounted for foreach(var fl in remotefiles) volumeIds[fl.File.Name] = updating ? restoredb.GetRemoteVolumeID(fl.File.Name) : restoredb.RegisterRemoteVolume(fl.File.Name, fl.FileType, fl.File.Size, RemoteVolumeState.Uploaded); var hasUpdatedOptions = false; if (updating) { Utility.UpdateOptionsFromDb(restoredb, m_options); Utility.VerifyParameters(restoredb, m_options); } //Record all blocksets and files needed using(var tr = restoredb.BeginTransaction()) { var filelistWork = (from n in filelists orderby n.Time select new RemoteVolume(n.File) as IRemoteVolume).ToList(); Logging.Log.WriteInformationMessage(LOGTAG, "RebuildStarted", "Rebuild database started, downloading {0} filelists", filelistWork.Count); var progress = 0; // Register the files we are working with, if not already updated if (updating) { foreach(var n in filelists) if (volumeIds[n.File.Name] == -1) volumeIds[n.File.Name] = restoredb.RegisterRemoteVolume(n.File.Name, n.FileType, RemoteVolumeState.Uploaded, n.File.Size, new TimeSpan(0), tr); } var isFirstFilelist = true; var blocksize = m_options.Blocksize; var hashes_pr_block = blocksize / m_options.BlockhashSize; foreach(var entry in new AsyncDownloader(filelistWork, backend)) try { if (m_result.TaskControlRendevouz() == TaskControlState.Stop) { backend.WaitForComplete(restoredb, null); m_result.EndTime = DateTime.UtcNow; return; } progress++; if (filelistWork.Count == 1 && m_options.RepairOnlyPaths) { m_result.OperationProgressUpdater.UpdateProgress(0.5f); } else { m_result.OperationProgressUpdater.UpdateProgress(((float)progress / filelistWork.Count()) * (m_options.RepairOnlyPaths ? 1f : 0.2f)); Logging.Log.WriteVerboseMessage(LOGTAG, "ProcessingFilelistVolumes", "Processing filelist volume {0} of {1}", progress, filelistWork.Count); } using(var tmpfile = entry.TempFile) { isFirstFilelist = false; if (entry.Hash != null && entry.Size > 0) restoredb.UpdateRemoteVolume(entry.Name, RemoteVolumeState.Verified, entry.Size, entry.Hash, tr); var parsed = VolumeBase.ParseFilename(entry.Name); if (!hasUpdatedOptions && (!updating || autoDetectBlockSize)) { VolumeReaderBase.UpdateOptionsFromManifest(parsed.CompressionModule, tmpfile, m_options); hasUpdatedOptions = true; // Recompute the cached sizes blocksize = m_options.Blocksize; hashes_pr_block = blocksize / m_options.BlockhashSize; } // Create timestamped operations based on the file timestamp var filesetid = restoredb.CreateFileset(volumeIds[entry.Name], parsed.Time, tr); // retrieve fileset data from dlist var filesetData = VolumeReaderBase.GetFilesetData(parsed.CompressionModule, tmpfile, m_options); // update fileset using filesetData restoredb.UpdateFullBackupStateInFileset(filesetid, filesetData.IsFullBackup); using(var filelistreader = new FilesetVolumeReader(parsed.CompressionModule, tmpfile, m_options)) foreach(var fe in filelistreader.Files.Where(x => Library.Utility.FilterExpression.Matches(filter, x.Path))) { try { var expectedmetablocks = (fe.Metasize + blocksize - 1) / blocksize; var expectedmetablocklisthashes = (expectedmetablocks + hashes_pr_block - 1) / hashes_pr_block; if (expectedmetablocks <= 1) expectedmetablocklisthashes = 0; var metadataid = long.MinValue; var split = Database.LocalDatabase.SplitIntoPrefixAndName(fe.Path); var prefixid = restoredb.GetOrCreatePathPrefix(split.Key, tr); switch (fe.Type) { case FilelistEntryType.Folder: metadataid = restoredb.AddMetadataset(fe.Metahash, fe.Metasize, fe.MetaBlocklistHashes, expectedmetablocklisthashes, tr); restoredb.AddDirectoryEntry(filesetid, prefixid, split.Value, fe.Time, metadataid, tr); break; case FilelistEntryType.File: var expectedblocks = (fe.Size + blocksize - 1) / blocksize; var expectedblocklisthashes = (expectedblocks + hashes_pr_block - 1) / hashes_pr_block; if (expectedblocks <= 1) expectedblocklisthashes = 0; var blocksetid = restoredb.AddBlockset(fe.Hash, fe.Size, fe.BlocklistHashes, expectedblocklisthashes, tr); metadataid = restoredb.AddMetadataset(fe.Metahash, fe.Metasize, fe.MetaBlocklistHashes, expectedmetablocklisthashes, tr); restoredb.AddFileEntry(filesetid, prefixid, split.Value, fe.Time, blocksetid, metadataid, tr); if (fe.Size <= blocksize) { if (!string.IsNullOrWhiteSpace(fe.Blockhash)) restoredb.AddSmallBlocksetLink(fe.Hash, fe.Blockhash, fe.Blocksize, tr); else if (m_options.BlockHashAlgorithm == m_options.FileHashAlgorithm) restoredb.AddSmallBlocksetLink(fe.Hash, fe.Hash, fe.Size, tr); else Logging.Log.WriteWarningMessage(LOGTAG, "MissingBlockHash", null, "No block hash found for file: {0}", fe.Path); } break; case FilelistEntryType.Symlink: metadataid = restoredb.AddMetadataset(fe.Metahash, fe.Metasize, fe.MetaBlocklistHashes, expectedmetablocklisthashes, tr); restoredb.AddSymlinkEntry(filesetid, prefixid, split.Value, fe.Time, metadataid, tr); break; default: Logging.Log.WriteWarningMessage(LOGTAG, "SkippingUnknownFileEntry", null, "Skipping file-entry with unknown type {0}: {1} ", fe.Type, fe.Path); break; } if (fe.Metasize <= blocksize && (fe.Type == FilelistEntryType.Folder || fe.Type == FilelistEntryType.File || fe.Type == FilelistEntryType.Symlink)) { if (!string.IsNullOrWhiteSpace(fe.Metablockhash)) restoredb.AddSmallBlocksetLink(fe.Metahash, fe.Metablockhash, fe.Metasize, tr); else if (m_options.BlockHashAlgorithm == m_options.FileHashAlgorithm) restoredb.AddSmallBlocksetLink(fe.Metahash, fe.Metahash, fe.Metasize, tr); else Logging.Log.WriteWarningMessage(LOGTAG, "MissingMetadataBlockHash", null, "No block hash found for file metadata: {0}", fe.Path); } } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "FileEntryProcessingFailed", ex, "Failed to process file-entry: {0}", fe.Path); } } } } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "FileProcessingFailed", ex, "Failed to process file: {0}", entry.Name); if (ex is System.Threading.ThreadAbortException) { m_result.EndTime = DateTime.UtcNow; throw; } if (isFirstFilelist && ex is System.Security.Cryptography.CryptographicException) { m_result.EndTime = DateTime.UtcNow; throw; } if (m_options.UnittestMode) throw; } //Make sure we write the config if (!updating) Utility.VerifyParameters(restoredb, m_options, tr); using(new Logging.Timer(LOGTAG, "CommitUpdateFilesetFromRemote", "CommitUpdateFilesetFromRemote")) tr.Commit(); } // do we stop after just handling the dlist files ? // (if yes, we never will be able to do a backup !) if (!m_options.RepairOnlyPaths) { var hashsize = 0; //Grab all index files, and update the block table using(var hashalg = HashFactory.CreateHasher(m_options.BlockHashAlgorithm)) using(var tr = restoredb.BeginTransaction()) { hashsize = hashalg.HashSize / 8; var indexfiles = ( from n in remotefiles where n.FileType == RemoteVolumeType.Index select new RemoteVolume(n.File) as IRemoteVolume).ToList(); Logging.Log.WriteInformationMessage(LOGTAG, "FilelistsRestored", "Filelists restored, downloading {0} index files", indexfiles.Count); var progress = 0; foreach(var sf in new AsyncDownloader(indexfiles, backend)) try { if (m_result.TaskControlRendevouz() == TaskControlState.Stop) { backend.WaitForComplete(restoredb, null); m_result.EndTime = DateTime.UtcNow; return; } progress++; m_result.OperationProgressUpdater.UpdateProgress((((float)progress / indexfiles.Count) * 0.5f) + 0.2f); Logging.Log.WriteVerboseMessage(LOGTAG, "ProcessingIndexlistVolumes", "Processing indexlist volume {0} of {1}", progress, indexfiles.Count); using(var tmpfile = sf.TempFile) { if (sf.Hash != null && sf.Size > 0) restoredb.UpdateRemoteVolume(sf.Name, RemoteVolumeState.Verified, sf.Size, sf.Hash, tr); using(var svr = new IndexVolumeReader(RestoreHandler.GetCompressionModule(sf.Name), tmpfile, m_options, hashsize)) { foreach(var a in svr.Volumes) { var filename = a.Filename; var volumeID = restoredb.GetRemoteVolumeID(filename); // No such file if (volumeID < 0) volumeID = ProbeForMatchingFilename(ref filename, restoredb); var missing = false; // Still broken, register a missing item if (volumeID < 0) { var p = VolumeBase.ParseFilename(filename); if (p == null) throw new Exception(string.Format("Unable to parse filename: {0}", filename)); Logging.Log.WriteErrorMessage(LOGTAG, "MissingFileDetected", null, "Remote file referenced as {0} by {1}, but not found in list, registering a missing remote file", filename, sf.Name); missing = true; volumeID = restoredb.RegisterRemoteVolume(filename, p.FileType, RemoteVolumeState.Temporary, tr); } bool anyChange = false; //Add all block/volume mappings foreach(var b in a.Blocks) restoredb.UpdateBlock(b.Key, b.Value, volumeID, tr, ref anyChange); restoredb.UpdateRemoteVolume(filename, missing ? RemoteVolumeState.Temporary : RemoteVolumeState.Verified, a.Length, a.Hash, tr); restoredb.AddIndexBlockLink(restoredb.GetRemoteVolumeID(sf.Name), volumeID, tr); } //If there are blocklists in the index file, add them to the temp blocklist hashes table foreach(var b in svr.BlockLists) restoredb.AddTempBlockListHash(b.Hash, b.Blocklist, tr); } } } catch (Exception ex) { //Not fatal Logging.Log.WriteErrorMessage(LOGTAG, "IndexFileProcessingFailed", ex, "Failed to process index file: {0}", sf.Name); if (ex is System.Threading.ThreadAbortException) { m_result.EndTime = DateTime.UtcNow; throw; } if (m_options.UnittestMode) throw; } using(new Logging.Timer(LOGTAG, "CommitRecreateDb", "CommitRecreatedDb")) tr.Commit(); // TODO: In some cases, we can avoid downloading all index files, // if we are lucky and pick the right ones } // In some cases we have a stale reference from an index file to a deleted block file if (!m_options.UnittestMode) restoredb.CleanupMissingVolumes(); // Update the real tables from the temp tables if (expRecreateDb) // add missing blocks and blocksetentry data (at this point // we have not yet anything in the blocksetentry table) restoredb.AddBlockAndBlockSetEntryFromTemp(hashsize, m_options.Blocksize, null); else restoredb.FindMissingBlocklistHashes(hashsize, m_options.Blocksize, null); // We have now grabbed as much information as possible, // if we are still missing data, we must now fetch block files //We do this in three passes for(var i = 0; i < 3; i++) { // Grab the list matching the pass type var lst = restoredb.GetMissingBlockListVolumes(i, m_options.Blocksize, hashsize, m_options.RepairForceBlockUse).ToList(); if (lst.Count > 0) { var fullist = ": " + string.Join(", ", lst.Select(x => x.Name)); switch (i) { case 0: Logging.Log.WriteVerboseMessage(LOGTAG, "ProcessingRequiredBlocklistVolumes", "Processing required {0} blocklist volumes{1}", lst.Count, fullist); Logging.Log.WriteInformationMessage(LOGTAG, "ProcessingRequiredBlocklistVolumes", "Processing required {0} blocklist volumes{1}", lst.Count, m_options.FullResult ? fullist : string.Empty); break; case 1: Logging.Log.WriteVerboseMessage(LOGTAG, "ProbingCandicateBlocklistVolumes", "Probing {0} candidate blocklist volumes{1}", lst.Count, fullist); Logging.Log.WriteInformationMessage(LOGTAG, "ProbingCandicateBlocklistVolumes", "Probing {0} candidate blocklist volumes{1}", lst.Count, m_options.FullResult ? fullist : string.Empty); break; default: Logging.Log.WriteVerboseMessage(LOGTAG, "ProcessingAllBlocklistVolumes", "Processing all of the {0} volumes for blocklists{1}", lst.Count, fullist); Logging.Log.WriteInformationMessage(LOGTAG, "ProcessingAllBlocklistVolumes", "Processing all of the {0} volumes for blocklists{1}", lst.Count, m_options.FullResult ? fullist : string.Empty); break; } } var progress = 0; foreach (var sf in new AsyncDownloader(lst, backend)) { try { using (var tmpfile = sf.TempFile) using (var rd = new BlockVolumeReader(RestoreHandler.GetCompressionModule(sf.Name), tmpfile, m_options)) using (var tr = restoredb.BeginTransaction()) { if (m_result.TaskControlRendevouz() == TaskControlState.Stop) { backend.WaitForComplete(restoredb, null); m_result.EndTime = DateTime.UtcNow; return; } progress++; m_result.OperationProgressUpdater.UpdateProgress((((float)progress / lst.Count) * 0.1f) + 0.7f + (i * 0.1f)); Logging.Log.WriteVerboseMessage(LOGTAG, "ProcessingBlocklistVolumes", "Pass {0} of 3, processing blocklist volume {1} of {2}", (i + 1), progress, lst.Count); var volumeid = restoredb.GetRemoteVolumeID(sf.Name); restoredb.UpdateRemoteVolume(sf.Name, RemoteVolumeState.Uploaded, sf.Size, sf.Hash, tr); bool anyChange = false; // Update the block table so we know about the block/volume map foreach (var h in rd.Blocks) restoredb.UpdateBlock(h.Key, h.Value, volumeid, tr, ref anyChange); // now that we have the blocks/volume relationships, we can go from the (already known from dlist step) blocklisthashes // to the needed list blocks in the volume, so grab them from the database // read the blocks list hashes from the volume data (the handled file) and insert them into the temp blocklisthash table foreach (var blocklisthash in restoredb.GetBlockLists(volumeid)) { if (restoredb.AddTempBlockListHash(blocklisthash, rd.ReadBlocklist(blocklisthash, hashsize), tr)) { anyChange = true; } } // Update tables if necessary (if no block or hash have been changed by a data volume // there is no need to run expensive queries - most data volumes have been // managed successfully by correct index volumes), so we know if we are done // if any change, add to the block and blocksetentry tables the references found in // the block lists of the volume saved in the temp blocklisthash table by AddTempBLockListHash if (anyChange) { if (i == 2) { Logging.Log.WriteWarningMessage(LOGTAG, "UpdatingTables", null, "Unexpected changes caused by block {0}", sf.Name); } if (expRecreateDb) restoredb.AddBlockAndBlockSetEntryFromTemp(hashsize, m_options.Blocksize, tr, false); else restoredb.FindMissingBlocklistHashes(hashsize, m_options.Blocksize, tr); } using (new Logging.Timer(LOGTAG, "CommitRestoredBlocklist", "CommitRestoredBlocklist")) tr.Commit(); //At this point we can patch files with data from the block volume if (blockprocessor != null) blockprocessor(sf.Name, rd); } } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "FailedRebuildingWithFile", ex, "Failed to use information from {0} to rebuild database: {1}", sf.Name, ex.Message); if (m_options.UnittestMode) throw; } } } } backend.WaitForComplete(restoredb, null); // In some cases we have a stale reference from an index file to a deleted block file if (!m_options.UnittestMode) restoredb.CleanupMissingVolumes(); if (m_options.RepairOnlyPaths) { Logging.Log.WriteInformationMessage(LOGTAG, "RecreateOrUpdateOnly", "Recreate/path-update completed, not running consistency checks"); } else { Logging.Log.WriteInformationMessage(LOGTAG, "RecreateCompletedCheckingDatabase", "Recreate completed, verifying the database consistency"); //All done, we must verify that we have all blocklist fully intact // if this fails, the db will not be deleted, so it can be used, // except to continue a backup m_result.EndTime = DateTime.UtcNow; using (var lbfdb = new LocalListBrokenFilesDatabase(restoredb)) { var broken = lbfdb.GetBrokenFilesets(new DateTime(0), null, null).Count(); if (broken != 0) throw new UserInformationException(string.Format("Recreated database has missing blocks and {0} broken filelists. Consider using \"{1}\" and \"{2}\" to purge broken data from the remote store and the database.", broken, "list-broken-files", "purge-broken-files"), "DatabaseIsBrokenConsiderPurge"); } restoredb.VerifyConsistency(m_options.Blocksize, m_options.BlockhashSize, true, null); Logging.Log.WriteInformationMessage(LOGTAG, "RecreateCompleted", "Recreate completed, and consistency checks completed, marking database as complete"); restoredb.RepairInProgress = false; } m_result.EndTime = DateTime.UtcNow; } } /// /// Look in the database for filenames similar to the current filename, but with a different compression and encryption module /// /// The volume id of the item /// The filename read and written /// The database to query public long ProbeForMatchingFilename(ref string filename, LocalRestoreDatabase restoredb) { var p = VolumeBase.ParseFilename(filename); if (p != null) { foreach(var compmodule in Library.DynamicLoader.CompressionLoader.Keys) foreach(var encmodule in Library.DynamicLoader.EncryptionLoader.Keys.Union(new string[] { "" })) { var testfilename = VolumeBase.GenerateFilename(p.FileType, p.Prefix, p.Guid, p.Time, compmodule, encmodule); var tvid = restoredb.GetRemoteVolumeID(testfilename); if (tvid >= 0) { Logging.Log.WriteWarningMessage(LOGTAG, "RewritingFilenameMapping", null, "Unable to find volume {0}, but mapping to matching file {1}", filename, testfilename); filename = testfilename; return tvid; } } } return -1; } public void Dispose() { } } }