Merge remote-tracking branch 'upstream/master' into test/recovery_tool

This commit is contained in:
Kenneth Hsu
2020-01-21 20:30:02 -08:00
10 changed files with 195 additions and 121 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ jobs:
directories:
- $BUILD_DIR
script:
- ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler
- ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler
- stage: tests
cache:
@@ -850,7 +850,7 @@ namespace Duplicati.Library.AutoUpdater
{
string optstr = Environment.GetEnvironmentVariable(string.Format(UPDATE_STRATEGY_ENVNAME_TEMPLATE, APPNAME));
AutoUpdateStrategy strategy;
if (string.IsNullOrWhiteSpace(optstr) || !Enum.TryParse(optstr, out strategy))
if (string.IsNullOrWhiteSpace(optstr) || !Enum.TryParse(optstr, true, out strategy))
strategy = defaultstrategy;
System.Threading.Thread backgroundChecker = null;
+98 -102
View File
@@ -309,138 +309,134 @@ namespace Duplicati.Library.Main.Operation
}
else if (n.Type == RemoteVolumeType.Index)
{
using (IndexVolumeWriter w = new IndexVolumeWriter(m_options))
IndexVolumeWriter w = new IndexVolumeWriter(m_options);
newEntry = w;
w.SetRemoteFilename(n.Name);
var h = Library.Utility.HashAlgorithmHelper.Create(m_options.BlockHashAlgorithm);
foreach (var blockvolume in db.GetBlockVolumesFromIndexName(n.Name))
{
newEntry = w;
w.SetRemoteFilename(n.Name);
w.StartVolume(blockvolume.Name);
var volumeid = db.GetRemoteVolumeID(blockvolume.Name);
var h = Library.Utility.HashAlgorithmHelper.Create(m_options.BlockHashAlgorithm);
foreach (var b in db.GetBlocks(volumeid))
w.AddBlock(b.Hash, b.Size);
foreach (var blockvolume in db.GetBlockVolumesFromIndexName(n.Name))
{
w.StartVolume(blockvolume.Name);
var volumeid = db.GetRemoteVolumeID(blockvolume.Name);
w.FinishVolume(blockvolume.Hash, blockvolume.Size);
foreach (var b in db.GetBlocks(volumeid))
w.AddBlock(b.Hash, b.Size);
if (m_options.IndexfilePolicy == Options.IndexFileStrategy.Full)
foreach (var b in db.GetBlocklists(volumeid, m_options.Blocksize, hashsize))
{
var bh = Convert.ToBase64String(h.ComputeHash(b.Item2, 0, b.Item3));
if (bh != b.Item1)
throw new Exception(string.Format("Internal consistency check failed, generated index block has wrong hash, {0} vs {1}", bh, b.Item1));
w.FinishVolume(blockvolume.Hash, blockvolume.Size);
w.WriteBlocklist(b.Item1, b.Item2, 0, b.Item3);
}
}
if (m_options.IndexfilePolicy == Options.IndexFileStrategy.Full)
foreach (var b in db.GetBlocklists(volumeid, m_options.Blocksize, hashsize))
{
var bh = Convert.ToBase64String(h.ComputeHash(b.Item2, 0, b.Item3));
if (bh != b.Item1)
throw new Exception(string.Format("Internal consistency check failed, generated index block has wrong hash, {0} vs {1}", bh, b.Item1));
w.Close();
w.WriteBlocklist(b.Item1, b.Item2, 0, b.Item3);
}
}
w.Close();
if (m_options.Dryrun)
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldReUploadIndexFile", "would re-upload index file {0}, with size {1}, previous size {2}", n.Name, Library.Utility.Utility.FormatSizeString(new System.IO.FileInfo(w.LocalFilename).Length), Library.Utility.Utility.FormatSizeString(n.Size));
else
{
db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
backend.Put(w);
}
if (m_options.Dryrun)
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldReUploadIndexFile", "would re-upload index file {0}, with size {1}, previous size {2}", n.Name, Library.Utility.Utility.FormatSizeString(new System.IO.FileInfo(w.LocalFilename).Length), Library.Utility.Utility.FormatSizeString(n.Size));
else
{
db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
backend.Put(w);
}
}
else if (n.Type == RemoteVolumeType.Blocks)
{
using (BlockVolumeWriter w = new BlockVolumeWriter(m_options))
BlockVolumeWriter w = new BlockVolumeWriter(m_options);
newEntry = w;
w.SetRemoteFilename(n.Name);
using (var mbl = db.CreateBlockList(n.Name))
{
newEntry = w;
w.SetRemoteFilename(n.Name);
using (var mbl = db.CreateBlockList(n.Name))
//First we grab all known blocks from local files
foreach (var block in mbl.GetSourceFilesWithBlocks(m_options.Blocksize))
{
//First we grab all known blocks from local files
foreach (var block in mbl.GetSourceFilesWithBlocks(m_options.Blocksize))
var hash = block.Hash;
var size = (int) block.Size;
foreach (var source in block.Sources)
{
var hash = block.Hash;
var size = (int) block.Size;
var file = source.File;
var offset = source.Offset;
foreach (var source in block.Sources)
{
var file = source.File;
var offset = source.Offset;
try
{
if (System.IO.File.Exists(file))
using (var f = System.IO.File.OpenRead(file))
{
f.Position = offset;
if (size == Library.Utility.Utility.ForceStreamRead(f, buffer, size))
{
var newhash = Convert.ToBase64String(blockhasher.ComputeHash(buffer, 0, size));
if (newhash == hash)
{
if (mbl.SetBlockRestored(hash, size))
w.AddBlock(hash, buffer, 0, size, Duplicati.Library.Interface.CompressionHint.Default);
break;
}
}
}
}
catch (Exception ex)
{
Logging.Log.WriteErrorMessage(LOGTAG, "FileAccessError", ex, "Failed to access file: {0}", file);
}
}
}
//Then we grab all remote volumes that have the missing blocks
foreach (var vol in new AsyncDownloader(mbl.GetMissingBlockSources().ToList(), backend))
{
try
{
using (var tmpfile = vol.TempFile)
using (var f = new BlockVolumeReader(RestoreHandler.GetCompressionModule(vol.Name), tmpfile, m_options))
foreach (var b in f.Blocks)
if (mbl.SetBlockRestored(b.Key, b.Value))
if (f.ReadBlock(b.Key, buffer) == b.Value)
w.AddBlock(b.Key, buffer, 0, (int) b.Value, Duplicati.Library.Interface.CompressionHint.Default);
if (System.IO.File.Exists(file))
using (var f = System.IO.File.OpenRead(file))
{
f.Position = offset;
if (size == Library.Utility.Utility.ForceStreamRead(f, buffer, size))
{
var newhash = Convert.ToBase64String(blockhasher.ComputeHash(buffer, 0, size));
if (newhash == hash)
{
if (mbl.SetBlockRestored(hash, size))
w.AddBlock(hash, buffer, 0, size, Duplicati.Library.Interface.CompressionHint.Default);
break;
}
}
}
}
catch (Exception ex)
{
Logging.Log.WriteErrorMessage(LOGTAG, "RemoteFileAccessError", ex, "Failed to access remote file: {0}", vol.Name);
Logging.Log.WriteErrorMessage(LOGTAG, "FileAccessError", ex, "Failed to access file: {0}", file);
}
}
}
// If we managed to recover all blocks, NICE!
var missingBlocks = mbl.GetMissingBlocks().Count();
if (missingBlocks > 0)
//Then we grab all remote volumes that have the missing blocks
foreach (var vol in new AsyncDownloader(mbl.GetMissingBlockSources().ToList(), backend))
{
try
{
Logging.Log.WriteInformationMessage(LOGTAG, "RepairMissingBlocks", "Repair cannot acquire {0} required blocks for volume {1}, which are required by the following filesets: ", missingBlocks, n.Name);
foreach (var f in mbl.GetFilesetsUsingMissingBlocks())
Logging.Log.WriteInformationMessage(LOGTAG, "AffectedFilesetName", f.Name);
using (var tmpfile = vol.TempFile)
using (var f = new BlockVolumeReader(RestoreHandler.GetCompressionModule(vol.Name), tmpfile, m_options))
foreach (var b in f.Blocks)
if (mbl.SetBlockRestored(b.Key, b.Value))
if (f.ReadBlock(b.Key, buffer) == b.Value)
w.AddBlock(b.Key, buffer, 0, (int) b.Value, Duplicati.Library.Interface.CompressionHint.Default);
}
catch (Exception ex)
{
Logging.Log.WriteErrorMessage(LOGTAG, "RemoteFileAccessError", ex, "Failed to access remote file: {0}", vol.Name);
}
}
var recoverymsg = string.Format("If you want to continue working with the database, you can use the \"{0}\" and \"{1}\" commands to purge the missing data from the database and the remote storage.", "list-broken-files", "purge-broken-files");
// If we managed to recover all blocks, NICE!
var missingBlocks = mbl.GetMissingBlocks().Count();
if (missingBlocks > 0)
{
Logging.Log.WriteInformationMessage(LOGTAG, "RepairMissingBlocks", "Repair cannot acquire {0} required blocks for volume {1}, which are required by the following filesets: ", missingBlocks, n.Name);
foreach (var f in mbl.GetFilesetsUsingMissingBlocks())
Logging.Log.WriteInformationMessage(LOGTAG, "AffectedFilesetName", f.Name);
if (!m_options.Dryrun)
{
Logging.Log.WriteInformationMessage(LOGTAG, "RecoverySuggestion", "This may be fixed by deleting the filesets and running repair again");
var recoverymsg = string.Format("If you want to continue working with the database, you can use the \"{0}\" and \"{1}\" commands to purge the missing data from the database and the remote storage.", "list-broken-files", "purge-broken-files");
throw new UserInformationException(string.Format("Repair not possible, missing {0} blocks.\n" + recoverymsg, missingBlocks), "RepairIsNotPossible");
}
else
{
Logging.Log.WriteInformationMessage(LOGTAG, "RecoverySuggestion", recoverymsg);
}
if (!m_options.Dryrun)
{
Logging.Log.WriteInformationMessage(LOGTAG, "RecoverySuggestion", "This may be fixed by deleting the filesets and running repair again");
throw new UserInformationException(string.Format("Repair not possible, missing {0} blocks.\n" + recoverymsg, missingBlocks), "RepairIsNotPossible");
}
else
{
if (m_options.Dryrun)
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldReUploadBlockFile", "would re-upload block file {0}, with size {1}, previous size {2}", n.Name, Library.Utility.Utility.FormatSizeString(new System.IO.FileInfo(w.LocalFilename).Length), Library.Utility.Utility.FormatSizeString(n.Size));
else
{
db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
backend.Put(w);
}
Logging.Log.WriteInformationMessage(LOGTAG, "RecoverySuggestion", recoverymsg);
}
}
else
{
if (m_options.Dryrun)
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldReUploadBlockFile", "would re-upload block file {0}, with size {1}, previous size {2}", n.Name, Library.Utility.Utility.FormatSizeString(new System.IO.FileInfo(w.LocalFilename).Length), Library.Utility.Utility.FormatSizeString(n.Size));
else
{
db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
backend.Put(w);
}
}
}
+1 -1
View File
@@ -173,7 +173,7 @@ namespace Duplicati.Library.UsageReporter
{
var str = Environment.GetEnvironmentVariable(string.Format(DISABLED_ENVNAME_TEMPLATE, AutoUpdater.AutoUpdateSettings.AppName));
ReportType tmp;
if (string.IsNullOrWhiteSpace(str) || !Enum.TryParse(str, out tmp))
if (string.IsNullOrWhiteSpace(str) || !Enum.TryParse(str, true, out tmp))
Cached_MaxReportLevel = ReportType.Information;
else
Cached_MaxReportLevel = tmp;
+1 -1
View File
@@ -204,4 +204,4 @@
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\NUnit.3.12.0\build\NUnit.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\NUnit.3.12.0\build\NUnit.props'))" />
</Target>
</Project>
</Project>
+48
View File
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Duplicati.Library.Main;
using NUnit.Framework;
namespace Duplicati.UnitTest
{
[TestFixture]
public class RepairHandlerTests : BasicSetupHelper
{
public override void SetUp()
{
base.SetUp();
File.WriteAllBytes(Path.Combine(this.DATAFOLDER, "file"), new byte[] {0});
}
[Test]
[Category("RepairHandler")]
[TestCase("true")]
[TestCase("false")]
public void RepairMissingIndexFiles(string noEncryption)
{
Dictionary<string, string> options = new Dictionary<string, string>(this.TestOptions) {["no-encryption"] = noEncryption};
using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null))
{
c.Backup(new[] {this.DATAFOLDER});
}
string[] dindexFiles = Directory.EnumerateFiles(this.TARGETFOLDER, "*dindex*").ToArray();
Assert.Greater(dindexFiles.Length, 0);
foreach (string f in dindexFiles)
{
File.Delete(f);
}
using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null))
{
c.Repair();
}
foreach (string file in dindexFiles)
{
Assert.IsTrue(File.Exists(Path.Combine(this.TARGETFOLDER, file)));
}
}
}
}
+6
View File
@@ -9,6 +9,12 @@ RUN dnf -y install deltarpm
RUN dnf -y upgrade
RUN dnf -y --allowerasing install @"Minimal Install" @buildsys-build yum-utils rpm-sign gnupg rpmdevtools
# Install real mono for building
# Instructions from here: https://www.mono-project.com/download/stable/#download-lin-fedora
RUN rpm --import "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF"
RUN curl https://download.mono-project.com/repo/centos8-stable.repo | tee /etc/yum.repos.d/mono-centos8-stable.repo
RUN dnf -y update
# Install mono things
RUN dnf -y install mono-devel gnome-sharp-devel dos2unix git nuget
+15 -3
View File
@@ -26,6 +26,9 @@ MONO=/Library/Frameworks/Mono.framework/Commands/mono
GPG=/usr/local/bin/gpg2
# Newer GPG needs this to allow input from a non-terminal
export GPG_TTY=$(tty)
ZIPFILE=$(basename "$1")
VERSION=$(echo "${ZIPFILE}" | cut -d "-" -f 2 | cut -d "_" -f 1)
BUILDTYPE=$(echo "${ZIPFILE}" | cut -d "-" -f 2 | cut -d "_" -f 2)
@@ -52,9 +55,18 @@ echo "DEBName: ${DEBNAME}"
echo "SPKName: ${SPKNAME}"
build_file_signatures() {
if [ "z${GPGID}" != "z" ]; then
echo "$GPGKEY" | "${GPG}" "--passphrase-fd" "0" "--batch" "--yes" "--default-key=${GPGID}" "--output" "$2.sig" "--detach-sig" "$1"
echo "$GPGKEY" | "${GPG}" "--passphrase-fd" "0" "--batch" "--yes" "--default-key=${GPGID}" "--armor" "--output" "$2.sig.asc" "--detach-sig" "$1"
if [ -f "${GPG_KEYFILE}" ]; then
"${MONO}" "BuildTools/GnupgSigningTool/bin/Debug/GnupgSigningTool.exe" \
--inputfile=\"$1\" \
--signaturefile=\"$2.sig\" \
--armor=false --gpgkeyfile="${GPG_KEYFILE}" --gpgpath="${GPG}" \
--keyfile-password="${KEYFILE_PASSWORD}"
"${MONO}" "BuildTools/GnupgSigningTool/bin/Debug/GnupgSigningTool.exe" \
--inputfile=\"$1\" \
--signaturefile=\"$2.sig.asc\" \
--armor=true --gpgkeyfile="${GPG_KEYFILE}" --gpgpath="${GPG}" \
--keyfile-password="${KEYFILE_PASSWORD}"
fi
md5 "$1" | awk -F ' ' '{print $NF}' > "$2.md5"
+23 -11
View File
@@ -33,6 +33,8 @@ XBUILD=/Library/Frameworks/Mono.framework/Commands/msbuild
NUGET=/Library/Frameworks/Mono.framework/Commands/nuget
MONO=/Library/Frameworks/Mono.framework/Commands/mono
GPG=/usr/local/bin/gpg2
AWS=/usr/local/bin/aws
GITHUB_RELEASE=/usr/local/bin/github-release
# Newer GPG needs this to allow input from a non-terminal
export GPG_TTY=$(tty)
@@ -57,6 +59,18 @@ if [ ! -f "$NUGET" ]; then
exit 1
fi
if [ ! -f "$AWS" ]; then
echo "aws-cli not found: $AWS"
exit 1
fi
if [ ! -f "$GITHUB_RELEASE" ]; then
echo "github-release executable not found: $GITHUB_RELEASE"
echo "Grab it from: https://github.com/aktau/github-release"
exit 1
fi
# The "OTHER_UPLOADS" setting is no longer used
if [ "${RELEASE_TYPE}" == "nightly" ]; then
OTHER_UPLOADS=""
@@ -281,12 +295,12 @@ cp "${UPDATE_TARGET}/latest.zip.sig.asc" "${UPDATE_TARGET}/${RELEASE_FILE_NAME}.
"${MONO}" "BuildTools/UpdateVersionStamp/bin/Debug/UpdateVersionStamp.exe" --version="2.0.0.7"
echo "Uploading binaries"
aws --profile=duplicati-upload s3 cp "${UPDATE_TARGET}/${RELEASE_FILE_NAME}.zip" "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.zip"
aws --profile=duplicati-upload s3 cp "${UPDATE_TARGET}/${RELEASE_FILE_NAME}.zip.sig" "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.zip.sig"
aws --profile=duplicati-upload s3 cp "${UPDATE_TARGET}/${RELEASE_FILE_NAME}.zip.sig.asc" "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.zip.sig.asc"
aws --profile=duplicati-upload s3 cp "${UPDATE_TARGET}/${RELEASE_FILE_NAME}.manifest" "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.manifest"
"${AWS}" --profile=duplicati-upload s3 cp "${UPDATE_TARGET}/${RELEASE_FILE_NAME}.zip" "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.zip"
"${AWS}" --profile=duplicati-upload s3 cp "${UPDATE_TARGET}/${RELEASE_FILE_NAME}.zip.sig" "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.zip.sig"
"${AWS}" --profile=duplicati-upload s3 cp "${UPDATE_TARGET}/${RELEASE_FILE_NAME}.zip.sig.asc" "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.zip.sig.asc"
"${AWS}" --profile=duplicati-upload s3 cp "${UPDATE_TARGET}/${RELEASE_FILE_NAME}.manifest" "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.manifest"
aws --profile=duplicati-upload s3 cp "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.manifest" "s3://updates.duplicati.com/${RELEASE_TYPE}/latest.manifest"
"${AWS}" --profile=duplicati-upload s3 cp "s3://updates.duplicati.com/${RELEASE_TYPE}/${RELEASE_FILE_NAME}.manifest" "s3://updates.duplicati.com/${RELEASE_TYPE}/latest.manifest"
ZIP_MD5=$(md5 ${UPDATE_TARGET}/${RELEASE_FILE_NAME}.zip | awk -F ' ' '{print $NF}')
ZIP_SHA1=$(shasum -a 1 ${UPDATE_TARGET}/${RELEASE_FILE_NAME}.zip | awk -F ' ' '{print $1}')
@@ -311,8 +325,8 @@ echo "duplicati_version_info =" > "latest.js"
cat "latest.json" >> "latest.js"
echo ";" >> "latest.js"
aws --profile=duplicati-upload s3 cp "latest.json" "s3://updates.duplicati.com/${RELEASE_TYPE}/latest.json"
aws --profile=duplicati-upload s3 cp "latest.js" "s3://updates.duplicati.com/${RELEASE_TYPE}/latest.js"
"${AWS}" --profile=duplicati-upload s3 cp "latest.json" "s3://updates.duplicati.com/${RELEASE_TYPE}/latest.json"
"${AWS}" --profile=duplicati-upload s3 cp "latest.js" "s3://updates.duplicati.com/${RELEASE_TYPE}/latest.js"
# echo "Propagating to other build types"
# for OTHER in ${OTHER_UPLOADS}; do
@@ -338,14 +352,12 @@ fi
RELEASE_MESSAGE=$(printf "Changes in this version:\n${RELEASE_CHANGEINFO_NEWS}")
# Using the tool from https://github.com/aktau/github-release
GITHUB_TOKEN=$(cat "${GITHUB_TOKEN_FILE}")
if [ "x${GITHUB_TOKEN}" == "x" ]; then
echo "No GITHUB_TOKEN found in environment, you can manually upload the binaries"
else
github-release release ${PRE_RELEASE_LABEL} \
"${GITHUB_RELEASE}" release ${PRE_RELEASE_LABEL} \
--tag "v${RELEASE_VERSION}-${RELEASE_NAME}" \
--name "v${RELEASE_VERSION}-${RELEASE_NAME}" \
--repo "duplicati" \
@@ -353,7 +365,7 @@ else
--security-token "${GITHUB_TOKEN}" \
--description "${RELEASE_MESSAGE}" \
github-release upload \
"${GITHUB_RELEASE}" upload \
--tag "v${RELEASE_VERSION}-${RELEASE_NAME}" \
--name "${RELEASE_FILE_NAME}.zip" \
--repo "duplicati" \
+1 -1
View File
@@ -8,5 +8,5 @@ ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNormal
${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNoSize
${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool
${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Border
${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler
${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler