upstream sync

This commit is contained in:
Max
2024-09-11 21:31:04 +02:00
351 changed files with 44464 additions and 31398 deletions
@@ -1,3 +1,12 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
<!-- Thank you for taking the time to submit an issue using this template. By following the instructions and filling out the sections below, you will help the developers get the necessary information to fix your issue. You may remove sections that aren't relevant to your particular case. You can also preview your report before submitting it. -->
<!-- Please note that the issues are a tool for Duplicati developers. If you post here, you are supposed to want to help the project by providing timely information on your problem so it can be fixed. If this is not the case, please use the forum instead -->
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions & support
url: https://forum.duplicati.com
about: Please ask and answer questions here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
+4 -4
View File
@@ -49,22 +49,22 @@
"console": "internalConsole"
},
{
"name": "Launch ConfigurationImporter executable",
"name": "Launch RecoveryTool executable",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.ConfigurationImporter/bin/Debug/net8.0/Duplicati.CommandLine.ConfigurationImporter",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.RecoveryTool/bin/Debug/net8.0/Duplicati.CommandLine.RecoveryTool",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
},
{
"name": "Launch RecoveryTool executable",
"name": "Launch ServerUtil executable",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.RecoveryTool/bin/Debug/net8.0/Duplicati.CommandLine.RecoveryTool",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.ServerUtil/bin/Debug/net8.0/Duplicati.CommandLine.ServerUtil",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
@@ -1,147 +0,0 @@
// 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.Linq;
using System.Collections.Generic;
using Duplicati.Server.Serialization;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.Serializable
{
/// <summary>
/// This class collects all reportable status properties into a single class that can be exported as JSON
/// </summary>
public class ServerStatus(LiveControls liveControls) : Duplicati.Server.Serialization.Interface.IServerStatus
{
public LiveControlState ProgramState
{
get { return EnumConverter.Convert<LiveControlState>(liveControls.State); }
}
public string UpdatedVersion
{
get
{
var u = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
if (u == null)
return null;
Version v;
if (!Version.TryParse(u.Version, out v))
return null;
if (v <= System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
return null;
return u.Displayname;
}
}
public string UpdateDownloadLink => FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion?.GetUpdateUrls()?.FirstOrDefault();
public UpdatePollerStates UpdaterState { get { return FIXMEGlobal.UpdatePoller.ThreadState; } }
public double UpdateDownloadProgress { get { return FIXMEGlobal.UpdatePoller.DownloadProgess; } }
public Tuple<long, string> ActiveTask
{
get
{
var t = FIXMEGlobal.WorkThread.CurrentTask;
if (t == null)
return null;
else
return new Tuple<long, string>(t.TaskID, t.Backup == null ? null : t.Backup.ID);
}
}
public IList<Tuple<long, string>> SchedulerQueueIds
{
get { return (from n in FIXMEGlobal.Scheduler.WorkerQueue where n.Backup != null select new Tuple<long, string>(n.TaskID, n.Backup.ID)).ToList(); }
}
public IList<Tuple<string, DateTime>> ProposedSchedule
{
get
{
return (
from n in FIXMEGlobal.Scheduler.Schedule
let backupid = (from t in n.Value.Tags
where t != null && t.StartsWith("ID=", StringComparison.Ordinal)
select t.Substring("ID=".Length)).FirstOrDefault()
where !string.IsNullOrWhiteSpace(backupid)
select new Tuple<string, DateTime>(backupid, n.Key)
).ToList();
}
}
public bool HasWarning { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning; } }
public bool HasError { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError; } }
public SuggestedStatusIcon SuggestedStatusIcon
{
get
{
if (this.ActiveTask == null)
{
if (this.ProgramState == LiveControlState.Paused)
return SuggestedStatusIcon.Paused;
if (this.HasError)
return SuggestedStatusIcon.ReadyError;
if (this.HasWarning)
return SuggestedStatusIcon.ReadyWarning;
return SuggestedStatusIcon.Ready;
}
else
{
if (this.ProgramState == LiveControlState.Running)
return SuggestedStatusIcon.Active;
else
return SuggestedStatusIcon.ActivePaused;
}
}
}
public DateTime EstimatedPauseEnd
{
get
{
return liveControls.EstimatedPauseEnd;
}
}
private long m_lastEventID = FIXMEGlobal.StatusEventNotifyer.EventNo;
public long LastEventID
{
get { return m_lastEventID; }
set { m_lastEventID = value; }
}
public long LastDataUpdateID => FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
public long LastNotificationUpdateID => FIXMEGlobal.NotificationUpdateService.LastNotificationUpdateId;
}
}
+22 -29
View File
@@ -93,12 +93,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.R
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Common", "Duplicati\Library\Common\Duplicati.Library.Common.csproj", "{D63E53E4-A458-4C2F-914D-92F715F58ACF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.ConfigurationImporter", "Duplicati\CommandLine\ConfigurationImporter\Duplicati.CommandLine.ConfigurationImporter.csproj", "{B93E3BF0-DAA7-49B7-B07D-0559C5816735}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Encryption", "Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj", "{2CF2D90E-C25B-47AD-91E0-98451BAB8058}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.AutoUpdater", "Duplicati\CommandLine\AutoUpdater\Duplicati.CommandLine.AutoUpdater.csproj", "{4D1B198B-F773-44F2-872B-2B283053323C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Storj", "Duplicati\Library\Backend\Storj\Duplicati.Library.Backend.Storj.csproj", "{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.TencentCOS", "Duplicati\Library\Backend\TencentCOS\Duplicati.Library.Backend.TencentCOS.csproj", "{545DD6D4-9476-42D6-B51C-A28E000C489E}"
@@ -115,16 +111,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.Backe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.BackendTool", "Executables\net8\Duplicati.CommandLine.BackendTool\Duplicati.CommandLine.BackendTool.csproj", "{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.ConfigurationImporter", "Executables\net8\Duplicati.CommandLine.ConfigurationImporter\Duplicati.CommandLine.ConfigurationImporter.csproj", "{156B9B77-C2B7-4169-8FE7-1171D221446C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.RecoveryTool", "Executables\net8\Duplicati.CommandLine.RecoveryTool\Duplicati.CommandLine.RecoveryTool.csproj", "{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.GUI.TrayIcon", "Executables\net8\Duplicati.GUI.TrayIcon\Duplicati.GUI.TrayIcon.csproj", "{AF32C621-30DC-40F5-8CE6-DD69053068E9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Server", "Executables\net8\Duplicati.Server\Duplicati.Server.csproj", "{55EEEBD2-CE45-45D6-9838-958F1C7354E4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsService", "Duplicati\WindowsService\WindowsService.csproj", "{8A651E2D-A7C8-4EC7-B421-7A457654811D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.WindowsService", "Executables\net8\Duplicati.WindowsService\Duplicati.WindowsService.csproj", "{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine", "Executables\net8\Duplicati.CommandLine\Duplicati.CommandLine.csproj", "{0F5A1F4E-25FA-4D02-920D-CA2138498081}"
@@ -144,7 +136,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backends"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.WebserverCore", "Duplicati\WebserverCore\Duplicati.WebserverCore.csproj", "{5A702CEE-DB36-4153-BD94-D8CF867E75A9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.RestAPI", "Duplicati.Library.RestAPI\Duplicati.Library.RestAPI.csproj", "{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.RestAPI", "Duplicati\Library\RestAPI\Duplicati.Library.RestAPI.csproj", "{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.AliyunOSS", "Duplicati\Library\Backend\AliyunOSS\Duplicati.Library.Backend.AliyunOSS.csproj", "{4EB3DABC-D412-4C12-8876-41A1427A389E}"
EndProject
@@ -152,6 +144,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.Sharp
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.Snapshots", "Executables\net8\Duplicati.CommandLine.Snapshots\Duplicati.CommandLine.Snapshots.csproj", "{0364E724-1929-445E-9145-90A70B01DDC0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.ServerUtil", "Duplicati\CommandLine\ServerUtil\Duplicati.CommandLine.ServerUtil.csproj", "{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.ServerUtil", "Executables\net8\Duplicati.CommandLine.ServerUtil\Duplicati.CommandLine.ServerUtil.csproj", "{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.WindowsService", "Duplicati\WindowsService\Duplicati.WindowsService.csproj", "{3476A88B-4123-45F4-AC96-700B747367EB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -338,18 +336,10 @@ Global
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Release|Any CPU.Build.0 = Release|Any CPU
{B93E3BF0-DAA7-49B7-B07D-0559C5816735}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B93E3BF0-DAA7-49B7-B07D-0559C5816735}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B93E3BF0-DAA7-49B7-B07D-0559C5816735}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B93E3BF0-DAA7-49B7-B07D-0559C5816735}.Release|Any CPU.Build.0 = Release|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.Build.0 = Release|Any CPU
{4D1B198B-F773-44F2-872B-2B283053323C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D1B198B-F773-44F2-872B-2B283053323C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D1B198B-F773-44F2-872B-2B283053323C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D1B198B-F773-44F2-872B-2B283053323C}.Release|Any CPU.Build.0 = Release|Any CPU
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -374,10 +364,6 @@ Global
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Release|Any CPU.Build.0 = Release|Any CPU
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Release|Any CPU.Build.0 = Release|Any CPU
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -390,10 +376,6 @@ Global
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Release|Any CPU.Build.0 = Release|Any CPU
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Release|Any CPU.Build.0 = Release|Any CPU
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -426,6 +408,18 @@ Global
{0364E724-1929-445E-9145-90A70B01DDC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0364E724-1929-445E-9145-90A70B01DDC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0364E724-1929-445E-9145-90A70B01DDC0}.Release|Any CPU.Build.0 = Release|Any CPU
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7}.Release|Any CPU.Build.0 = Release|Any CPU
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B}.Release|Any CPU.Build.0 = Release|Any CPU
{3476A88B-4123-45F4-AC96-700B747367EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3476A88B-4123-45F4-AC96-700B747367EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3476A88B-4123-45F4-AC96-700B747367EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3476A88B-4123-45F4-AC96-700B747367EB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -461,8 +455,6 @@ Global
{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{32A74526-3E5F-413A-8CB4-1EFDAD4C8B91} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{851A1CB8-3CEB-41B4-956F-34D760D2A8E5} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{B93E3BF0-DAA7-49B7-B07D-0559C5816735} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
{4D1B198B-F773-44F2-872B-2B283053323C} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{545DD6D4-9476-42D6-B51C-A28E000C489E} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3} = {FA88A246-EF8E-46E3-90AF-539B8C0A6ADE}
@@ -470,17 +462,18 @@ Global
{34149709-F3ED-4FB5-A087-43EB195C948B} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{156B9B77-C2B7-4169-8FE7-1171D221446C} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{AF32C621-30DC-40F5-8CE6-DD69053068E9} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{55EEEBD2-CE45-45D6-9838-958F1C7354E4} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{8A651E2D-A7C8-4EC7-B421-7A457654811D} = {566EBBDA-19A4-4056-A615-D901D57D2439}
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{0F5A1F4E-25FA-4D02-920D-CA2138498081} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{6B594D23-B629-465C-B799-70EE9E56C218} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B} = {FA88A246-EF8E-46E3-90AF-539B8C0A6ADE}
{FE6FD36C-E171-4599-8D55-62DA579C0864} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{0364E724-1929-445E-9145-90A70B01DDC0} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{5AF834B1-D227-4A98-9377-6BFA6BCF99A7} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
{C09A7DE2-F0F3-4FA4-B36C-A0DE7844739B} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{3476A88B-4123-45F4-AC96-700B747367EB} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B40BAFE-D862-4397-9495-8F5EAF5CE80C}
@@ -1,24 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Duplicati.CommandLine.AutoUpdater.Implementation</AssemblyName>
<RootNamespace>Duplicati.CommandLine.AutoUpdater</RootNamespace>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj" />
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
+12 -10
View File
@@ -34,9 +34,9 @@ namespace Duplicati.CommandLine.BackendTester
/// <summary>
/// Used to maintain a reference to initialized system settings.
/// </summary>
#pragma warning disable CS0414 // The private field `Duplicati.CommandLine.BackendTester.Program.SystemSettings' is assigned but its value is never used
#pragma warning disable CS0414 // The private field `Duplicati.CommandLine.BackendTester.Program.SystemSettings' is assigned but its value is never used
private static IDisposable SystemSettings;
#pragma warning restore CS0414 // The private field `Duplicati.CommandLine.BackendTester.Program.SystemSettings' is assigned but its value is never used
#pragma warning restore CS0414 // The private field `Duplicati.CommandLine.BackendTester.Program.SystemSettings' is assigned but its value is never used
class TempFile
{
@@ -66,6 +66,8 @@ namespace Duplicati.CommandLine.BackendTester
{
try
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, Library.AutoUpdater.PackageHelper.NamedExecutable.BackendTester);
if (_args.Length == 1)
{
try
@@ -73,8 +75,8 @@ namespace Duplicati.CommandLine.BackendTester
var p = Environment.ExpandEnvironmentVariables(_args[0]);
if (System.IO.File.Exists(p))
_args = (from x in System.IO.File.ReadLines(p)
where !string.IsNullOrWhiteSpace(x) && !x.Trim().StartsWith("#", StringComparison.Ordinal)
select x.Trim()
where !string.IsNullOrWhiteSpace(x) && !x.Trim().StartsWith("#", StringComparison.Ordinal)
select x.Trim()
).ToArray();
}
catch
@@ -105,7 +107,7 @@ namespace Duplicati.CommandLine.BackendTester
if (options.ContainsKey("tempdir") && !string.IsNullOrEmpty(options["tempdir"]))
Library.Utility.SystemContextSettings.DefaultTempPath = options["tempdir"];
SystemSettings = Duplicati.Library.Utility.SystemContextSettings.StartSession();
if (!options.ContainsKey("auth_password") && !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("AUTH_PASSWORD")))
@@ -594,11 +596,11 @@ namespace Duplicati.CommandLine.BackendTester
new CommandLineArgument("max-file-size", CommandLineArgument.ArgumentType.Size, "The maximum allowed file size", "File sizes are chosen at random, this value is the upper bound", "50mb"),
new CommandLineArgument("min-filename-length", CommandLineArgument.ArgumentType.Integer, "The minimum allowed filename length", "File name lengths are chosen at random, this value is the lower bound", "5"),
new CommandLineArgument("max-filename-length", CommandLineArgument.ArgumentType.Integer, "The minimum allowed filename length", "File name lengths are chosen at random, this value is the upper bound", "80"),
new CommandLineArgument("trim-filename-spaces", CommandLineArgument.ArgumentType.Boolean, "Trims whitespace from filenames", "A value that indicates if whitespace should be trimmed from the ends of randomly generated filenames", "false"),
new CommandLineArgument("auto-create-folder", CommandLineArgument.ArgumentType.Boolean, "Allows automatic folder creation", "A value that indicates if missing folders are created automatically", "false"),
new CommandLineArgument("skip-overwrite-test", CommandLineArgument.ArgumentType.Boolean, "Bypasses the overwrite test", "A value that indicates if dummy files should be uploaded prior to uploading the real files", "false"),
new CommandLineArgument("auto-clean", CommandLineArgument.ArgumentType.Boolean, "Removes any files found in target folder", "A value that indicates if all files in the target folder should be deleted before starting the first test", "false"),
new CommandLineArgument("force", CommandLineArgument.ArgumentType.Boolean, "Activates file deletion", "A value that indicates if existing files should really be deleted when using auto-clean", "false"),
new CommandLineArgument("trim-filename-spaces", CommandLineArgument.ArgumentType.Boolean, "Trim whitespace from filenames", "A value that indicates if whitespace should be trimmed from the ends of randomly generated filenames", "false"),
new CommandLineArgument("auto-create-folder", CommandLineArgument.ArgumentType.Boolean, "Allow automatic folder creation", "A value that indicates if missing folders are created automatically", "false"),
new CommandLineArgument("skip-overwrite-test", CommandLineArgument.ArgumentType.Boolean, "Bypass the overwrite test", "A value that indicates if dummy files should be uploaded prior to uploading the real files", "false"),
new CommandLineArgument("auto-clean", CommandLineArgument.ArgumentType.Boolean, "Remove any files found in target folder", "A value that indicates if all files in the target folder should be deleted before starting the first test", "false"),
new CommandLineArgument("force", CommandLineArgument.ArgumentType.Boolean, "Activate file deletion", "A value that indicates if existing files should really be deleted when using auto-clean", "false"),
});
}
}
+32 -28
View File
@@ -39,6 +39,8 @@ namespace Duplicati.CommandLine.BackendTool
bool debugoutput = false;
try
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, Library.AutoUpdater.PackageHelper.NamedExecutable.BackendTool);
List<string> args = new List<string>(_args);
Dictionary<string, string> options = Library.Utility.CommandLineParser.ExtractOptions(args);
@@ -77,8 +79,8 @@ namespace Duplicati.CommandLine.BackendTool
{
Console.WriteLine("Unsupported command: {0}", args[0]);
Console.WriteLine();
}
}
Console.WriteLine("Usage: <command> <protocol>://<username>:<password>@<path> [filename]");
Console.WriteLine("Example: LIST ftp://user:pass@server/folder");
Console.WriteLine();
@@ -89,37 +91,39 @@ namespace Duplicati.CommandLine.BackendTool
}
var modules = (from n in Library.DynamicLoader.GenericLoader.Modules
where n is Library.Interface.IConnectionModule
select n).ToArray();
where n is Library.Interface.IConnectionModule
select n).ToArray();
var uri = new Library.Utility.Uri(args[1]);
var qp = uri.QueryParameters;
var uri = new Library.Utility.Uri(args[1]);
var qp = uri.QueryParameters;
var backendOpts = new Dictionary<string, string>();
foreach (var k in qp.Keys.Cast<string>())
backendOpts[k] = qp[k];
var backendOpts = new Dictionary<string, string>();
foreach (var k in qp.Keys.Cast<string>())
backendOpts[k] = qp[k];
foreach (var k in backendOpts.Keys) {
options.Remove(k);
}
foreach (var k in backendOpts.Keys)
{
options.Remove(k);
}
foreach (var n in modules) {
n.Configure(options);
n.Configure(backendOpts);
}
using(var backend = Library.DynamicLoader.BackendLoader.GetBackend(args[1], options))
foreach (var n in modules)
{
n.Configure(options);
n.Configure(backendOpts);
}
using (var backend = Library.DynamicLoader.BackendLoader.GetBackend(args[1], options))
{
if (backend == null)
throw new UserInformationException("Backend not supported", "InvalidBackend");
if (command == "list")
{
if (args.Count != 2)
throw new UserInformationException(string.Format("too many arguments: {0}", string.Join(",", args)), "BackendToolTooManyArguments");
Console.WriteLine("{0}\t{1}\t{2}\t{3}", "Name", "Dir/File", "LastChange", "Size");
foreach(var e in backend.List())
foreach (var e in backend.List())
Console.WriteLine("{0}\t{1}\t{2}\t{3}", e.Name, e.IsFolder ? "Dir" : "File", e.LastModification, e.Size < 0 ? "" : Library.Utility.Utility.FormatSizeString(e.Size));
return 0;
@@ -130,7 +134,7 @@ namespace Duplicati.CommandLine.BackendTool
throw new UserInformationException(string.Format("too many arguments: {0}", string.Join(",", args)), "BackendToolTooManyArguments");
backend.CreateFolder();
return 0;
}
else if (command == "delete")
@@ -140,7 +144,7 @@ namespace Duplicati.CommandLine.BackendTool
if (args.Count > 3)
throw new Exception(string.Format("too many arguments: {0}", string.Join(",", args)));
backend.Delete(Path.GetFileName(args[2]));
return 0;
}
else if (command == "get")
@@ -152,21 +156,21 @@ namespace Duplicati.CommandLine.BackendTool
if (File.Exists(args[2]))
throw new UserInformationException("File already exists, not overwriting!", "BackendToolFileAlreadyExists");
backend.Get(Path.GetFileName(args[2]), args[2]);
return 0;
}
else if (command == "put")
{
if (args.Count < 3)
throw new UserInformationException("PUT requires a filename argument","BackendToolPutRequiresAndArgument");
throw new UserInformationException("PUT requires a filename argument", "BackendToolPutRequiresAndArgument");
if (args.Count > 3)
throw new UserInformationException(string.Format("too many arguments: {0}", string.Join(",", args)), "BackendToolTooManyArguments");
backend.PutAsync(Path.GetFileName(args[2]), args[2], CancellationToken.None).Wait();
return 0;
}
throw new Exception("Internal error");
}
}
+1 -1
View File
@@ -662,7 +662,7 @@ namespace Duplicati.CommandLine
}
var parsedStats = result.BackendStatistics as Duplicati.Library.Interface.IParsedBackendStatistics;
output.MessageEvent(string.Format(" Duration of backup: {0:hh\\:mm\\:ss}", result.Duration));
output.MessageEvent(string.Format(" Duration of backup: {0:c}", result.Duration));
if (parsedStats != null)
{
if (parsedStats.KnownFileCount > 0)
+3 -1
View File
@@ -40,6 +40,8 @@ namespace Duplicati.CommandLine
/// </summary>
public static int Main(string[] args)
{
PreloadSettingsLoader.ConfigurePreloadSettings(ref args, PackageHelper.NamedExecutable.CommandLine);
Library.UsageReporter.Reporter.Initialize();
FROM_COMMANDLINE = true;
try
@@ -85,7 +87,7 @@ namespace Duplicati.CommandLine
["systeminfo"] = Commands.SystemInfo,
["send-mail"] = Commands.SendMail
};
return knownCommands;
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ namespace Duplicati.CommandLine.Strings
public static string FailedToParseParametersFileError(string path, string message) { return LC.L(@"Unable to read the parameters file ""{0}"", reason: {1}", path, message); }
public static string FiltersCannotBeUsedWithFileError2 { get { return LC.L(@"Filters cannot be specified on the commandline if filters are also present in the parameter file. Use the special --{0}, --{1}, or --{2} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {3}.", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
public static string InternalOptionUsedError(string optionname) { return LC.L(@"The option --{0} was supplied, but it is reserved for internal use and may not be set on the commandline.", optionname); }
public static string ParametersFileOptionLong2 { get { return LC.L(@"This option can be used to store some or all of the options given to the commandline client. The file must be a plain text file, and UTF-8 encoding is preferred. Each line in the file should be of the format --option=value. The special options --{0} and --{1} can be used to override the localpath and the remote destination uri, respectively. The options in this file take precedence over the options provided on the commandline. You cannot specify filters in both the file and on the commandline. Instead, you can use the special --{2}, --{3}, or --{4} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {5}.", "source", "target", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
public static string ParametersFileOptionLong2 { get { return LC.L(@"Use this option to store some or all of the options given to the commandline client. The file must be a plain text file, and UTF-8 encoding is preferred. Each line in the file should be of the format --option=value. Use the special options --{0} and --{1} to override the localpath and the remote destination uri, respectively. The options in this file take precedence over the options provided on the commandline. You cannot specify filters in both the file and on the commandline. Instead, you can use the special --{2}, --{3}, or --{4} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {5}.", "source", "target", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
public static string ParametersFileOptionShort { get { return LC.L(@"Path to a file with parameters"); } }
public static string UnhandledException(string message) { return LC.L(@"An error occured: {0}", message); }
public static string UnhandledInnerException(string message) { return LC.L(@"The inner error message is: {0}", message); }
@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<AssemblyName>Duplicati.CommandLine.ConfigurationImporter.Implementation</AssemblyName>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\..\Server\Duplicati.Server.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -1,103 +0,0 @@
// 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 Duplicati.Library.AutoUpdater;
using Duplicati.Library.RestAPI;
using Duplicati.Server.Serializable;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Duplicati.CommandLine.ConfigurationImporter
{
public static class ConfigurationImporter
{
private static readonly string UsageString = $"Usage: {PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.ConfigurationImporter)} <configuration-file> --import-metadata=(true | false) --server-datafolder=<folder containing Duplicati-server.sqlite>";
public static int Main(string[] args)
{
if (args.Length != 3)
{
Console.WriteLine($"Incorrect number of input arguments.");
Console.WriteLine(UsageString);
return 1;
}
string configurationFile = args[0];
Dictionary<string, string> importOptions = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args.Skip(1).ToList());
if (!importOptions.TryGetValue("import-metadata", out string importMetadataString))
{
Console.WriteLine($"Missing import-metadata argument.");
Console.WriteLine(UsageString);
return 1;
}
bool importMetadata = Duplicati.Library.Utility.Utility.ParseBool(importMetadataString, false);
if (!importOptions.TryGetValue("server-datafolder", out string serverDatafolder))
{
Console.WriteLine($"Missing server-datafolder argument.");
Console.WriteLine(UsageString);
return 1;
}
Dictionary<string, string> advancedOptions = new Dictionary<string, string>
{
{ "server-datafolder", serverDatafolder }
};
ImportExportStructure importedStructure = BackupImportExportHandler.ImportBackup(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: "), advancedOptions);
Console.WriteLine($"Imported \"{importedStructure.Backup.Name}\" with ID {importedStructure.Backup.ID} and local database at {importedStructure.Backup.DBPath}.");
return 0;
}
private static string ReadPassword(string prompt)
{
Console.Write(prompt);
string password = "";
while (true)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
if (keyInfo.Key == ConsoleKey.Enter)
{
break;
}
if (keyInfo.Key == ConsoleKey.Backspace)
{
if (password.Length > 0)
{
password = password.Substring(0, password.Length - 1);
Console.Write("\b \b");
}
}
else if (keyInfo.KeyChar != '\u0000') // Only accept if the key maps to a Unicode character (e.g., ignore F1 or Home).
{
password += keyInfo.KeyChar;
Console.Write("*");
}
}
Console.WriteLine();
return password;
}
}
}
@@ -37,6 +37,8 @@ namespace Duplicati.CommandLine.RecoveryTool
{
try
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, Library.AutoUpdater.PackageHelper.NamedExecutable.RecoveryTool);
var args = new List<string>(_args);
var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(args);
var options = tmpparsed.Item1;
@@ -99,7 +101,7 @@ namespace Duplicati.CommandLine.RecoveryTool
return command(args, options, filter);
}
catch(Exception ex)
catch (Exception ex)
{
if (ex is Duplicati.Library.Interface.UserInformationException)
Console.WriteLine(ex.Message);
@@ -127,7 +129,7 @@ namespace Duplicati.CommandLine.RecoveryTool
if (!newfilter.Empty)
filter = newfilter;
foreach(KeyValuePair<String, String> keyvalue in opt)
foreach (KeyValuePair<String, String> keyvalue in opt)
options[keyvalue.Key] = keyvalue.Value;
cargs.AddRange(
@@ -135,7 +137,7 @@ namespace Duplicati.CommandLine.RecoveryTool
where !string.IsNullOrWhiteSpace(c) && !c.StartsWith("#", StringComparison.Ordinal) && !c.StartsWith("!", StringComparison.Ordinal) && !c.StartsWith("REM ", StringComparison.OrdinalIgnoreCase)
select c
);
return true;
}
catch (Exception e)
@@ -199,7 +199,7 @@ namespace Duplicati.CommandLine.RecoveryTool
{
Console.Write(" recompressing ...");
//Recompressing from e.g. zip to zip
//Recompressing from e.g. ZIP to ZIP
if (localFileSource == localFileTarget)
{
File.Move(localFileSource, localFileSource + ".same");
+2 -2
View File
@@ -25,7 +25,7 @@ Optionally you can also run:
Shows what files are available and tests filters
5: Recompress
Ability to change compression type of files on remote backend e.g. from 7z to zip
Ability to change compression type of files on remote backend e.g. from 7z to ZIP
@@ -71,7 +71,7 @@ If the process is interrupted for any reason, note the file counter and use --of
Advanced performance options are:
--reduce-memory-use: Disables keeping all hashes in memory; use if memory is limited on the restoring machine
--disable-file-verify: Disables the initial hashing of the restored file
--disable-wrapped-zip: Disable using the faster .NET native Zip archive in favor of the more resilient one in Duplicati
--disable-wrapped-zip: Disable using the faster .NET native ZIP archive in favor of the more resilient one in Duplicati
--max-open-archives: Sets the number of archives to keep open for faster access (uses some memory pr. archive); default 200
List
@@ -0,0 +1,22 @@
using System.CommandLine;
using System.CommandLine.Invocation;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Extensions for <see cref="Command"/>.
/// </summary>
public static class CommandExtensions
{
/// <summary>
/// Adds the missing WithHandler method to <see cref="Command"/>.
/// </summary>
/// <param name="command">The command to add the handler to.</param>
/// <param name="handler">The handler to add.</param>
/// <returns>The command with the handler added.</returns>
public static Command WithHandler(this Command command, ICommandHandler handler)
{
command.Handler = handler;
return command;
}
}
@@ -0,0 +1,28 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class ChangePassword
{
public static Command Create() =>
new Command("change-password", "Changes the server password")
{
new Argument<string>("new-password", "The new password to use") {
Arity = ArgumentArity.ZeroOrOne
},
}
.WithHandler(CommandHandler.Create<Settings, string>(async (settings, newPassword) =>
{
// Ask for previous password first, if needed
var connection = await settings.GetConnection();
if (string.IsNullOrWhiteSpace(newPassword))
newPassword = HelperMethods.ReadPasswordFromConsole("Please provide the new password: ");
if (string.IsNullOrWhiteSpace(newPassword))
throw new UserReportedException("No password provided");
await connection.ChangePassword(newPassword);
}));
}
@@ -0,0 +1,49 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Import
{
public static Command Create() =>
new Command("import", "Import a backup configuration")
{
new Argument<FileInfo>("file", "The file to import, may be encrypted") {
Arity = ArgumentArity.ExactlyOne
},
new Argument<string>("passphrase", "The passphrase to use for decryption") {
Arity = ArgumentArity.ZeroOrOne
},
new Option<bool>(name: "--import-metadata", description: "Import metadata from the backup", getDefaultValue: () => false)
}
.WithHandler(CommandHandler.Create<Settings, FileInfo, string, bool>(async (settings, file, passphrase, importMetadata) =>
{
if (!file.Exists)
throw new UserReportedException($"File {file.FullName} does not exist");
Console.WriteLine($"Importing backup configuration from {file.FullName}");
if (IsEncrypted(file))
{
if (string.IsNullOrWhiteSpace(passphrase))
passphrase = HelperMethods.ReadPasswordFromConsole("The file is encrypted. Please provide the encryption password: ");
if (string.IsNullOrWhiteSpace(passphrase))
throw new UserReportedException("No password provided");
}
var connection = await settings.GetConnection();
var result = await connection.ImportBackup(file.FullName, passphrase, importMetadata);
Console.WriteLine($"Imported \"{result.Name}\" with ID {result.ID}");
}));
private static bool IsEncrypted(FileInfo file)
{
using var fs = file.OpenRead();
var header = new byte[3].AsSpan();
if (fs.Read(header) != 3)
return false;
return header.SequenceEqual("AES"u8);
}
}
@@ -0,0 +1,28 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class ListBackups
{
public static Command Create() =>
new Command("list-backups", "List all backups")
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
{
var bks = await (await settings.GetConnection()).ListBackups();
if (!bks.Any())
{
Console.WriteLine("No backups found");
return;
}
foreach (var bk in bks)
{
Console.WriteLine($"{bk.ID}: {bk.Name}");
if (!string.IsNullOrEmpty(bk.Description))
Console.WriteLine($" {bk.Description}");
Console.WriteLine();
}
}));
}
@@ -0,0 +1,18 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Login
{
public static Command Create() =>
new Command("login", "Logs in to the server")
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
{
Console.WriteLine("Logging in to the server");
await Connection.Connect(settings, true);
Console.WriteLine("Logged in, persistent token saved");
})
);
}
@@ -0,0 +1,13 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Logout
{
public static Command Create()
=> new Command("logout", "Logs out of the server")
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
await (await settings.GetConnection()).Logout(settings))
);
}
@@ -0,0 +1,24 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Pause
{
public static Command Create() =>
new Command("pause", "Pauses the server")
{
new Argument<string?>("duration", description: "The duration to pause the server for", getDefaultValue: () => null) {
Arity = ArgumentArity.ZeroOrOne
},
}
.WithHandler(CommandHandler.Create<Settings, string?>(async (settings, duration) =>
{
if (string.IsNullOrWhiteSpace(duration))
Console.WriteLine("Pausing the server indefinitely");
else
Console.WriteLine($"Pausing the server for {duration}");
await (await settings.GetConnection()).Pause(duration);
}));
}
@@ -0,0 +1,13 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class Resume
{
public static Command Create()
=> new Command("resume", "Resumes the server")
.WithHandler(CommandHandler.Create<Settings>(async (settings) =>
await (await settings.GetConnection()).Resume())
);
}
@@ -0,0 +1,28 @@
using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
namespace Duplicati.CommandLine.ServerUtil.Commands;
public static class RunBackup
{
public static Command Create() =>
new Command("run", "Runs a backup")
{
new Argument<string>("backup", "The backup to run, either ID or exact name (case-insensitive)") {
Arity = ArgumentArity.ExactlyOne
},
}
.WithHandler(CommandHandler.Create<Settings, string>(async (settings, backup) =>
{
var connection = await settings.GetConnection();
var matchingBackup = (await connection.ListBackups())
.FirstOrDefault(b => string.Equals(b.Name, backup, StringComparison.OrdinalIgnoreCase) || string.Equals(b.ID, backup));
if (matchingBackup == null)
throw new UserReportedException("No backup found with supplied ID or name");
Console.WriteLine($"Running backup {matchingBackup.Name} (ID: {matchingBackup.ID})");
await connection.RunBackup(matchingBackup.ID);
}));
}
@@ -0,0 +1,451 @@
using System.Net.Http.Json;
using System.Text.Json;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Implementation of actions performed on the server.
/// </summary>
public class Connection
{
/// <summary>
/// The reported backup data
/// </summary>
/// <param name="ID">The ID of the backup</param>
/// <param name="Name">The name of the backup</param>
/// <param name="Description">The description of the backup</param>
/// <param name="Metadata">The metadata of the backup</param>
public sealed record BackupEntry(
string ID,
string Name,
string Description,
Dictionary<string, string>? Metadata
);
/// <summary>
/// The response backup data returned from the server
/// </summary>
/// <param name="Backup">The backup details</param>
private sealed record ResponseBackupEntry(ResponseBackupEntry.ResponseBackupDetailsEntry Backup)
{
/// <summary>
/// The response backup details entry
/// </summary>
/// <param name="ID">The ID of the backup</param>
/// <param name="Name">The name of the backup</param>
/// <param name="Description">The description of the backup</param>
/// <param name="DBPath">The path to the local database</param>
/// <param name="Metadata">The metadata of the backup</param>
public sealed record ResponseBackupDetailsEntry(
string ID,
string Name,
string? Description,
string? DBPath,
Dictionary<string, string>? Metadata
);
/// <summary>
/// Converts the response backup entry to a backup entry
/// </summary>
/// <returns>The backup entry</returns>
public BackupEntry ToBackupEntry()
=> new BackupEntry(Backup.ID, Backup.Name, Backup.Description ?? "", Backup.Metadata);
}
/// <summary>
/// The task entry
/// </summary>
/// <param name="TaskID">The ID of the task</param>
/// <param name="BackupID">The ID of the backup</param>
/// <param name="Operation">The operation of the task</param>
public sealed record TaskEntry(
long TaskID,
string BackupID,
string Operation
);
/// <summary>
/// The stop level
/// </summary>
public enum StopLevel
{
/// <summary>
/// Stop after the current file
/// </summary>
AfterCurrentFile,
/// <summary>
/// Stop now
/// </summary>
StopNow,
/// <summary>
/// Stop immediately
/// </summary>
Abort
}
/// <summary>
/// The HTTP client used to connect to the server
/// </summary>
private readonly HttpClient client;
/// <summary>
/// Initializes a new instance of the <see cref="Connection"/> class
/// </summary>
private Connection(HttpClient client)
{
this.client = client;
}
/// <summary>
/// Connects to the server
/// </summary>
/// <param name="settings">The settings to use for the connection</param>
/// <param name="obtainRefreshToken">Whether to obtain a refresh token</param>
/// <returns>The connection</returns>
public static async Task<Connection> Connect(Settings settings, bool obtainRefreshToken = false)
{
Console.WriteLine($"Connecting to {settings.HostUrl}...");
// Configure the client for requests
var client = new HttpClient(new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = settings.Insecure
? HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
: null
})
{
BaseAddress = new Uri(settings.HostUrl + "api/v1/")
};
// If we already have a refresh token, try that first
try
{
if (!string.IsNullOrWhiteSpace(settings.RefreshToken))
{
var (accessToken, refreshToken) = await LoginWithRefreshToken(client, settings.RefreshToken);
if (string.IsNullOrWhiteSpace(accessToken))
throw new InvalidOperationException("Failed to get access token");
if (string.IsNullOrWhiteSpace(refreshToken))
throw new InvalidOperationException("Failed to get refresh token");
(settings with { RefreshToken = refreshToken }).Save();
return CreateConnectionWithClient(client, accessToken);
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to use refresh token: {ex.Message}");
}
// If we can read the server database, try to create a signin token
try
{
var opts = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(settings.ServerDatafolder))
opts.Add("server-datafolder", settings.ServerDatafolder);
if (File.Exists(Path.Combine(Server.Program.GetDataFolderPath(opts), Server.Program.SERVER_DATABASE_FILENAME)))
{
string? cfg = null;
using (var connection = Duplicati.Server.Program.GetDatabaseConnection(opts, true))
cfg = connection.ApplicationSettings.JWTConfig;
if (!string.IsNullOrWhiteSpace(cfg))
{
var signinjwt = new WebserverCore.Middlewares.JWTTokenProvider(
JsonSerializer.Deserialize<WebserverCore.Middlewares.JWTConfig>(cfg)
?? throw new InvalidOperationException("Failed to deserialize JWTConfig")
).CreateSigninToken("server-cli");
var responseTask = client.PostAsync("auth/signin", JsonContent.Create(new { SigninToken = signinjwt, RememberMe = obtainRefreshToken }));
var (accessToken, refreshToken) = await ParseAuthResponse(responseTask);
if (string.IsNullOrWhiteSpace(accessToken))
throw new InvalidOperationException("Failed to get access token");
if (!string.IsNullOrWhiteSpace(refreshToken))
(settings with { RefreshToken = refreshToken }).Save();
return CreateConnectionWithClient(client, accessToken);
}
}
else if (!string.IsNullOrWhiteSpace(settings.ServerDatafolder))
{
Console.WriteLine($"No database found in {settings.ServerDatafolder}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to obtain a signin token: {ex.Message}");
}
// Otherwise, we need a password to log in
try
{
// Try obtaining the password from the user
if (string.IsNullOrWhiteSpace(settings.Password))
settings = settings with { Password = HelperMethods.ReadPasswordFromConsole("Enter server password: ") };
if (string.IsNullOrWhiteSpace(settings.Password))
throw new UserReportedException("Password is required");
var (accessToken, refreshToken) = await LoginWithPassword(client, settings.Password, obtainRefreshToken);
if (string.IsNullOrWhiteSpace(accessToken))
throw new InvalidOperationException("Failed to get access token");
if (!string.IsNullOrWhiteSpace(refreshToken))
(settings with { RefreshToken = refreshToken }).Save();
return CreateConnectionWithClient(client, accessToken);
}
catch (Exception ex)
{
client.Dispose();
throw new UserReportedException($"Failed to connect to server: {ex.Message}", ex);
}
}
/// <summary>
/// Creates the connection and adds the authorization header
/// </summary>
/// <param name="client">The HTTP client</param>
/// <param name="accessToken">The access token</param>
/// <returns>The connection</returns>
private static Connection CreateConnectionWithClient(HttpClient client, string accessToken)
{
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}");
return new Connection(client);
}
/// <summary>
/// Logs in with a password
/// </summary>
/// <param name="client">The HTTP client</param>
/// <param name="password">The password to use</param>
/// <param name="obtainRefreshToken">Whether to obtain a refresh token</param>
/// <returns>The access and refresh tokens</returns>
private static Task<(string? AccessToken, string? RefreshToken)> LoginWithPassword(HttpClient client, string password, bool obtainRefreshToken)
=> ParseAuthResponse(
client.PostAsync($"auth/login", JsonContent.Create(new { Password = password, RememberMe = obtainRefreshToken }))
);
/// <summary>
/// Logs in with a refresh token
/// </summary>
/// <param name="client">The HTTP client</param>
/// <param name="refreshToken">The refresh token to use</param>
/// <returns>The access and refresh tokens</returns>
private static Task<(string? AccessToken, string? RefreshToken)> LoginWithRefreshToken(HttpClient client, string refreshToken)
=> ParseAuthResponse(client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "auth/refresh")
{
Headers = { { "Cookie", $"RefreshToken_{client.BaseAddress!.Port}={refreshToken}" } },
}));
/// <summary>
/// Parses the authentication response
/// </summary>
/// <param name="response">The response to parse</param>
/// <returns>The access and refresh tokens</returns>
private static async Task<(string? AccessToken, string? RefreshToken)> ParseAuthResponse(Task<HttpResponseMessage> responseTask)
{
var response = await responseTask;
await EnsureSuccessStatusCodeWithParsing(response);
var json = JsonSerializer.Deserialize<Dictionary<string, string>>(response.Content.ReadAsStringAsync().Result)
?? throw new InvalidOperationException("Failed to parse response");
if (!json.TryGetValue("AccessToken", out var accessToken))
throw new InvalidOperationException("Failed to get access token");
response.Headers.TryGetValues("Set-Cookie", out var cookies);
var refreshToken = cookies?.SelectMany(c => c.Split(';')).FirstOrDefault(c => c.StartsWith($"RefreshToken_"))?.Split('=', 2)[1];
return (accessToken, refreshToken);
}
/// <summary>
/// Pauses the server
/// </summary>
/// <param name="duration">The duration to pause for</param>
/// <returns>The task</returns>
public async Task Pause(string? duration)
{
var query = string.IsNullOrWhiteSpace(duration) ? "" : $"?duration={Uri.EscapeDataString(duration)}";
var response = await client.PostAsync($"serverstate/pause{query}", null);
await EnsureSuccessStatusCodeWithParsing(response);
}
/// <summary>
/// Resumes the server
/// </summary>
/// <returns>The task</returns>
public async Task Resume()
{
var response = await client.PostAsync($"serverstate/resume", null);
await EnsureSuccessStatusCodeWithParsing(response);
}
/// <summary>
/// Lists the backups configured on the server
/// </summary>
/// <returns>The backups</returns>
public async Task<IEnumerable<BackupEntry>> ListBackups()
{
var response = await client.GetAsync("backups");
await EnsureSuccessStatusCodeWithParsing(response);
return (JsonSerializer.Deserialize<IEnumerable<ResponseBackupEntry>>(await response.Content.ReadAsStringAsync())
?? throw new UserReportedException("Failed to parse response"))
.Select(x => x.ToBackupEntry())
.ToArray();
}
/// <summary>
/// Gets a backup by ID
/// </summary>
/// <param name="backupId">The ID of the backup</param>
/// <returns>The backup</returns>
public async Task<BackupEntry> GetBackup(string backupId)
{
var response = await client.GetAsync($"backup/{Uri.EscapeDataString(backupId)}");
await EnsureSuccessStatusCodeWithParsing(response);
return (JsonSerializer.Deserialize<ResponseBackupEntry>(await response.Content.ReadAsStringAsync())
?? throw new UserReportedException("Failed to parse response"))
.ToBackupEntry();
}
/// <summary>
/// Runs a backup
/// </summary>
/// <param name="backupId">The ID of the backup</param>
/// <returns>The task</returns>
public async Task RunBackup(string backupId)
{
var response = await client.PostAsync($"backup/{Uri.EscapeDataString(backupId)}/run", null);
await EnsureSuccessStatusCodeWithParsing(response);
}
/// <summary>
/// Lists the active tasks
/// </summary>
/// <returns>The tasks</returns>
public async Task<IEnumerable<TaskEntry>> ListTasks()
{
var response = await client.GetAsync("tasks");
await EnsureSuccessStatusCodeWithParsing(response);
return JsonSerializer.Deserialize<IEnumerable<TaskEntry>>(await response.Content.ReadAsStringAsync())
?? throw new InvalidOperationException("Failed to parse response");
}
/// <summary>
/// Stops a task
/// </summary>
/// <param name="taskId">The ID of the task</param>
/// <param name="level">The level to stop at</param>
/// <returns>The task</returns>
public async Task StopTask(string taskId, StopLevel level = StopLevel.AfterCurrentFile)
{
var levelString = level switch
{
StopLevel.AfterCurrentFile => "stopaftercurrentfile",
StopLevel.StopNow => "stopnow",
StopLevel.Abort => "abort",
_ => throw new ArgumentOutOfRangeException(nameof(level)),
};
var response = await client.PostAsync($"task/{Uri.EscapeDataString(taskId)}/{levelString}", null);
await EnsureSuccessStatusCodeWithParsing(response);
}
/// <summary>
/// Logs out of the server
/// </summary>
/// <param name="settings">The settings to use</param>
/// <returns>The task</returns>
public async Task Logout(Settings settings)
{
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "auth/refresh/logout")
{
Headers = { { "Cookie", $"RefreshToken_{client.BaseAddress!.Port}={settings.RefreshToken}" } }
});
await EnsureSuccessStatusCodeWithParsing(response);
(settings with { RefreshToken = null }).Save();
}
/// <summary>
/// Changes the server password
/// </summary>
/// <param name="newPassword">The new password to use</param>
/// <returns>The task</returns>
public async Task ChangePassword(string newPassword)
{
var response = await client.PutAsync("serversetting/server-passphrase", JsonContent.Create(newPassword));
await EnsureSuccessStatusCodeWithParsing(response);
}
/// <summary>
/// Imports a backup
/// </summary>
/// <param name="file">The file to import</param>
/// <param name="password">The password to use</param>
/// <param name="importMetadata">Whether to import metadata</param>
/// <returns>The backup</returns>
public async Task<BackupEntry> ImportBackup(string file, string? password, bool importMetadata)
{
var payload = JsonContent.Create(new
{
config = Convert.ToBase64String(await File.ReadAllBytesAsync(file)),
import_metadata = importMetadata,
passphrase = password,
direct = true
});
var response = await client.PostAsync("backups/import", payload);
await EnsureSuccessStatusCodeWithParsing(response);
var json = JsonSerializer.Deserialize<Dictionary<string, string>>(await response.Content.ReadAsStringAsync())
?? throw new UserReportedException("Failed to parse response");
if (!json.TryGetValue("Id", out var id))
throw new UserReportedException("Added backup but failed to get from response");
return await GetBackup(id);
}
/// <summary>
/// The server error structure for JSON deserialization
/// </summary>
/// <param name="Error">The error message</param>
/// <param name="Code">The error code</param>
private sealed record ServerError(string Error, int Code);
/// <summary>
/// Ensures the response is successful or extracts an error message
/// </summary>
/// <param name="message">The message to check</param>
/// <returns>The task</returns>
private static async Task EnsureSuccessStatusCodeWithParsing(HttpResponseMessage? message)
{
if (message is null)
throw new UserReportedException("No response received");
if (message.IsSuccessStatusCode)
return;
var content = await message.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(content))
message.EnsureSuccessStatusCode();
ServerError? errMsg = null;
try
{
errMsg = JsonSerializer.Deserialize<ServerError>(content);
}
catch
{
}
if (errMsg is not null)
throw new UserReportedException($"Server error ({errMsg.Code}): {errMsg.Error}");
throw new UserReportedException($"Failed to parse response ({message.StatusCode}): {content}");
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Duplicati.CommandLine.ServerUtil.Implementation</AssemblyName>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
<DefaultNamespace>Duplicati.CommandLine.ServerUtil</DefaultNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Server\Duplicati.Server.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,41 @@
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Various helper methods.
/// </summary>
public static class HelperMethods
{
/// <summary>
/// Reads a password from the console.
/// </summary>
/// <param name="prompt">The prompt to display.</param>
public static string ReadPasswordFromConsole(string prompt)
{
Console.Write(prompt);
string password = "";
while (true)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
if (keyInfo.Key == ConsoleKey.Enter)
{
break;
}
if (keyInfo.Key == ConsoleKey.Backspace)
{
if (password.Length > 0)
{
password = password.Substring(0, password.Length - 1);
Console.Write("\b \b");
}
}
else if (keyInfo.KeyChar != '\u0000') // Only accept if the key maps to a Unicode character (e.g., ignore F1 or Home).
{
password += keyInfo.KeyChar;
Console.Write("*");
}
}
Console.WriteLine();
return password;
}
}
@@ -0,0 +1,62 @@
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Parsing;
using Duplicati.CommandLine.ServerUtil.Commands;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// The entry point of the application
/// </summary>
public static class Program
{
/// <summary>
/// Invokes the builder
/// </summary>
/// <param name="args"></param>
/// <returns>The return code</returns>
public static Task<int> Main(string[] args)
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref args, Library.AutoUpdater.PackageHelper.NamedExecutable.ServerUtil);
var rootCmd = new RootCommand("Server CLI tool for Duplicati")
{
Pause.Create(),
Resume.Create(),
ListBackups.Create(),
RunBackup.Create(),
Login.Create(),
ChangePassword.Create(),
Logout.Create(),
Import.Create(),
};
rootCmd = SettingsBinder.AddGlobalOptions(rootCmd);
return new CommandLineBuilder(rootCmd)
.UseDefaults()
.UseExceptionHandler((ex, context) =>
{
if (ex is UserReportedException ure)
{
Console.WriteLine(ure.Message);
context.ExitCode = 2;
}
else
{
Console.WriteLine(ex.ToString());
context.ExitCode = 1;
}
})
.AddMiddleware(async (context, next) =>
{
// Inject settings with custom binder
if (context.ParseResult.CommandResult?.Command is Command cmd)
context.BindingContext.AddService(_ => SettingsBinder.GetSettings(context.BindingContext));
await next(context);
})
.Build()
.InvokeAsync(args);
}
}
@@ -0,0 +1,124 @@
using System.Text.Json;
using Duplicati.Library.Encryption;
using Duplicati.Library.Main;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Settings instance for the server utility.
/// </summary>
/// <param name="Password">The commandline password</param>
/// <param name="RefreshToken">The saved refresh token</param>
/// <param name="HostUrl">The host url to connect to</param>
/// <param name="ServerDatafolder">The server datafolder for password-free connections</param>
/// <param name="SettingsFile">The settings file where data is loaded/saved</param>
/// <param name="Insecure">Whether to disable TLS/SSL certificate trust check</param>
public sealed record Settings(
string? Password,
string? RefreshToken,
Uri HostUrl,
string? ServerDatafolder,
string SettingsFile,
bool Insecure
)
{
/// <summary>
/// The JSON serialized settings for a single host
/// </summary>
/// <param name="RefreshToken">Encrypted refresh token</param>
/// <param name="HostUrl">The host url to connect to</param>
/// <param name="ServerDatafolder">The server datafolder, if any</param>
private sealed record PersistedSettings(
string? RefreshToken,
Uri HostUrl,
string? ServerDatafolder
);
private static string GetDefaultStorageFolder(string filename)
{
var folder = DatabaseLocator.GetDefaultStorageFolderWithDebugSupport(filename);
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
return folder;
}
/// <summary>
/// Loads the settings from the settings file
/// </summary>
/// <param name="password">The password to use</param>
/// <param name="hostUrl">The host URL to use</param>
/// <param name="serverDataFolder">The server data folder to use</param>
/// <param name="settingsFile">The settings file to use</param>
/// <param name="insecure">Whether to disable TLS/SSL certificate trust check</param>
/// <returns>The loaded settings</returns>
public static Settings Load(string? password, Uri? hostUrl, string? serverDataFolder, string settingsFile, bool insecure)
{
hostUrl ??= new Uri("http://localhost:8200");
if (string.IsNullOrWhiteSpace(serverDataFolder))
serverDataFolder = GetDefaultStorageFolder("Duplicati-server.sqlite");
if (!string.IsNullOrWhiteSpace(settingsFile) && !Path.IsPathRooted(settingsFile))
settingsFile = Path.Combine(GetDefaultStorageFolder(settingsFile), settingsFile);
var persistedSettings = LoadSettings(settingsFile)
.FirstOrDefault(x => x.HostUrl == hostUrl);
return new Settings(
password,
persistedSettings?.RefreshToken,
hostUrl,
serverDataFolder,
settingsFile,
insecure
);
}
/// <summary>
/// Saves the settings to the settings file
/// </summary>
public void Save()
{
if (!string.IsNullOrWhiteSpace(RefreshToken))
{
if (!EncryptedFieldHelper.HasValidDefaultKey)
Console.WriteLine("Warning: The encryption key is missing, saving login token without encryption");
else if (EncryptedFieldHelper.IsDefaultKeyBlacklisted)
Console.WriteLine("Warning: The current encryption key is blacklisted and cannot be used, saving login token without encryption");
}
File.WriteAllText(SettingsFile, JsonSerializer.Serialize(LoadSettings(SettingsFile)
.Where(x => x.HostUrl != HostUrl)
.Append(new PersistedSettings(RefreshToken, HostUrl, ServerDatafolder))
.Select(x => x with
{
RefreshToken = string.IsNullOrWhiteSpace(x.RefreshToken) || EncryptedFieldHelper.IsDefaultKeyBlacklisted || !EncryptedFieldHelper.HasValidDefaultKey
? x.RefreshToken
: EncryptedFieldHelper.Encrypt(x.RefreshToken)
})
));
}
/// <summary>
/// Gets a connection to the server
/// </summary>
/// <returns>The connection</returns>
public Task<Connection> GetConnection()
{
return Connection.Connect(this);
}
/// <summary>
/// Loads the settings from the settings file
/// </summary>
/// <param name="filename">The filename to load</param>
/// <returns>The loaded settings</returns>
private static List<PersistedSettings> LoadSettings(string filename)
{
if (File.Exists(filename))
return (JsonSerializer.Deserialize<List<PersistedSettings>>(File.ReadAllText(filename)) ?? [])
.Select(x => x with { RefreshToken = EncryptedFieldHelper.Decrypt(x.RefreshToken) })
.ToList();
return [];
}
}
@@ -0,0 +1,70 @@
using System.CommandLine;
using System.CommandLine.Binding;
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// Binds settings from command line options.
/// </summary>
public class SettingsBinder : BinderBase<Settings>
{
/// <summary>
/// The password option.
/// </summary>
public static readonly Option<string?> passwordOption = new Option<string?>("--password", description: "The password to use", getDefaultValue: () => null);
/// <summary>
/// The host URL option.
/// </summary>
public static readonly Option<Uri> hostUrlOption = new Option<Uri>("--hosturl", description: "The host URL to use", getDefaultValue: () => new Uri("http://localhost:8200"));
/// <summary>
/// The server datafolder option.
/// </summary>
public static readonly Option<DirectoryInfo?> serverDatafolderOption = new Option<DirectoryInfo?>("--server-datafolder", description: "The server datafolder to use for locating the database", getDefaultValue: () => null);
/// <summary>
/// The settings file option.
/// </summary>
public static readonly Option<FileInfo?> settingsFileOption = new Option<FileInfo?>("--settings-file", description: "The settings file to use", getDefaultValue: () => null);
/// <summary>
/// The settings file option.
/// </summary>
public static readonly Option<bool> insecureOption = new Option<bool>("--insecure", description: "Accepts any TLS/SSL certificate (dangerous)", getDefaultValue: () => false);
/// <summary>
/// Adds global options to the root command.
/// </summary>
/// <param name="rootCommand">The root command to add the options to.</param>
/// <returns>The root command with the options added.</returns>
public static RootCommand AddGlobalOptions(RootCommand rootCommand)
{
rootCommand.AddGlobalOption(passwordOption);
rootCommand.AddGlobalOption(hostUrlOption);
rootCommand.AddGlobalOption(serverDatafolderOption);
rootCommand.AddGlobalOption(settingsFileOption);
rootCommand.AddGlobalOption(insecureOption);
return rootCommand;
}
/// <summary>
/// Gets the settings instance from the binding context.
/// </summary>
/// <param name="bindingContext">The binding context to get the settings from.</param>
/// <returns>The settings instance.</returns>
public static Settings GetSettings(BindingContext bindingContext) =>
Settings.Load(
bindingContext.ParseResult.GetValueForOption(passwordOption),
bindingContext.ParseResult.GetValueForOption(hostUrlOption),
bindingContext.ParseResult.GetValueForOption(serverDatafolderOption)?.FullName,
bindingContext.ParseResult.GetValueForOption(settingsFileOption)?.FullName ?? "settings.json",
bindingContext.ParseResult.GetValueForOption(insecureOption)
);
/// <summary>
/// Gets the bound value.
/// </summary>
/// <param name="bindingContext">The binding context to get the value from.</param>
/// <returns>The bound value.</returns>
protected override Settings GetBoundValue(BindingContext bindingContext) =>
GetSettings(bindingContext);
}
@@ -0,0 +1,9 @@
namespace Duplicati.CommandLine.ServerUtil;
/// <summary>
/// An exception that should be reported to the user.
/// </summary>
/// <param name="message">The message of the exception</param>
/// <param name="innerException">The inner exception</param>
[Serializable]
public class UserReportedException(string message, Exception? innerException = null) : Exception(message, innerException);
@@ -52,6 +52,7 @@ namespace Duplicati.GUI.TrayIcon
{
m_runnerException = ex;
Duplicati.Server.Program.ServerStartedEvent.Set();
Duplicati.Server.Program.ApplicationExitEvent.Set();
}
finally
{
@@ -73,13 +74,13 @@ namespace Duplicati.GUI.TrayIcon
if (!Duplicati.Server.Program.ServerStartedEvent.WaitOne(TimeSpan.FromSeconds(100), true))
{
if (m_runnerException != null)
throw m_runnerException;
throw new Duplicati.Library.Interface.UserInformationException("Server crashed on startup", "HostedStartupErrorCrash", m_runnerException);
else
throw new Duplicati.Library.Interface.UserInformationException("Hosted server startup timed out", "HostedStartupError");
}
if (m_runnerException != null)
throw m_runnerException;
throw new Duplicati.Library.Interface.UserInformationException("Server crashed on startup", "HostedStartupErrorCrash", m_runnerException);
}
public void Dispose()
@@ -58,20 +58,11 @@ namespace Duplicati.GUI.TrayIcon
};
private record ServerStatusImpl(
Tuple<long, string> ActiveTask,
LiveControlState ProgramState,
IList<Tuple<long, string>> SchedulerQueueIds,
bool HasWarning,
bool HasError,
SuggestedStatusIcon SuggestedStatusIcon,
DateTime EstimatedPauseEnd,
long LastEventID,
long LastDataUpdateID,
long LastNotificationUpdateID,
string UpdatedVersion,
string UpdateDownloadLink,
UpdatePollerStates UpdaterState,
double UpdateDownloadProgress
long LastNotificationUpdateID
) : IServerStatus;
private record NotificationImpl(
@@ -288,7 +279,7 @@ namespace Duplicati.GUI.TrayIcon
private T PerformRequest<T>(string method, string urlfragment, string body, TimeSpan? timeout)
{
if (string.IsNullOrWhiteSpace(m_accesstoken))
if (string.IsNullOrWhiteSpace(m_accesstoken) && !urlfragment.StartsWith("/auth/"))
ObtainAccessToken();
var hasTriedPassword = false;
@@ -382,7 +373,7 @@ namespace Duplicati.GUI.TrayIcon
string signinjwt = null;
// If we host the server, issue the token from the service
if (FIXMEGlobal.IsServerStarted || m_passwordSource == Program.PasswordSource.HostedServer)
if (FIXMEGlobal.IsServerStarted && m_passwordSource == Program.PasswordSource.HostedServer)
signinjwt = FIXMEGlobal.Provider.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("trayicon");
// If we have database access, grab the issuer key from the db and issue a token
@@ -18,28 +18,16 @@
// 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 Duplicati.Server.Serialization;
namespace Duplicati.Server.Serialization.Interface
namespace Duplicati.GUI.TrayIcon
{
public interface IServerStatus
{
Tuple<long, string> ActiveTask { get; }
LiveControlState ProgramState { get; }
IList<Tuple<long,string>> SchedulerQueueIds { get; }
bool HasWarning { get; }
bool HasError { get; }
SuggestedStatusIcon SuggestedStatusIcon { get; }
DateTime EstimatedPauseEnd { get; }
long LastEventID { get; }
long LastDataUpdateID { get; }
long LastNotificationUpdateID { get; }
string UpdatedVersion { get; }
string UpdateDownloadLink { get; }
UpdatePollerStates UpdaterState { get; }
double UpdateDownloadProgress { get; }
}
}
+30 -11
View File
@@ -21,6 +21,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Duplicati.Library.Interface;
using Duplicati.Server;
@@ -61,6 +62,7 @@ namespace Duplicati.GUI.TrayIcon
[STAThread]
public static int Main(string[] _args)
{
Library.AutoUpdater.PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, Library.AutoUpdater.PackageHelper.NamedExecutable.TrayIcon);
List<string> args = new List<string>(_args);
Dictionary<string, string> options = Library.Utility.CommandLineParser.ExtractOptions(args);
@@ -101,6 +103,8 @@ namespace Duplicati.GUI.TrayIcon
{
try
{
// Tell the hosted server it was started by the TrayIcon
Server.Program.Origin = "Tray icon";
hosted = new HostedInstanceKeeper(_args);
}
catch (Server.SingleInstance.MultipleInstanceException)
@@ -111,8 +115,6 @@ namespace Duplicati.GUI.TrayIcon
// We have a hosted server, if this is the first run,
// we should open the main page
openui = Server.Program.IsFirstRun || Server.Program.ServerPortChanged;
// Tell the hosted server it was started by the TrayIcon
Server.Program.Origin = "Tray icon";
var scheme = "http";
@@ -127,22 +129,25 @@ namespace Duplicati.GUI.TrayIcon
}
else if (Library.Utility.Utility.ParseBoolOption(options, READCONFIGFROMDB_OPTION))
{
databaseConnection = Server.Program.GetDatabaseConnection(options);
if (databaseConnection != null)
if (File.Exists(Path.Combine(Server.Program.GetDataFolderPath(options), Server.Program.SERVER_DATABASE_FILENAME)))
{
disableTrayIconLogin = databaseConnection.ApplicationSettings.DisableTrayIconLogin;
databaseConnection = Server.Program.GetDatabaseConnection(options, true);
if (databaseConnection != null)
{
disableTrayIconLogin = databaseConnection.ApplicationSettings.DisableTrayIconLogin;
var scheme = "http";
if (!string.IsNullOrEmpty(databaseConnection.ApplicationSettings.ServerSSLCertificate))
scheme = "https";
serverURL = new UriBuilder(serverURL)
{
Port = databaseConnection.ApplicationSettings.LastWebserverPort == -1 ? serverURL.Port : databaseConnection.ApplicationSettings.LastWebserverPort,
Scheme = scheme
}.Uri;
serverURL = new UriBuilder(serverURL)
{
Port = databaseConnection.ApplicationSettings.LastWebserverPort == -1 ? serverURL.Port : databaseConnection.ApplicationSettings.LastWebserverPort,
Scheme = scheme
}.Uri;
}
}
}
@@ -152,9 +157,23 @@ namespace Duplicati.GUI.TrayIcon
if (options.TryGetValue(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, out pwd))
password = pwd;
// Let the user specify the port, if they are not providing a hosturl
if (!options.ContainsKey(HOSTURL_OPTION) && options.TryGetValue(WebServerLoader.OPTION_PORT, out var portString) && int.TryParse(portString, out var port))
serverURL = new UriBuilder(serverURL) { Port = port }.Uri;
if (options.TryGetValue(HOSTURL_OPTION, out var url))
serverURL = new Uri(url);
if (string.IsNullOrWhiteSpace(password) && databaseConnection == null && hosted == null)
{
Console.WriteLine($@"
When running the TrayIcon without a hosted server, you must provide the server password via the option --{WebServerLoader.OPTION_WEBSERVICE_PASSWORD}=<password>.
If the TrayIcon instance has read access to the server database, you can also or use the option --{READCONFIGFROMDB_OPTION}, possibly with --server-datafolder=<path>.
No password provided, unable to connect to server, exiting");
return 1;
}
StartTray(_args, options, hosted, password);
return 0;
@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Duplicati.Library.Common;
using Duplicati.Library.Utility;
namespace Duplicati.Library.AutoUpdater
{
@@ -58,9 +53,13 @@ namespace Duplicati.Library.AutoUpdater
/// </summary>
Snapshots,
/// <summary>
/// The configuration importer
/// The server utility
/// </summary>
ConfigurationImporter
ServerUtil,
/// <summary>
/// The service wrapping the server
/// </summary>
Service
}
@@ -83,7 +82,8 @@ namespace Duplicati.Library.AutoUpdater
NamedExecutable.BackendTester => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.BackendTester.exe" : "duplicati-backend-tester",
NamedExecutable.SharpAESCrypt => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.SharpAESCrypt.exe" : "duplicati-aescrypt",
NamedExecutable.Snapshots => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.Snapshots.exe" : "duplicati-snapshots",
NamedExecutable.ConfigurationImporter => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.ConfigurationImporter.exe" : "duplicati-configuration-importer",
NamedExecutable.ServerUtil => OperatingSystem.IsWindows() ? "Duplicati.CommandLine.ServerUtil.exe" : "duplicati-server-util",
NamedExecutable.Service => OperatingSystem.IsWindows() ? "Duplicati.Service.exe" : "duplicati-service",
_ => throw new ArgumentException($"Named executable not known: {exe}", nameof(exe))
};
@@ -0,0 +1,314 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace Duplicati.Library.AutoUpdater;
/// <summary>
/// Utility class for loading the preload settings
/// </summary>
public static class PreloadSettingsLoader
{
/// <summary>
/// The environment variable to specify the preload settings file
/// </summary>
private const string PreloadSettingsEnvVar = "DUPLICATI_PRELOAD_SETTINGS";
/// <summary>
/// The environment variable to enable debug output for preload settings
/// </summary>
private const string PreloadSettingsDebugEnvVar = "DUPLICATI_PRELOAD_SETTINGS_DEBUG";
/// <summary>
/// The marker for any executable
/// </summary>
private const string AnyExecutableMarker = "*";
/// <summary>
/// Cached value for toggling debug code
/// </summary>
private static readonly bool PreloadDebug = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(PreloadSettingsDebugEnvVar));
/// <summary>
/// The preload paths to search for settings in.
/// Each path is checked and applied to obtain the final settings.
/// Later paths take precedence over earlier ones, so the env variable is most specific.
/// These are statically loaded so the preload cannot change the settings after startup.
/// </summary>
private static readonly string[] PreloadPaths = new string[]
{
// The default path for preload settings
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"Duplicati",
"preload.json"
),
// The path for preload settings with the install directory
Path.Combine(
Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ?? "",
"preload.json"
),
// The path for preload settings specified with an environment variable
Environment.GetEnvironmentVariable(PreloadSettingsEnvVar) ?? "",
}
.Where(x => !string.IsNullOrEmpty(x))
.ToArray();
/// <summary>
/// Configures the preload settings for the given executable
/// </summary>
/// <param name="arguments">The source commandline arguments</param>
/// <param name="executable">The executable to match</param>
public static void ConfigurePreloadSettings(ref string[] arguments, PackageHelper.NamedExecutable executable)
=> ConfigurePreloadSettings(ref arguments, executable, out _);
/// <summary>
/// Configures the preload settings for the given executable
/// </summary>
/// <param name="arguments">The source commandline arguments</param>
/// <param name="executable">The executable to match</param>
/// <param name="dbsettings">The database settings</param>
public static void ConfigurePreloadSettings(ref string[] arguments, PackageHelper.NamedExecutable executable, out Dictionary<string, string?> dbsettings)
{
var (env, args, db) = GetExecutableMergedSettings(executable);
dbsettings = db;
ApplyEnvironmentVariables(env);
ApplyCommandLineArguments(ref arguments, args);
}
/// <summary>
/// Gets the argument name from the given argument
/// </summary>
/// <param name="arg">The argument to get the name from</param>
/// <returns>The argument name</returns>
private static string GetArgumentName(string arg)
=> arg.Split('=', 2)[0];
/// <summary>
/// Gets the merged settings for the given executable
/// </summary>
/// <param name="executable">The executable to get settings for</param>
/// <returns>The merged settings</returns>
private static (Dictionary<string, string> env, List<string> args, Dictionary<string, string?> db) GetExecutableMergedSettings(PackageHelper.NamedExecutable executable)
{
// Collect settings in generic and specific dictionaries
// The executable-specific settings take precedence over the generic ones,
// but the loading order is used so that the most specific file is loaded last
var env_generic = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var env_specific = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var args_generic = new List<string>();
var args_specific = new List<string>();
var db_generic = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var db_specific = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var exename = MapExecutableName(executable);
void MergeDicts(Dictionary<string, string?> target, Dictionary<string, string?>? source)
{
if (source != null)
foreach (var kvp in source)
target[kvp.Key] = kvp.Value;
}
foreach (var path in PreloadPaths)
{
if (!Path.IsPathRooted(path))
{
if (PreloadDebug)
Console.WriteLine($"Preload settings path is not rooted, ignoring: {path}");
continue;
}
if (!File.Exists(path))
{
if (PreloadDebug)
Console.WriteLine($"Preload settings file does not exist, ignoring: {path}");
continue;
}
var settings = LoadSettings(path);
if (settings == null)
continue;
if (settings.db != null)
{
if (settings.db.TryGetValue(AnyExecutableMarker, out var entry))
MergeDicts(db_generic, entry);
if (exename != AnyExecutableMarker && settings.db.TryGetValue(exename, out entry))
MergeDicts(db_specific, entry);
}
if (settings.env != null)
{
if (settings.env.TryGetValue(AnyExecutableMarker, out var entry))
MergeDicts(env_generic, entry);
if (exename != AnyExecutableMarker && settings.env.TryGetValue(exename, out entry))
MergeDicts(env_specific, entry);
}
if (settings.args != null)
{
if (settings.args.TryGetValue(AnyExecutableMarker, out var entry))
args_generic.AddRange(entry ?? []);
if (exename != AnyExecutableMarker && settings.args.TryGetValue(exename, out entry))
args_specific.AddRange(entry ?? []);
}
}
// Merge specific settings into generic ones
foreach (var kvp in db_specific)
db_generic[kvp.Key] = kvp.Value;
foreach (var kvp in env_specific)
env_generic[kvp.Key] = kvp.Value;
args_generic.AddRange(args_specific);
// Remove duplicates from the arguments, preserve order
var mapped = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var args = new List<string>();
foreach (var value in args_generic)
{
var argname = GetArgumentName(value);
if (mapped.TryGetValue(argname, out var index))
args.RemoveAt(index);
mapped[argname] = args.Count;
args.Add(value);
}
var env = env_generic.ToDictionary(x => x.Key, x => x.Value ?? "");
return (env, args, db_generic);
}
/// <summary>
/// Applies loaded environment variables, but does not overwrite existing ones
/// </summary>
/// <param name="env">The environment variables to apply</param>
private static void ApplyEnvironmentVariables(Dictionary<string, string> env)
{
var current = Environment.GetEnvironmentVariables();
foreach (var kvp in env)
if (!current.Contains(kvp.Key))
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value ?? "");
}
/// <summary>
/// Applies loaded commandline arguments, but does not overwrite existing ones
/// </summary>
/// <param name="arguments">The source commandline arguments</param>
/// <param name="args">The arguments to apply</param>
private static void ApplyCommandLineArguments(ref string[] arguments, List<string> args)
{
arguments ??= [];
var existing = new HashSet<string>(arguments.Select(GetArgumentName), StringComparer.OrdinalIgnoreCase);
var result = arguments.ToList();
foreach (var value in args)
if (!existing.Contains(GetArgumentName(value)))
result.Add(value);
arguments = result.ToArray();
}
/// <summary>
/// Loads the settings from the given path
/// </summary>
/// <param name="path">The path to load settings from</param>
/// <returns>The loaded settings, or null if an error occurred</returns>
private static PreloadSettingsRoot? LoadSettings(string path)
{
try
{
var result = JsonSerializer.Deserialize<PreloadSettingsRoot>(File.ReadAllText(path));
if (PreloadDebug)
{
if (result == null)
{
Console.WriteLine($"Loaded empty preload settings from {path}");
return null;
}
var jsData = JsonSerializer.Deserialize<JsonElement>(File.ReadAllText(path));
var unmatched_keys = jsData.EnumerateObject().Select(x => x.Name).Except(["db", "env", "args"]);
if (unmatched_keys.Any())
Console.WriteLine($"Unexpected key(s) in preload settings: {string.Join(", ", unmatched_keys)}");
Console.WriteLine($"Loaded preload settings from {path}");
var allowedSources = Enum.GetValues<PackageHelper.NamedExecutable>()
.Select(MapExecutableName)
.Append(AnyExecutableMarker)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var unmatched_db = result.db?.Keys.Where(x => !allowedSources.Contains(x)) ?? [];
var unmatched_env = result.env?.Keys.Where(x => !allowedSources.Contains(x)) ?? [];
var unmatched_args = result.args?.Keys.Where(x => !allowedSources.Contains(x)) ?? [];
if (unmatched_db.Any())
Console.WriteLine($"Found unknown executable name(s) in db preload settings: {string.Join(", ", unmatched_db)}");
if (unmatched_env.Any())
Console.WriteLine($"Found unknown executable name(s) in env preload settings: {string.Join(", ", unmatched_env)}");
if (unmatched_args.Any())
Console.WriteLine($"Found unknown executable name(s) in args preload settings: {string.Join(", ", unmatched_args)}");
}
return result;
}
catch (Exception ex)
{
// Logging is usually not set up at this point
if (PreloadDebug)
Console.WriteLine($"Failed to load preload settings from {path}: {ex}");
return null;
}
}
/// <summary>
/// Maps the executable name to the preload settings key
/// </summary>
/// <param name="exe">The executable to map</param>
/// <returns>The mapped name</returns>
private static string MapExecutableName(PackageHelper.NamedExecutable exe)
=> exe switch
{
PackageHelper.NamedExecutable.TrayIcon => "tray",
PackageHelper.NamedExecutable.CommandLine => "cli",
PackageHelper.NamedExecutable.AutoUpdater => "autoupdater",
PackageHelper.NamedExecutable.Server => "server",
PackageHelper.NamedExecutable.WindowsService => "winservice",
PackageHelper.NamedExecutable.BackendTool => "backendtool",
PackageHelper.NamedExecutable.RecoveryTool => "recoverytool",
PackageHelper.NamedExecutable.BackendTester => "backendtester",
PackageHelper.NamedExecutable.SharpAESCrypt => "aescrypt",
PackageHelper.NamedExecutable.Snapshots => "snapshots",
PackageHelper.NamedExecutable.ServerUtil => "serverutil",
PackageHelper.NamedExecutable.Service => "service",
_ => AnyExecutableMarker,
};
/// <summary>
/// JSON root object for preload settings
/// </summary>
/// <param name="db">The database settings</param>
/// <param name="env">The environment variables</param>
/// <param name="args">The executable settings</param>
private sealed record PreloadSettingsRoot(
Dictionary<string, Dictionary<string, string?>>? db,
Dictionary<string, Dictionary<string, string?>>? env,
Dictionary<string, List<string>>? args
);
}
+29 -12
View File
@@ -27,6 +27,8 @@ using Duplicati.Library.Utility;
using Duplicati.Library.Common;
using System.Diagnostics;
using System.Text.Json;
using System.Net.Http;
using System.Threading;
namespace Duplicati.Library.AutoUpdater
{
@@ -105,6 +107,16 @@ namespace Duplicati.Library.AutoUpdater
/// </summary>
public static UpdateInfo LastUpdateCheckVersion { get; private set; }
/// <summary>
/// The default timeout in seconds for download operations
/// </summary>
private const int DOWNLOAD_OPERATION_TIMEOUT_SECONDS = 3600;
/// <summary>
/// The default timeout in seconds for fast get version metadata operations
/// </summary>
private const int SHORT_OPERATION_TIMEOUT_SECONDS = 30;
/// <summary>
/// Performs static initialization of the update manager, populating the readonly fields of the manager
/// </summary>
@@ -316,10 +328,15 @@ namespace Duplicati.Library.AutoUpdater
using (var tmpfile = new Library.Utility.TempFile())
{
System.Net.WebClient wc = new System.Net.WebClient();
wc.Headers.Add(System.Net.HttpRequestHeader.UserAgent, string.Format("{0} v{1}{2}", APPNAME, SelfVersion.Version, string.IsNullOrWhiteSpace(InstallID) ? "" : " -" + InstallID));
wc.Headers.Add("X-Install-ID", InstallID);
wc.DownloadFile(url, tmpfile);
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add(System.Net.HttpRequestHeader.UserAgent.ToString(), string.Format("{0} v{1}{2}", APPNAME, SelfVersion.Version, string.IsNullOrWhiteSpace(InstallID) ? "" : " -" + InstallID));
request.Headers.Add("X-Install-ID", InstallID);
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(SHORT_OPERATION_TIMEOUT_SECONDS));
HttpClientHelper.DefaultClient.DownloadFile(request, tmpfile, null, timeoutToken.Token).ConfigureAwait(false).GetAwaiter().GetResult();
using (var fs = System.IO.File.OpenRead(tmpfile))
{
@@ -439,15 +456,15 @@ namespace Duplicati.Library.AutoUpdater
if (progress != null)
cb = (s) => { progress(Math.Min(1.0, Math.Max(0.0, (double)s / package.Length))); };
var wreq = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url);
wreq.UserAgent = string.Format("{0} v{1}", APPNAME, SelfVersion.Version);
wreq.Headers.Add("X-Install-ID", InstallID);
using var request = new HttpRequestMessage(HttpMethod.Get, url);
var areq = new Duplicati.Library.Utility.AsyncHttpRequest(wreq);
using (var resp = areq.GetResponse())
using (var rss = areq.GetResponseStream())
using (var pgs = new Duplicati.Library.Utility.ProgressReportingStream(rss, cb))
Duplicati.Library.Utility.Utility.CopyStream(pgs, tempfile);
request.Headers.Add(System.Net.HttpRequestHeader.UserAgent.ToString(), string.Format("{0} v{1}", APPNAME, SelfVersion.Version));
request.Headers.Add("X-Install-ID", InstallID);
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(DOWNLOAD_OPERATION_TIMEOUT_SECONDS));
HttpClientHelper.DefaultClient.DownloadFile(request, tempfile, cb, timeoutToken.Token).ConfigureAwait(false).GetAwaiter().GetResult();
var sha256 = System.Security.Cryptography.SHA256.Create();
var md5 = System.Security.Cryptography.MD5.Create();
@@ -11,7 +11,7 @@ namespace Duplicati.Library.Backend.Strings
public static string OSSAccessKeySecretDescriptionLong { get { return LC.L(@"Access Key Secret is the key used by the user to encrypt signature strings and by OSS to verify these signature strings."); } }
public static string OSSAccessKeySecretDescriptionShort { get { return LC.L(@"Access Key Secret"); } }
public static string OSSBucketNameDescriptionLong { get { return LC.L(@"A storage space is a container used to store objects (Object), and all objects must belong to a specific storage space."); } }
public static string OSSBucketNameDescriptionShort { get { return LC.L(@"Bucket Name"); } }
public static string OSSBucketNameDescriptionShort { get { return LC.L(@"Bucket name"); } }
public static string OSSRegionDescriptionLong { get { return LC.L(@"Region indicates the physical location of the OSS data center."); } }
public static string OSSRegionDescriptionShort { get { return LC.L(@"Region"); } }
public static string OSSEndpointDescriptionLong { get { return LC.L(@"Endpoint refers to the domain name through which OSS provides external services."); } }
@@ -30,9 +30,9 @@ namespace Duplicati.Library.Backend.AlternativeFTP
public static string Description { get { return LC.L(@"This backend can read and write data to an FTP based backend using an alternative FTP client. Allowed formats are ""aftp://hostname/folder"" and ""aftp://username:password@hostname/folder""."); } }
public static string DisplayName { get { return LC.L(@"Alternative FTP"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionLogToConsoleLong { get { return LC.L(@"Use this option to log FTP dialog to terminal console for debugging purposes."); } }
public static string DescriptionLogToConsoleShort { get { return LC.L(@"Log FTP dialog to terminal console"); } }
public static string DescriptionLogPrivateInfoToConsoleLong { get { return LC.L(@"Use this option to log FTP PRIVATE info (username, password) to console for debugging purposes (DO NOT POST THIS TO THE INTERNET!)"); } }
@@ -35,8 +35,8 @@ namespace Duplicati.Library.Backend.AzureBlob.Strings {
public static string SasTokenDescriptionShort { get { return LC.L(@"The SAS token"); } }
public static string NoAccessKeyOrSasToken { get { return LC.L(@"No Azure access key or SAS token given"); } }
public static string AuthPasswordDescriptionLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string AuthUsernameDescriptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
}
}
@@ -24,14 +24,14 @@ namespace Duplicati.Library.Backend.Strings {
internal static class B2 {
public static string Description { get { return LC.L(@"This backend can read and write data to the Backblaze B2 Cloud Storage. Allowed format is ""b2://bucketname/prefix""."); } }
public static string DisplayName { get { return LC.L(@"B2 Cloud Storage"); } }
public static string B2applicationkeyDescriptionLong { get { return LC.L(@"B2 Cloud Storage Application Key can be obtained after logging into your Backblaze account. This can also be supplied through the ""auth-password"" property."); } }
public static string B2applicationkeyDescriptionLong { get { return LC.L(@"B2 Cloud Storage Application Key can be obtained after logging into your Backblaze account. This can also be supplied through the option --{0}.", "auth-password"); } }
public static string B2applicationkeyDescriptionShort { get { return LC.L(@"B2 Cloud Storage Application Key"); } }
public static string B2accountidDescriptionLong { get { return LC.L(@"B2 Cloud Storage Account ID can be obtained after logging into your Backblaze account. This can also be supplied through the ""auth-username"" property."); } }
public static string B2accountidDescriptionLong { get { return LC.L(@"B2 Cloud Storage Account ID can be obtained after logging into your Backblaze account. This can also be supplied through the option --{0}.", "auth-username"); } }
public static string B2accountidDescriptionShort { get { return LC.L(@"B2 Cloud Storage Account ID"); } }
public static string AuthPasswordDescriptionLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string AuthUsernameDescriptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string NoB2KeyError { get { return LC.L(@"No B2 Cloud Storage Application Key given"); } }
public static string NoB2UserIDError { get { return LC.L(@"No B2 Cloud Storage Account ID given"); } }
public static string B2createbuckettypeDescriptionLong { get { return LC.L(@"By default, a private bucket is created. Use this option to set the bucket type. Refer to the B2 documentation for allowed types."); } }
@@ -26,15 +26,15 @@ namespace Duplicati.Library.Backend.Strings {
public static string DescriptionAuthenticationURLLong_v2(string optionname) { return LC.L(@"CloudFiles use different servers for authentication based on where the account resides. Use this option to set an alternate authentication URL. This option overrides --{0}.", optionname); }
public static string DescriptionAuthenticationURLShort { get { return LC.L(@"Provide another authentication URL"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionPasswordLong { get { return LC.L(@"The API Access Key used to authenticate with CloudFiles."); } }
public static string DescriptionPasswordShort { get { return LC.L(@"Supplies the access key used to connect to the server"); } }
public static string DescriptionPasswordShort { get { return LC.L(@"Supply the access key used to connect to the server"); } }
public static string DescriptionUKAccountLong(string optionname, string optionvalue) { return LC.L(@"Duplicati will assume that the credentials given are for a US account. Use this option if the account is a UK based account. Note that this is equivalent to setting --{0}={1}.", optionname, optionvalue); }
public static string DescriptionUKAccountShort { get { return LC.L(@"Use a UK account"); } }
public static string DescriptionUsernameLong { get { return LC.L(@"The username used to authenticate with CloudFiles."); } }
public static string DescriptionUsernameShort { get { return LC.L(@"Supplies the username used to authenticate with CloudFiles"); } }
public static string DescriptionUsernameShort { get { return LC.L(@"Supply the username used to authenticate with CloudFiles"); } }
public static string ETagVerificationError { get { return LC.L(@"MD5 Hash (ETag) verification failed"); } }
public static string FileDeleteError { get { return LC.L(@"Failed to delete file"); } }
public static string FileUploadError { get { return LC.L(@"Failed to upload file"); } }
+5 -5
View File
@@ -27,15 +27,15 @@ namespace Duplicati.Library.Backend.Strings {
public static string Description { get { return LC.L(@"This backend can read and write data to an FTP based backend. Allowed formats are ""ftp://hostname/folder"" and ""ftp://username:password@hostname/folder""."); } }
public static string DisplayName { get { return LC.L(@"FTP"); } }
public static string DescriptionFTPActiveLong { get { return LC.L(@"Activate this option to make the FTP connection in active mode. Even if the option --{0} is also set, the connection will be made in active mode.", "ftp-passive"); } }
public static string DescriptionFTPActiveShort { get { return LC.L(@"Toggles the FTP connections method"); } }
public static string DescriptionFTPActiveShort { get { return LC.L(@"Toggle the FTP connections method"); } }
public static string DescriptionFTPPassiveLong { get { return LC.L(@"Activate this option to make the FTP connection in passive mode, which works better with some firewalls. If the option --{0} is set, this option is ignored.", "ftp-regular"); } }
public static string DescriptionFTPPassiveShort { get { return LC.L(@"Toggles the FTP connections method"); } }
public static string DescriptionFTPPassiveShort { get { return LC.L(@"Toggle the FTP connections method"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionUseSSLLong { get { return LC.L(@"Use this option to communicate using Secure Socket Layer (SSL) over ftp (ftps)."); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instructs Duplicati to use an SSL (ftps) connection"); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (ftps) connection"); } }
public static string DescriptionDisableUploadVerifyLong { get { return LC.L(@"To protect against network failures, every upload will be attempted verified. Use this option to disable this verification to make the upload faster but less reliable."); } }
public static string DescriptionDisableUploadVerifyShort { get { return LC.L(@"Disable upload verification"); } }
public static string MissingFolderError(string foldername, string message) { return LC.L(@"The folder {0} was not found, message: {1}", foldername, message); }
+2 -2
View File
@@ -28,9 +28,9 @@ namespace Duplicati.Library.Backend.Strings {
public static string AlternateTargetPathsLong(string optionname, char pathseparator) { return LC.L(@"This option allows multiple targets to be specified. The primary target path is placed before the list of paths supplied with this option. Before starting the backup, each folder in the list is checked for existence and optionally the presence of the marker file supplied by --{0}. The first existing path that optionally contains the marker file is then used as the destination. Multiple destinations are separated with a ""{1}"". On Windows, the path may be a UNC path, and the drive letter may be substituted with an asterisk (*), e.g: ""*:\backup"", which will examine all drive letters. If a username and password is supplied, the same credentials are used for all destinations.", optionname, pathseparator); }
public static string AlternateTargetPathsShort { get { return LC.L(@"A list of secondary target paths"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string FolderMissingError(string foldername) { return LC.L(@"The folder {0} does not exist", foldername); }
public static string NoDestinationWithMarkerFileError(string markername, string[] folders) { return LC.L(@"The marker file ""{0}"" was not found in any of the examined destinations: {1}", markername, string.Join(", ", folders)); }
public static string UseMoveForPutLong { get { return LC.L(@"When storing the file, the standard operation is to copy the file and delete the original. This sequence ensures that the operation can be retried if something goes wrong. Activating this option may cause the retry operation to fail. This option has no effect unless the option --{0} is activated.", "disable-streaming-transfers"); } }
@@ -70,7 +70,7 @@ namespace Duplicati.Library.Backend.GoogleServices
public string DisplayName { get { return LC.L("Google Cloud Storage configuration module"); } }
public string Description { get { return LC.L("Exposes Google Cloud Storage configuration as a web module"); } }
public string Description { get { return LC.L("Expose Google Cloud Storage configuration as a web module"); } }
public System.Collections.Generic.IList<ICommandLineArgument> SupportedCommands
@@ -78,7 +78,7 @@ namespace Duplicati.Library.Backend.GoogleServices
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provides different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
]);
}
@@ -32,12 +32,12 @@ namespace Duplicati.Library.Backend.Strings
public static string AuthidShort { get { return LC.L(@"The authorization code"); } }
public static string LocationDescriptionLong(string regions) { return LC.L(@"This option is only used when creating new buckets. Use this option to change what region the data is stored in. Charges vary with bucket location. Known bucket locations:
{0}", regions); }
public static string LocationDescriptionShort { get { return LC.L(@"Specifies location option for creating a bucket"); } }
public static string LocationDescriptionShort { get { return LC.L(@"Specify location option for creating a bucket"); } }
public static string StorageclassDescriptionLong(string classes) { return LC.L(@"This option is only used when creating new buckets. Use this option to change what storage type the bucket has. Charges and functionality vary with bucket storage class. Known storage classes:
{0}", classes); }
public static string StorageclassDescriptionShort { get { return LC.L(@"Specifies storage class for creating a bucket"); } }
public static string StorageclassDescriptionShort { get { return LC.L(@"Specify storage class for creating a bucket"); } }
public static string ProjectDescriptionLong { get { return LC.L(@"This option is only used when creating new buckets. Use this option to supply the project ID that the bucket is attached to. The project determines where usage charges are applied."); } }
public static string ProjectDescriptionShort { get { return LC.L(@"Specifies project for creating a bucket"); } }
public static string ProjectDescriptionShort { get { return LC.L(@"Specify project for creating a bucket"); } }
}
internal static class GoogleDrive {
@@ -23,9 +23,9 @@ namespace Duplicati.Library.Backend.Strings {
internal static class Idrivee2Backend {
public static string Description { get { return LC.L(@"This backend can read and write data to IDrive e2."); } }
public static string DisplayName { get { return LC.L(@"IDrive e2"); } }
public static string KeySecretDescriptionLong { get { return LC.L(@"Access Key Secret can be obtained after logging into your IDrive e2 account. This can also be supplied through the ""auth-password"" property."); } }
public static string KeySecretDescriptionLong { get { return LC.L(@"Access Key Secret can be obtained after logging into your IDrive e2 account. This can also be supplied through the option --{0}.", "auth-password"); } }
public static string KeySecretDescriptionShort { get { return LC.L(@"Access Key Secret"); } }
public static string KeyIDDescriptionLong { get { return LC.L(@"Access Key ID can be obtained after logging into your IDrive e2 account. This can also be supplied through the ""auth-username"" property."); } }
public static string KeyIDDescriptionLong { get { return LC.L(@"Access Key ID can be obtained after logging into your IDrive e2 account. This can also be supplied through the option --{0}.", "auth-username"); } }
public static string KeyIDDescriptionShort { get { return LC.L(@"Access Key ID"); } }
public static string BucketNameOrPathDescriptionLong { get { return LC.L(@"The ""Bucket Name or Complete Path"" is name of target bucket or complete of a folder inside the bucket."); } }
@@ -31,9 +31,9 @@ namespace Duplicati.Library.Backend.Strings {
public static string IllegalMountPoint { get { return LC.L(@"Illegal mount point given."); } }
public static string FileUploadError { get { return LC.L(@"Failed to upload file"); } }
public static string DescriptionDeviceLong(string mountPointOption) { return LC.L(@"The backup device to use. Will be created if not already exists. You can manage your devices from the backup panel in the Jottacloud web interface. When you specify a custom device you should also specify the mount point to use on this device with the ""{0}"" option.", mountPointOption); }
public static string DescriptionDeviceShort { get { return LC.L(@"Supplies the backup device to use"); } }
public static string DescriptionDeviceShort { get { return LC.L(@"Supply the backup device to use"); } }
public static string DescriptionMountPointLong(string deviceOptionName) { return LC.L(@"The mount point to use on the server. The default is ""Archive"" for using the built-in archive mount point. Set this option to ""Sync"" to use the built-in synchronization mount point instead, or if you have specified a custom device with option ""{0}"" you are free to name the mount point as you like.", deviceOptionName); }
public static string DescriptionMountPointShort { get { return LC.L(@"Supplies the mount point to use on the server"); } }
public static string DescriptionMountPointShort { get { return LC.L(@"Supply the mount point to use on the server"); } }
public static string ThreadsLong { get { return LC.L(@"Number of threads for restore operations. In some cases the download rate is limited to 18.5 Mbps per stream. Use multiple threads to increase throughput."); } }
public static string ThreadsShort { get { return LC.L(@"Number of threads for restore operations"); } }
public static string ChunksizeLong { get { return LC.L(@"The chunk size for simultaneous downloading. These chunks will be held in memory, so keep it as low as possible."); } }
+4 -3
View File
@@ -1,5 +1,6 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
// Copyright (C) 2024, Suguru Hirahara
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
@@ -24,10 +25,10 @@ namespace Duplicati.Library.Backend.Strings {
public static string Description { get { return LC.L(@"This backend can read and write data to Mega.co.nz. Allowed format is ""mega://folder/subfolder""."); } }
public static string DisplayName { get { return LC.L(@"mega.nz"); } }
public static string AuthPasswordDescriptionLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string AuthUsernameDescriptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string AuthTwoFactorKeyDescriptionLong { get { return LC.L(@"For accounts with two-factor authentication enabled, this is the shared secret used to generate the two-factor TOTP codes."); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string AuthTwoFactorKeyDescriptionLong { get { return LC.L(@"For accounts with two-factor authentication enabled, set the shared secret used to generate the two-factor TOTP codes."); } }
public static string AuthTwoFactorKeyDescriptionShort { get { return LC.L(@"The shared secret used to generate two-factor TOTP codes"); } }
public static string NoPasswordError { get { return LC.L(@"No password given"); } }
public static string NoUsernameError { get { return LC.L(@"No username given"); } }
@@ -38,7 +38,7 @@ namespace Duplicati.Library.Backend.Strings
internal static class OneDriveV2
{
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Stores files in Microsoft OneDrive or Microsoft OneDrive for Business via the Microsoft Graph API. Usage of this backend requires that you agree to the terms in {0} ({1}) and {2} ({3}).", mssadescription, mssalink, msopdescription, msoplink); }
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Store files in Microsoft OneDrive or Microsoft OneDrive for Business via the Microsoft Graph API. Usage of this backend requires that you agree to the terms in {0} ({1}) and {2} ({3}).", mssadescription, mssalink, msopdescription, msoplink); }
public static string DisplayName { get { return LC.L(@"Microsoft OneDrive v2"); } }
public static string DriveIdLong(string defaultDrive) { return LC.L(@"ID of the drive to store data in. If no drive is specified, the default OneDrive or OneDrive for Business drive will be used via '{0}'.", defaultDrive); }
public static string DriveIdShort { get { return LC.L(@"Optional ID of the drive"); } }
@@ -46,7 +46,7 @@ namespace Duplicati.Library.Backend.Strings
internal static class SharePointV2
{
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Stores files in a Microsoft SharePoint site via the Microsoft Graph API. Usage of this backend requires that you agree to the terms in {0} ({1}) and {2} ({3}).", mssadescription, mssalink, msopdescription, msoplink); }
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Store files in a Microsoft SharePoint site via the Microsoft Graph API. Usage of this backend requires that you agree to the terms in {0} ({1}) and {2} ({3}).", mssadescription, mssalink, msopdescription, msoplink); }
public static string DisplayName { get { return LC.L(@"Microsoft SharePoint v2"); } }
public static string SiteIdLong { get { return LC.L(@"ID of the site to store data in."); } }
public static string SiteIdShort { get { return LC.L(@"ID of the site"); } }
@@ -56,7 +56,7 @@ namespace Duplicati.Library.Backend.Strings
internal static class MicrosoftGroup
{
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Stores files in a Microsoft Office 365 Group via the Microsoft Graph API. Allowed formats are ""sharepoint://tenant.sharepoint.com/{{PathToWeb}}//{{Documents}}/subfolder"" (with ""//"" being optionally used to indicate the root document folder) and ""sharepoint://subfolder"" (in which case you must also explicitly specify the SharePoint site's ID via --{0}). Usage of this backend requires that you agree to the terms in {1} ({2}) and {3} ({4}).", "site-id", mssadescription, mssalink, msopdescription, msoplink); }
public static string Description(string mssadescription, string mssalink, string msopdescription, string msoplink) { return LC.L(@"Store files in a Microsoft Office 365 Group via the Microsoft Graph API. Allowed formats are ""sharepoint://tenant.sharepoint.com/{{PathToWeb}}//{{Documents}}/subfolder"" (with ""//"" being optionally used to indicate the root document folder) and ""sharepoint://subfolder"" (in which case you must also explicitly specify the SharePoint site's ID via --{0}). Usage of this backend requires that you agree to the terms in {1} ({2}) and {3} ({4}).", "site-id", mssadescription, mssalink, msopdescription, msoplink); }
public static string DisplayName { get { return LC.L(@"Microsoft Office 365 Group"); } }
public static string GroupIdLong { get { return LC.L(@"ID of the group to store data in."); } }
public static string GroupIdShort { get { return LC.L(@"ID of the group"); } }
@@ -66,7 +66,7 @@ namespace Duplicati.Library.Backend.OpenStack
public string DisplayName { get { return LC.L("OpenStack configuration module"); } }
public string Description { get { return LC.L("Exposes OpenStack configuration as a web module"); } }
public string Description { get { return LC.L("Expose OpenStack configuration as a web module"); } }
public System.Collections.Generic.IList<ICommandLineArgument> SupportedCommands
@@ -74,11 +74,10 @@ namespace Duplicati.Library.Backend.OpenStack
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provides different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
]);
}
}
}
}
@@ -28,21 +28,21 @@ namespace Duplicati.Library.Backend.Strings
public static string DisplayName { get { return LC.L(@"OpenStack Simple Storage"); } }
public static string MissingOptionError(string optionname) { return LC.L(@"Missing required option: {0}", optionname); }
public static string PasswordOptionLong(string tenantnameoption) { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD"". If the password is supplied, --{0} must also be set.", tenantnameoption); }
public static string PasswordOptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string PasswordOptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DomainnameOptionLong { get { return LC.L(@"The domain name of the user used to connect to the server."); } }
public static string DomainnameOptionShort { get { return LC.L(@"Supplies the domain used to connect to the server"); } }
public static string DomainnameOptionShort { get { return LC.L(@"Supply the domain used to connect to the server"); } }
public static string UsernameOptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string UsernameOptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string UsernameOptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string TenantnameOptionLong { get { return LC.L(@"The Tenant Name is commonly the paying user account name. This option must be supplied when authenticating with a password, but is not required when using an API key."); } }
public static string TenantnameOptionShort { get { return LC.L(@"Supplies the Tenant Name used to connect to the server"); } }
public static string TenantnameOptionShort { get { return LC.L(@"Supply the Tenant Name used to connect to the server"); } }
public static string ApikeyOptionLong { get { return LC.L(@"The API key can be used to connect without supplying a password and tenant ID with some providers."); } }
public static string ApikeyOptionShort { get { return LC.L(@"Supplies the API key used to connect to the server"); } }
public static string ApikeyOptionShort { get { return LC.L(@"Supply the API key used to connect to the server"); } }
public static string AuthuriOptionLong(string providers) { return LC.L(@"The authentication URL is used to authenticate the user and find the storage service. The URL commonly ends with ""/v2.0"". Known providers are: {0}{1}", System.Environment.NewLine, providers); }
public static string AuthuriOptionShort { get { return LC.L(@"Supplies the authentication URL"); } }
public static string AuthuriOptionShort { get { return LC.L(@"Supply the authentication URL"); } }
public static string VersionOptionLong { get { return LC.L(@"The keystone API version to use. Valid values are 'v2' and 'v3'."); } }
public static string VersionOptionShort { get { return LC.L(@"The keystone API version to use"); } }
public static string RegionOptionLong { get { return LC.L(@"This option is only used when creating a container, and is used to indicate where the container should be placed. Consult your provider for a list of valid regions, or leave empty for the default region."); } }
public static string RegionOptionShort { get { return LC.L(@"Supplies the region used for creating a container"); } }
public static string RegionOptionShort { get { return LC.L(@"Supply the region used for creating a container"); } }
}
}
+2 -2
View File
@@ -74,14 +74,14 @@ namespace Duplicati.Library.Backend
public string DisplayName { get { return LC.L("S3 configuration module"); } }
public string Description { get { return LC.L("Exposes S3 configuration as a web module"); } }
public string Description { get { return LC.L("Expose S3 configuration as a web module"); } }
public IList<ICommandLineArgument> SupportedCommands
{
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provides different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
]);
}
+2 -3
View File
@@ -70,7 +70,7 @@ namespace Duplicati.Library.Backend
public string DisplayName { get { return LC.L("S3 IAM support module"); } }
public string Description { get { return LC.L("Exposes S3 IAM manipulation as a web module"); } }
public string Description { get { return LC.L("Expose S3 IAM manipulation as a web module"); } }
public IList<ICommandLineArgument> SupportedCommands
@@ -78,7 +78,7 @@ namespace Duplicati.Library.Backend
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_OPERATION, CommandLineArgument.ArgumentType.Enumeration, LC.L("The operation to perform"), LC.L("Selects the operation to perform"), null, Enum.GetNames(typeof(Operation))),
new CommandLineArgument(KEY_OPERATION, CommandLineArgument.ArgumentType.Enumeration, LC.L("The operation to perform"), LC.L("Select the operation to perform"), null, Enum.GetNames(typeof(Operation))),
new CommandLineArgument(KEY_USERNAME, CommandLineArgument.ArgumentType.String, LC.L("The username"), LC.L("The Amazon Access Key ID")),
new CommandLineArgument(KEY_PASSWORD, CommandLineArgument.ArgumentType.String, LC.L("The password"), LC.L("The Amazon Secret Key")),
]);
@@ -235,4 +235,3 @@ namespace Duplicati.Library.Backend
}
}
}
+8 -8
View File
@@ -25,26 +25,26 @@ namespace Duplicati.Library.Backend.Strings
{
public static string Description_v2 { get { return LC.L(@"This backend can read and write data to an S3 compatible server. Allowed format is ""s3://bucketname/prefix""."); } }
public static string DisplayName { get { return LC.L(@"S3 compatible"); } }
public static string AMZKeyDescriptionLong { get { return LC.L(@"AWS Secret Access Key can be obtained after logging into your AWS account. This can also be supplied through the ""auth-password"" property."); } }
public static string AMZKeyDescriptionLong { get { return LC.L(@"AWS Secret Access Key can be obtained after logging into your AWS account. This can also be supplied through the option --{0}.", "auth-password"); } }
public static string AMZKeyDescriptionShort { get { return LC.L(@"AWS Secret Access Key"); } }
public static string AMZUserIDDescriptionLong { get { return LC.L(@"AWS Access Key ID can be obtained after logging into your AWS account. This can also be supplied through the ""auth-username"" property."); } }
public static string AMZUserIDDescriptionLong { get { return LC.L(@"AWS Access Key ID can be obtained after logging into your AWS account. This can also be supplied through the option --{0}.", "auth-username"); } }
public static string AMZUserIDDescriptionShort { get { return LC.L(@"AWS Access Key ID"); } }
public static string AuthPasswordDescriptionLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string AuthUsernameDescriptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string NoAMZKeyError { get { return LC.L(@"No S3 secret key given"); } }
public static string NoAMZUserIDError { get { return LC.L(@"No S3 userID given"); } }
public static string S3LocationDescriptionLong(string regions) { return LC.L(@"This option is only used when creating new buckets. Use this option to change what region the data is stored in. Amazon charges slightly more for non-US buckets. Known bucket locations:
{0}", regions); }
public static string S3LocationDescriptionShort { get { return LC.L(@"Specifies S3 location constraints"); } }
public static string S3LocationDescriptionShort { get { return LC.L(@"Specify S3 location constraints"); } }
public static string S3ServerNameDescriptionLong(string providers) { return LC.L(@"Companies other than Amazon are now supporting the S3 API, meaning that this backend can read and write data to those providers as well. Use this option to set the hostname. Currently known providers are:
{0}", providers); }
public static string S3ServerNameDescriptionShort { get { return LC.L(@"Specifies an alternate S3 server name"); } }
public static string S3ServerNameDescriptionShort { get { return LC.L(@"Specify an alternate S3 server name"); } }
public static string S3ClientDescriptionLong { get { return LC.L(@"Set either to aws or minio. Then either the AWS SDK or Minio SDK will be used to communicate with S3 services."); } }
public static string S3ClientDescriptionShort { get { return LC.L(@"Specifies the S3 client library to use"); } }
public static string S3ClientDescriptionShort { get { return LC.L(@"Specify the S3 client library to use"); } }
public static string DescriptionUseSSLLong { get { return LC.L(@"Use this option to communicate using Secure Socket Layer (SSL) over http (https). Note that bucket names containing a period has problems with SSL connections."); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instructs Duplicati to use an SSL (https) connection"); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (https) connection"); } }
public static string DescriptionDisableChunkEncodingLong { get { return LC.L(@"This disables chunk encoding for the aws client, which is not supported by all S3 providers."); } }
public static string DescriptionDisableChunkEncodingShort { get { return LC.L(@"Disable chunk encoding (aws client only)"); } }
public static string S3StorageclassDescriptionLong { get { return LC.L(@"Use this option to specify a storage class. If this option is not used, the server will choose a default storage class."); } }
+12 -12
View File
@@ -46,21 +46,21 @@ namespace Duplicati.Library.Backend.Strings
public static string Description { get { return LC.L(@"This backend can read and write data to an SSH based backend, using SFTP. Allowed formats are ""ssh://hostname/folder"" and ""ssh://username:password@hostname/folder""."); } }
public static string DisplayName { get { return LC.L(@"SFTP (SSH)"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionFingerprintLong { get { return LC.L(@"The server fingerprint used for validation of server identity. Format is e.g. ""ssh-rsa 4096 11:22:33:44:55:66:77:88:99:00:11:22:33:44:55:66""."); } }
public static string DescriptionFingerprintShort { get { return LC.L(@"Supplies server fingerprint used for validation of server identity"); } }
public static string DescriptionFingerprintShort { get { return LC.L(@"Supply server fingerprint used for validation of server identity"); } }
public static string DescriptionAnyFingerprintLong { get { return LC.L(@"To guard against man-in-the-middle attacks, the server fingerprint is verified on connection. Use this option to disable host-key fingerprint verification. You should only use this option for testing."); } }
public static string DescriptionAnyFingerprintShort { get { return LC.L(@"Disables fingerprint validation"); } }
public static string DescriptionSshkeyfileLong { get { return LC.L(@"Points to a valid OpenSSH keyfile. If the file is encrypted, the password supplied is used to decrypt the keyfile. If this option is supplied, the password is not used to authenticate."); } }
public static string DescriptionSshkeyfileShort { get { return LC.L(@"Uses a SSH private key to authenticate"); } }
public static string DescriptionSshkeyLong(string urlprefix) { return LC.L(@"An url-encoded SSH private key. The private key must be prefixed with {0}. If the file is encrypted, the password supplied is used to decrypt the keyfile. If this option is supplied, the password is not used to authenticate.", urlprefix); }
public static string DescriptionSshkeyShort { get { return LC.L(@"Uses a SSH private key to authenticate"); } }
public static string DescriptionSshtimeoutLong { get { return LC.L(@"Use this option to manage the internal timeout for SSH operations. If this options is set to zero, the operations will not time out."); } }
public static string DescriptionSshtimeoutShort { get { return LC.L(@"Sets the operation timeout value"); } }
public static string DescriptionSshkeepaliveLong { get { return LC.L(@"This option can be used to enable the keep-alive interval for the SSH connection. If the connection is idle, aggressive firewalls might close the connection. Using keep-alive will keep the connection open in this scenario. If this value is set to zero, the keep-alive is disabled."); } }
public static string DescriptionSshkeepaliveShort { get { return LC.L(@"Sets a keepalive value"); } }
public static string DescriptionAnyFingerprintShort { get { return LC.L(@"Disable fingerprint validation"); } }
public static string DescriptionSshkeyfileLong { get { return LC.L(@"Point to a valid OpenSSH keyfile. If the file is encrypted, the password supplied is used to decrypt it. If the keyfile is specified, the password is not used to authenticate."); } }
public static string DescriptionSshkeyfileShort { get { return LC.L(@"Use a SSH private key to authenticate"); } }
public static string DescriptionSshkeyLong(string urlprefix) { return LC.L(@"An url-encoded SSH private key. The private key must be prefixed with {0}. If the key is encrypted, the password supplied is used to decrypt it. If the private key is specified, the password is not used to authenticate.", urlprefix); }
public static string DescriptionSshkeyShort { get { return LC.L(@"Use a SSH private key to authenticate"); } }
public static string DescriptionSshtimeoutLong { get { return LC.L(@"Use this option to manage the internal timeout for SSH operations. If the value is set to zero, the operations will not time out."); } }
public static string DescriptionSshtimeoutShort { get { return LC.L(@"Set the operation timeout value"); } }
public static string DescriptionSshkeepaliveLong { get { return LC.L(@"Use this option to enable the keep-alive interval for the SSH connection. If the connection is idle, aggressive firewalls might close the connection. Using keep-alive will keep the connection open in this scenario. If this value is set to zero, the keep-alive is disabled."); } }
public static string DescriptionSshkeepaliveShort { get { return LC.L(@"Set a keepalive value"); } }
public static string FolderNotFoundManagedError(string foldername, string message) { return LC.L(@"Unable to set folder to {0}, error message: {1}", foldername, message); }
public static string FingerprintNotMatchManagedError(string fingerprint) { return LC.L(@"Validation of server fingerprint failed. Server returned fingerprint ""{0}"". Cause of this message is either not correct configuration or Man-in-the-middle attack!", fingerprint); }
public static string FingerprintNotSpecifiedManagedError(string fingerprint, string hostkeyoption, string allkeysoptions) { return LC.L(@"Please add --{1}=""{0}"" to trust this host. Optionally you can use --{2} (NOT SECURE) for testing!", fingerprint, hostkeyoption, allkeysoptions); }
@@ -26,9 +26,9 @@ namespace Duplicati.Library.Backend.Strings
public static string Description { get { return LC.L(@"This backend can read and write data to a SharePoint server (including OneDrive for Business). Allowed formats are ""mssp://tennant.sharepoint.com/PathToWeb//BaseDocLibrary/subfolder"" and ""mssp://username:password@tennant.sharepoint.com/PathToWeb//BaseDocLibrary/subfolder"". Use a double slash '//' in the path to denote the web from the documents library."); } }
public static string DisplayName { get { return LC.L(@"Microsoft SharePoint"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionIntegratedAuthenticationLong { get { return LC.L(@"If the server and client both supports integrated authentication, this option enables that authentication method. This is likely only available with windows servers and clients."); } }
public static string DescriptionIntegratedAuthenticationShort { get { return LC.L(@"Use windows integrated authentication to connect to the server"); } }
public static string DescriptionUseRecyclerLong { get { return LC.L(@"Use this option to have files moved to the recycle bin folder instead of removing them permanently when compacting or deleting backups."); } }
@@ -50,7 +50,7 @@ namespace Duplicati.Library.Backend.Strings
internal static class OneDriveForBusiness
{
public static string Description { get { return LC.L(@"Supports connections to Microsoft OneDrive for Business. Allowed formats are ""od4b://tennant.sharepoint.com/personal/username_domain/Documents/subfolder"" and ""od4b://username:password@tennant.sharepoint.com/personal/username_domain/Documents/folder"". You can use a double slash '//' in the path to denote the base path from the documents folder."); } }
public static string Description { get { return LC.L(@"This backend can read and write data to Microsoft OneDrive for Business. Allowed formats are ""od4b://tennant.sharepoint.com/personal/username_domain/Documents/subfolder"" and ""od4b://username:password@tennant.sharepoint.com/personal/username_domain/Documents/folder"". You can use a double slash '//' in the path to denote the base path from the documents folder."); } }
public static string DisplayName { get { return LC.L(@"Microsoft OneDrive for Business"); } }
}
}
@@ -43,14 +43,14 @@ namespace Duplicati.Library.Backend.Storj
public string DisplayName { get { return LC.L("Storj DCS configuration module"); } }
public string Description { get { return LC.L("Exposes Storj DCS configuration as a web module"); } }
public string Description { get { return LC.L("Expose Storj DCS configuration as a web module"); } }
public IList<ICommandLineArgument> SupportedCommands
{
get
{
return new List<ICommandLineArgument>([
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provides different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, LC.L("The config to get"), LC.L("Provide different config values"), DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType)))
]);
}
+15 -15
View File
@@ -26,20 +26,20 @@ namespace Duplicati.Library.Backend.Strings
{
public static string Description { get { return LC.L(@"This backend can read and write data to the Storj DCS."); } }
public static string DisplayName { get { return LC.L(@"Storj DCS (Decentralized Cloud Storage)"); } }
public static string TestConnectionFailed { get { return LC.L(@"The connection-test failed."); } }
public static string StorjAuthMethodDescriptionLong { get { return LC.L(@"The authentication method describes which way to use to connect to the network - either via API key or via an access grant."); } }
public static string StorjAuthMethodDescriptionShort { get { return LC.L(@"The authentication method"); } }
public static string StorjSatelliteDescriptionLong { get { return LC.L(@"The satellite that keeps track of all metadata. Use a Storj DCS server for high-performance SLA-backed connectivity or use a community server. Or even host your own."); } }
public static string StorjSatelliteDescriptionShort { get { return LC.L(@"The satellite"); } }
public static string StorjAPIKeyDescriptionLong { get { return LC.L(@"The API key grants access to a specific project on your chosen satellite. Head over to the dashboard of your satellite to create one if you do not already have an API key."); } }
public static string StorjAPIKeyDescriptionShort { get { return LC.L(@"The API key"); } }
public static string StorjSecretDescriptionLong { get { return LC.L(@"The encryption passphrase is used to encrypt your data before sending it to the Storj network. This passphrase can be the only secret to provide - for Storj you do not necessary need any additional encryption (from Duplicati) in place."); } }
public static string StorjSecretDescriptionShort { get { return LC.L(@"The encryption passphrase"); } }
public static string StorjSharedAccessDescriptionLong { get { return LC.L(@"An access grant contains all information in one encrypted string. You may use it instead of a satellite, API key and secret."); } }
public static string StorjSharedAccessDescriptionShort { get { return LC.L(@"The access grant"); } }
public static string StorjBucketDescriptionLong { get { return LC.L(@"The bucket where the backup will reside in."); } }
public static string StorjBucketDescriptionShort { get { return LC.L(@"The bucket"); } }
public static string StorjFolderDescriptionLong { get { return LC.L(@"The folder within the bucket where the backup will reside in."); } }
public static string StorjFolderDescriptionShort { get { return LC.L(@"The folder"); } }
public static string TestConnectionFailed { get { return LC.L(@"Connection-test failed."); } }
public static string StorjAuthMethodDescriptionLong { get { return LC.L(@"Specify the authentication method which describes which way to use to connect to the network - either via API key or via an access grant."); } }
public static string StorjAuthMethodDescriptionShort { get { return LC.L(@"Authentication method"); } }
public static string StorjSatelliteDescriptionLong { get { return LC.L(@"Specify the satellite that keeps track of all metadata. Use a Storj DCS server for high-performance SLA-backed connectivity or use a community server. Or even host your own."); } }
public static string StorjSatelliteDescriptionShort { get { return LC.L(@"Satellite"); } }
public static string StorjAPIKeyDescriptionLong { get { return LC.L(@"Supply the API key which grants access to a specific project on your chosen satellite. Head over to the dashboard of your satellite to create one if you do not already have an API key."); } }
public static string StorjAPIKeyDescriptionShort { get { return LC.L(@"API key"); } }
public static string StorjSecretDescriptionLong { get { return LC.L(@"Supply the encryption passphrase used to encrypt your data before sending it to the Storj network. This passphrase can be the only secret to provide - for Storj you do not necessary need any additional encryption (from Duplicati) in place."); } }
public static string StorjSecretDescriptionShort { get { return LC.L(@"Encryption passphrase"); } }
public static string StorjSharedAccessDescriptionLong { get { return LC.L(@"Supply the access grant which contains all information in one encrypted string. You may use it instead of a satellite, API key and secret."); } }
public static string StorjSharedAccessDescriptionShort { get { return LC.L(@"Access grant"); } }
public static string StorjBucketDescriptionLong { get { return LC.L(@"Specify the bucket for storing the backup."); } }
public static string StorjBucketDescriptionShort { get { return LC.L(@"Bucket"); } }
public static string StorjFolderDescriptionLong { get { return LC.L(@"Specify the folder in the bucket for storing the backup."); } }
public static string StorjFolderDescriptionShort { get { return LC.L(@"Folder"); } }
}
}
@@ -24,7 +24,7 @@ namespace Duplicati.Library.Backend.Strings {
public static string Description { get { return LC.L(@"This backend can read and write data to a Tahoe-LAFS based backend. Allowed format is ""tahoe://hostname:port/uri/$DIRCAP""."); } }
public static string Displayname { get { return LC.L(@"Tahoe-LAFS"); } }
public static string DescriptionUseSSLLong { get { return LC.L(@"Use this option to communicate using Secure Socket Layer (SSL) over http (https)."); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instructs Duplicati to use an SSL (https) connection"); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (https) connection"); } }
public static string MissingFolderError(string foldername, string message) { return LC.L(@"The folder {0} was not found, message: {1}", foldername, message); }
public static string UnrecognizedUriError { get { return LC.L(@"Unsupported URL format, must start with ""uri/URI:DIR2:"""); } }
}
@@ -32,10 +32,10 @@ namespace Duplicati.Library.Backend.Strings
public static string COSAPISecretIdDescriptionShort { get { return LC.L(@"Secret ID"); } }
public static string COSAPISecretKeyDescriptionLong { get { return LC.L(@"Cloud API Secret Key."); } }
public static string COSAPISecretKeyDescriptionShort { get { return LC.L(@"Secret Key"); } }
public static string COSBucketDescriptionLong { get { return LC.L(@"Bucket, format: BucketName-APPID"); } }
public static string COSBucketDescriptionShort { get { return LC.L(@"Bucket"); } }
public static string COSBucketDescriptionLong { get { return LC.L(@"Bucket name, format: BucketName-APPID"); } }
public static string COSBucketDescriptionShort { get { return LC.L(@"Bucket name"); } }
public static string COSLocationDescriptionLong { get { return LC.L(@"Region is the distribution area of ​​the Tencent cloud hosting machine room. The object storage COS data is stored in the storage buckets of these regions. https://intl.cloud.tencent.com/document/product/436/6224."); } }
public static string COSLocationDescriptionShort { get { return LC.L(@"Specifies COS location constraints"); } }
public static string COSLocationDescriptionShort { get { return LC.L(@"Specify COS location constraints"); } }
public static string COSStorageClassDescriptionLong { get { return LC.L(@"Storage class of the object; check enumerated values at https://intl.cloud.tencent.com/document/product/436/30925."); } }
public static string COSStorageClassDescriptionShort { get { return LC.L(@"Storage class of the object"); } }
}
+4 -4
View File
@@ -21,14 +21,14 @@
using Duplicati.Library.Localization.Short;
namespace Duplicati.Library.Backend.Strings {
internal static class WEBDAV {
public static string Description { get { return LC.L(@"Supports connections to a WEBDAV enabled web server, using the HTTP protocol. Allowed formats are ""webdav://hostname/folder"" and ""webdav://username:password@hostname/folder""."); } }
public static string Description { get { return LC.L(@"This backend can read and write data to a WEBDAV enabled web server, using the HTTP protocol. Allowed formats are ""webdav://hostname/folder"" and ""webdav://username:password@hostname/folder""."); } }
public static string DisplayName { get { return LC.L(@"WebDAV"); } }
public static string DescriptionForceDigestLong { get { return LC.L(@"Using the HTTP Digest authentication method allows the user to authenticate with the server, without sending the password in clear. However, a man-in-the-middle attack is easy, because the HTTP protocol specifies a fallback to Basic authentication, which will make the client send the password to the attacker. Using this option, the client does not accept this, and always uses Digest authentication or fails to connect."); } }
public static string DescriptionForceDigestShort { get { return LC.L(@"Force the use of the HTTP Digest authentication method"); } }
public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supply the password used to connect to the server"); } }
public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supply the username used to connect to the server"); } }
public static string DescriptionIntegratedAuthenticationLong { get { return LC.L(@"If the server and client both supports integrated authentication, this option enables that authentication method. This is likely only available with windows servers and clients."); } }
public static string DescriptionIntegratedAuthenticationShort { get { return LC.L(@"Use windows integrated authentication to connect to the server"); } }
public static string MethodNotAllowedError(System.Net.HttpStatusCode statuscode) { return LC.L(@"The server returned the error code {0} ({1}), indicating that the server does not support WebDAV connections", (int)statuscode, statuscode); }
@@ -37,7 +37,7 @@ namespace Duplicati.Library.Backend.Strings {
This can be because the file is deleted or unavailable, but it can also be because the file extension {2} is blocked by the web server. IIS blocks unknown extensions by default.
Error message: {3}", foldername, filename, extension, errormessage); }
public static string DescriptionUseSSLLong { get { return LC.L(@"Use this option to communicate using Secure Socket Layer (SSL) over http (https)."); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instructs Duplicati to use an SSL (https) connection"); } }
public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (https) connection"); } }
public static string DescriptionDebugPropfindLong { get { return LC.L(@"To aid in debugging issues, it is possible to set a path to a file that will be overwritten with the PROPFIND response."); } }
public static string DescriptionDebugPropfindShort { get { return LC.L(@"Dump the PROPFIND response"); } }
}
+175 -167
View File
@@ -1,29 +1,32 @@
// 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.
// 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 Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -31,7 +34,31 @@ namespace Duplicati.Library.Backend
{
public class WEBDAV : IBackend, IStreamingBackend
{
private readonly System.Net.NetworkCredential m_userInfo;
private record RequestResources : IDisposable
{
public RequestResources(HttpClient httpClient, HttpRequestMessage requestMessage)
{
HttpClient = httpClient;
RequestMessage = requestMessage;
}
public HttpRequestMessage RequestMessage { get; init; }
public HttpClient HttpClient { get; init; }
public void Dispose()
{
try
{
RequestMessage?.Dispose();
}
catch { }
try
{
HttpClient?.Dispose();
}
catch { }
}
}
private readonly NetworkCredential m_userInfo;
private readonly string m_url;
private readonly string m_path;
private readonly string m_sanitizedUrl;
@@ -42,8 +69,6 @@ namespace Duplicati.Library.Backend
private readonly bool m_useIntegratedAuthentication = false;
private readonly bool m_forceDigestAuthentication = false;
private readonly bool m_useSSL = false;
private readonly string m_debugPropfindFile = null;
private readonly byte[] m_copybuffer = new byte[Duplicati.Library.Utility.Utility.DEFAULT_BUFFER_SIZE];
/// <summary>
/// A list of files seen in the last List operation.
@@ -59,6 +84,16 @@ namespace Duplicati.Library.Backend
//private static readonly byte[] PROPFIND_BODY = System.Text.Encoding.UTF8.GetBytes("<?xml version=\"1.0\"?><D:propfind xmlns:D=\"DAV:\"><D:allprop/></D:propfind>");
private static readonly byte[] PROPFIND_BODY = new byte[0];
/// <summary>
/// The default timeout in seconds for PUT/GET file operations
/// </summary>
private const int LONG_OPERATION_TIMEOUT_SECONDS = 30000;
/// <summary>
/// The default timeout in seconds for LIST/CreateFolder operations
/// </summary>
private const int SHORT_OPERATION_TIMEOUT_SECONDS = 30;
public WEBDAV()
{
}
@@ -71,7 +106,7 @@ namespace Duplicati.Library.Backend
if (!string.IsNullOrEmpty(u.Username))
{
m_userInfo = new System.Net.NetworkCredential();
m_userInfo = new NetworkCredential();
m_userInfo.UserName = u.Username;
if (!string.IsNullOrEmpty(u.Password))
m_userInfo.Password = u.Password;
@@ -82,13 +117,13 @@ namespace Duplicati.Library.Backend
{
if (options.ContainsKey("auth-username"))
{
m_userInfo = new System.Net.NetworkCredential();
m_userInfo = new NetworkCredential();
m_userInfo.UserName = options["auth-username"];
if (options.ContainsKey("auth-password"))
m_userInfo.Password = options["auth-password"];
}
}
//Bugfix, see http://connect.microsoft.com/VisualStudio/feedback/details/695227/networkcredential-default-constructor-leaves-domain-null-leading-to-null-object-reference-exceptions-in-framework-code
if (m_userInfo != null)
m_userInfo.Domain = "";
@@ -105,7 +140,7 @@ namespace Duplicati.Library.Backend
m_path = "/" + m_path;
m_path = Util.AppendDirSeparator(m_path, "/");
m_path = Library.Utility.Uri.UrlDecode(m_path);
m_path = Utility.Uri.UrlDecode(m_path);
m_rawurl = new Utility.Uri(m_useSSL ? "https" : "http", u.Host, m_path).ToString();
int port = u.Port;
@@ -115,35 +150,27 @@ namespace Duplicati.Library.Backend
m_rawurlPort = new Utility.Uri(m_useSSL ? "https" : "http", u.Host, m_path, null, null, null, port).ToString();
m_sanitizedUrl = new Utility.Uri(m_useSSL ? "https" : "http", u.Host, m_path).ToString();
m_reverseProtocolUrl = new Utility.Uri(m_useSSL ? "http" : "https", u.Host, m_path).ToString();
options.TryGetValue("debug-propfind-file", out m_debugPropfindFile);
}
#region IBackend Members
public string DisplayName
{
get { return Strings.WEBDAV.DisplayName; }
}
public string DisplayName => Strings.WEBDAV.DisplayName;
public string ProtocolKey
{
get { return "webdav"; }
}
public string ProtocolKey => "webdav";
public IEnumerable<IFileEntry> List()
{
try
{
return this.ListWithouExceptionCatch();
return ListWithouExceptionCatch();
}
catch (System.Net.WebException wex)
catch (HttpRequestException wex)
{
if (wex.Response as System.Net.HttpWebResponse != null &&
((wex.Response as System.Net.HttpWebResponse).StatusCode == System.Net.HttpStatusCode.NotFound || (wex.Response as System.Net.HttpWebResponse).StatusCode == System.Net.HttpStatusCode.Conflict))
throw new Interface.FolderMissingException(Strings.WEBDAV.MissingFolderError(m_path, wex.Message), wex);
if (wex.StatusCode == HttpStatusCode.NotFound || wex.StatusCode == HttpStatusCode.Conflict)
throw new FolderMissingException(Strings.WEBDAV.MissingFolderError(m_path, wex.Message), wex);
if (wex.Response as System.Net.HttpWebResponse != null && (wex.Response as System.Net.HttpWebResponse).StatusCode == System.Net.HttpStatusCode.MethodNotAllowed)
throw new UserInformationException(Strings.WEBDAV.MethodNotAllowedError((wex.Response as System.Net.HttpWebResponse).StatusCode), "WebdavMethodNotAllowed", wex);
if (wex.StatusCode == HttpStatusCode.MethodNotAllowed)
throw new UserInformationException(Strings.WEBDAV.MethodNotAllowedError((HttpStatusCode)wex.StatusCode), "WebdavMethodNotAllowed", wex);
throw;
}
@@ -151,38 +178,21 @@ namespace Duplicati.Library.Backend
private IEnumerable<IFileEntry> ListWithouExceptionCatch()
{
var req = CreateRequest("");
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(SHORT_OPERATION_TIMEOUT_SECONDS));
req.Method = "PROPFIND";
req.Headers.Add("Depth", "1");
req.ContentType = "text/xml";
req.ContentLength = PROPFIND_BODY.Length;
using var requestResources = CreateRequest(string.Empty, new HttpMethod("PROPFIND"));
requestResources.RequestMessage.Headers.Add("Depth", "1");
requestResources.RequestMessage.Content = new StreamContent(new MemoryStream(PROPFIND_BODY));
requestResources.RequestMessage.Content.Headers.ContentLength = PROPFIND_BODY.Length;
var areq = new Utility.AsyncHttpRequest(req);
using (System.IO.Stream s = areq.GetRequestStream())
s.Write(PROPFIND_BODY, 0, PROPFIND_BODY.Length);
using var response = requestResources.HttpClient.SendAsync(requestResources.RequestMessage, HttpCompletionOption.ResponseContentRead, timeoutToken.Token).ConfigureAwait(false).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode(); // This replaces the if needed when Mono was used.
var doc = new System.Xml.XmlDocument();
using (var resp = (System.Net.HttpWebResponse)areq.GetResponse())
{
int code = (int)resp.StatusCode;
if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically
throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp);
if (!string.IsNullOrEmpty(m_debugPropfindFile))
{
using (var rs = areq.GetResponseStream())
using (var fs = new System.IO.FileStream(m_debugPropfindFile, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None))
Utility.Utility.CopyStream(rs, fs, false, m_copybuffer);
doc.Load(m_debugPropfindFile);
}
else
{
using (var rs = areq.GetResponseStream())
doc.Load(rs);
}
}
doc.Load(response.Content.ReadAsStream());
System.Xml.XmlNamespaceManager nm = new System.Xml.XmlNamespaceManager(doc.NameTable);
nm.AddNamespace("D", "DAV:");
@@ -194,7 +204,7 @@ namespace Duplicati.Library.Backend
{
//IIS uses %20 for spaces and %2B for +
//Apache uses %20 for spaces and + for +
string name = Library.Utility.Uri.UrlDecode(n.InnerText.Replace("+", "%2B"));
string name = Utility.Uri.UrlDecode(n.InnerText.Replace("+", "%2B"));
string cmp_path;
@@ -246,7 +256,7 @@ namespace Duplicati.Library.Backend
if (s != null)
isCollection = s.InnerText.Trim() == "1";
else
isCollection = (stat.SelectSingleNode("D:resourcetype/D:collection", nm) != null);
isCollection = stat.SelectSingleNode("D:resourcetype/D:collection", nm) != null;
}
FileEntry fe = new FileEntry(name, size, lastAccess, lastModified);
@@ -254,19 +264,19 @@ namespace Duplicati.Library.Backend
files.Add(fe);
m_filenamelist.Add(name);
}
return files;
}
public async Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
{
using (System.IO.FileStream fs = System.IO.File.OpenRead(filename))
using (FileStream fs = File.OpenRead(filename))
await PutAsync(remotename, fs, cancelToken);
}
public void Get(string remotename, string filename)
{
using (System.IO.FileStream fs = System.IO.File.Create(filename))
using (FileStream fs = File.Create(filename))
Get(remotename, fs);
}
@@ -274,22 +284,20 @@ namespace Duplicati.Library.Backend
{
try
{
System.Net.HttpWebRequest req = CreateRequest(remotename);
req.Method = "DELETE";
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)areq.GetResponse())
{
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
throw new FileMissingException();
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(SHORT_OPERATION_TIMEOUT_SECONDS));
int code = (int)resp.StatusCode;
if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically
throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp);
}
}
catch (System.Net.WebException wex)
using var requestResources = CreateRequest(remotename);
requestResources.RequestMessage.Method = HttpMethod.Delete;
var response = requestResources.HttpClient.SendAsync(requestResources.RequestMessage, timeoutToken.Token).ConfigureAwait(false).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode(); // This replaces the if needed when Mono was used.
}
catch (HttpRequestException wex)
{
if (wex.Response is HttpWebResponse response && response.StatusCode == System.Net.HttpStatusCode.NotFound)
if (wex.StatusCode == HttpStatusCode.NotFound)
throw new FileMissingException(wex);
else
throw;
@@ -298,7 +306,7 @@ namespace Duplicati.Library.Backend
public IList<ICommandLineArgument> SupportedCommands
{
get
get
{
return new List<ICommandLineArgument>(new ICommandLineArgument[] {
new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.WEBDAV.DescriptionAuthPasswordShort, Strings.WEBDAV.DescriptionAuthPasswordLong),
@@ -306,8 +314,7 @@ namespace Duplicati.Library.Backend
new CommandLineArgument("integrated-authentication", CommandLineArgument.ArgumentType.Boolean, Strings.WEBDAV.DescriptionIntegratedAuthenticationShort, Strings.WEBDAV.DescriptionIntegratedAuthenticationLong),
new CommandLineArgument("force-digest-authentication", CommandLineArgument.ArgumentType.Boolean, Strings.WEBDAV.DescriptionForceDigestShort, Strings.WEBDAV.DescriptionForceDigestLong),
new CommandLineArgument("use-ssl", CommandLineArgument.ArgumentType.Boolean, Strings.WEBDAV.DescriptionUseSSLShort, Strings.WEBDAV.DescriptionUseSSLLong),
new CommandLineArgument("debug-propfind-file", CommandLineArgument.ArgumentType.Path, Strings.WEBDAV.DescriptionDebugPropfindShort, Strings.WEBDAV.DescriptionDebugPropfindLong),
});
});
}
}
@@ -316,28 +323,27 @@ namespace Duplicati.Library.Backend
get { return Strings.WEBDAV.Description; }
}
public string[] DNSName
public string[] DNSName
{
get { return new string[] { m_dnsName }; }
}
public void Test()
{
this.List();
List();
}
public void CreateFolder()
{
System.Net.HttpWebRequest req = CreateRequest("");
req.Method = System.Net.WebRequestMethods.Http.MkCol;
req.KeepAlive = false;
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)areq.GetResponse())
{
int code = (int)resp.StatusCode;
if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically
throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp);
}
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(SHORT_OPERATION_TIMEOUT_SECONDS));
using var requestResources = CreateRequest(string.Empty, new HttpMethod("MKCOL"));
using var response = requestResources.HttpClient.SendAsync(requestResources.RequestMessage, timeoutToken.Token).ConfigureAwait(false).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode(); // This replaces the if needed when Mono was used.
}
#endregion
@@ -346,112 +352,114 @@ namespace Duplicati.Library.Backend
public void Dispose()
{
}
#endregion
private System.Net.HttpWebRequest CreateRequest(string remotename)
private RequestResources CreateRequest(string remotename, HttpMethod method = null)
{
System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(m_url + Library.Utility.Uri.UrlEncode(remotename).Replace("+", "%20"));
HttpClient httpClient;
if (m_useIntegratedAuthentication)
{
req.UseDefaultCredentials = true;
httpClient = HttpClientHelper.CreateClient(new HttpClientHandler
{
UseDefaultCredentials = true
});
}
else if (m_forceDigestAuthentication)
{
System.Net.CredentialCache cred = new System.Net.CredentialCache();
cred.Add(new Uri(m_url), "Digest", m_userInfo);
req.Credentials = cred;
httpClient = HttpClientHelper.CreateClient(new HttpClientHandler
{
Credentials = new CredentialCache
{
{ new System.Uri(m_url), "Digest", m_userInfo }
}
});
}
else
{
req.Credentials = m_userInfo;
//We need this under Mono for some reason,
// and it appears some servers require this as well
req.PreAuthenticate = true;
httpClient = HttpClientHelper.CreateClient();
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
"Basic",
Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{m_userInfo.UserName}:{m_userInfo.Password}"))
);
}
req.KeepAlive = false;
req.UserAgent = "Duplicati WEBDAV Client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
httpClient.Timeout = Timeout.InfiniteTimeSpan;
var request = new HttpRequestMessage(HttpMethod.Get, $"{m_url}{Utility.Uri.UrlEncode(remotename).Replace("+", "%20")}");
request.Headers.Add(HttpRequestHeader.UserAgent.ToString(), "Duplicati WEBDAV Client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
request.Headers.ConnectionClose = true; // Equivalent to KeepAlive = false
if (method != null)
request.Method = method;
return new RequestResources(httpClient, request);
return req;
}
#region IStreamingBackend Members
public async Task PutAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
public async Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken)
{
try
{
System.Net.HttpWebRequest req = CreateRequest(remotename);
req.Method = System.Net.WebRequestMethods.Http.Put;
req.ContentType = "application/octet-stream";
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(LONG_OPERATION_TIMEOUT_SECONDS));
using var combinedTokens = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancelToken);
try { req.ContentLength = stream.Length; }
catch { }
using var requestResources = CreateRequest(remotename, HttpMethod.Put);
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
using (System.IO.Stream s = areq.GetRequestStream())
await Utility.Utility.CopyStreamAsync(stream, s, true, cancelToken, m_copybuffer);
requestResources.RequestMessage.Content = new StreamContent(stream);
requestResources.RequestMessage.Content.Headers.ContentLength = stream.Length;
requestResources.RequestMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
requestResources.RequestMessage.Version = HttpVersion.Version11;
using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)areq.GetResponse())
{
int code = (int)resp.StatusCode;
if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically
throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp);
}
using var response = await requestResources.HttpClient.SendAsync(requestResources.RequestMessage, HttpCompletionOption.ResponseHeadersRead, combinedTokens.Token);
response.EnsureSuccessStatusCode(); // This replaces the if needed when Mono was used.
}
catch (System.Net.WebException wex)
catch (HttpRequestException wex)
{
//Convert to better exception
if (wex.Response as System.Net.HttpWebResponse != null)
if ((wex.Response as System.Net.HttpWebResponse).StatusCode == System.Net.HttpStatusCode.Conflict || (wex.Response as System.Net.HttpWebResponse).StatusCode == System.Net.HttpStatusCode.NotFound)
throw new Interface.FolderMissingException(Strings.WEBDAV.MissingFolderError(m_path, wex.Message), wex);
if (wex.StatusCode == HttpStatusCode.Conflict || wex.StatusCode == HttpStatusCode.NotFound)
throw new FolderMissingException(Strings.WEBDAV.MissingFolderError(m_path, wex.Message), wex);
throw;
}
}
public void Get(string remotename, System.IO.Stream stream)
public void Get(string remotename, Stream stream)
{
var req = CreateRequest(remotename);
req.Method = System.Net.WebRequestMethods.Http.Get;
try
{
var areq = new Utility.AsyncHttpRequest(req);
using (var resp = (System.Net.HttpWebResponse)areq.GetResponse())
{
int code = (int)resp.StatusCode;
if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically
throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp);
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(LONG_OPERATION_TIMEOUT_SECONDS));
using var requestResources = CreateRequest(remotename, HttpMethod.Get);
requestResources.HttpClient.DownloadFile(requestResources.RequestMessage, stream, null, timeoutToken.Token).ConfigureAwait(false).GetAwaiter().GetResult();
using (var s = areq.GetResponseStream())
Utility.Utility.CopyStream(s, stream, true, m_copybuffer);
}
}
catch (System.Net.WebException wex)
catch (HttpRequestException wex)
{
if (wex.Response as System.Net.HttpWebResponse != null)
{
if ((wex.Response as System.Net.HttpWebResponse).StatusCode == System.Net.HttpStatusCode.Conflict)
throw new Interface.FolderMissingException(Strings.WEBDAV.MissingFolderError(m_path, wex.Message), wex);
if
(
(wex.Response as System.Net.HttpWebResponse).StatusCode == System.Net.HttpStatusCode.NotFound
&&
m_filenamelist != null
&&
m_filenamelist.Contains(remotename)
)
throw new Exception(Strings.WEBDAV.SeenThenNotFoundError(m_path, remotename, System.IO.Path.GetExtension(remotename), wex.Message), wex);
}
if (wex.StatusCode == HttpStatusCode.Conflict)
throw new FolderMissingException(Strings.WEBDAV.MissingFolderError(m_path, wex.Message), wex);
if
(
wex.StatusCode == HttpStatusCode.NotFound
&&
m_filenamelist != null
&&
m_filenamelist.Contains(remotename)
)
throw new Exception(Strings.WEBDAV.SeenThenNotFoundError(m_path, remotename, Path.GetExtension(remotename), wex.Message), wex);
throw;
}
}
#endregion
}
}
}
@@ -34,7 +34,7 @@ using System.Linq;
namespace Duplicati.Library.Compression
{
/// <summary>
/// An abstraction of a zip archive as a FileArchive, based on SharpCompress.
/// An abstraction of a ZIP archive as a FileArchive, based on SharpCompress.
/// Please note, duplicati does not require both Read &amp; Write access at the same time so this has not been implemented.
/// </summary>
public class FileArchiveZip : ICompression
@@ -62,7 +62,7 @@ namespace Duplicati.Library.Compression
/// </summary>
private const string COMPRESSION_METHOD_OPTION = "zip-compression-method";
/// <summary>
/// The commandline option for toggling the zip64 support
/// The commandline option for toggling the ZIP64 support
/// </summary>
private const string COMPRESSION_ZIP64_OPTION = "zip-compression-zip64";
@@ -77,7 +77,7 @@ namespace Duplicati.Library.Compression
private const CompressionType DEFAULT_COMPRESSION_METHOD = CompressionType.Deflate;
/// <summary>
/// The default setting for the zip64 support
/// The default setting for the ZIP64 support
/// </summary>
private const bool DEFAULT_ZIP64 = false;
@@ -87,7 +87,7 @@ namespace Duplicati.Library.Compression
private const int CENTRAL_HEADER_ENTRY_SIZE = 8 + 2 + 2 + 4 + 4 + 4 + 4 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 4;
/// <summary>
/// The size of the extended zip64 header
/// The size of the extended ZIP64 header
/// </summary>
private const int CENTRAL_HEADER_ENTRY_SIZE_ZIP64_EXTRA = 2 + 2 + 8 + 8 + 8 + 4;
@@ -137,7 +137,7 @@ namespace Duplicati.Library.Compression
private readonly CompressionType m_compressionType;
/// <summary>
/// A flag indicating if zip64 is in use
/// A flag indicating if ZIP64 is in use
/// </summary>
private readonly bool m_usingZip64;
@@ -199,7 +199,7 @@ namespace Duplicati.Library.Compression
}
/// <summary>
/// Constructs a new zip instance.
/// Constructs a new ZIP instance.
/// Access mode is specified by mode parameter.
/// Note that stream would not be disposed by FileArchiveZip instance so
/// you may reuse it and have to dispose it yourself.
@@ -361,7 +361,7 @@ namespace Duplicati.Library.Compression
if (m_using_reader)
throw;
Logging.Log.WriteWarningMessage(LOGTAG, "BrokenCentralHeaderFallback", ex, "Zip archive appears to have a broken Central Record Header, switching to stream mode");
Logging.Log.WriteWarningMessage(LOGTAG, "BrokenCentralHeaderFallback", ex, "ZIP archive appears to have a broken Central Record Header, switching to stream mode");
SwitchToReader();
var d = new Dictionary<string, IEntry>(Duplicati.Library.Utility.Utility.ClientFilenameStringComparer);
@@ -385,7 +385,7 @@ namespace Duplicati.Library.Compression
if (d.Count < 2)
throw;
Logging.Log.WriteWarningMessage(LOGTAG, "BrokenCentralHeader", ex2, "Zip archive appears to have broken records, returning the {0} records that could be recovered", d.Count);
Logging.Log.WriteWarningMessage(LOGTAG, "BrokenCentralHeader", ex2, "ZIP archive appears to have broken records, returning the {0} records that could be recovered", d.Count);
}
m_entryDict = d;
@@ -69,7 +69,7 @@ namespace Duplicati.Library.Compression
public SevenZipCompression() { }
/// <summary>
/// Constructs a new zip instance.
/// Constructs a new ZIP instance.
/// Access mode is specified by mode parameter.
/// Note that stream would not be disposed by FileArchiveZip instance so
/// you may reuse it and have to dispose it yourself.
+10 -10
View File
@@ -21,15 +21,15 @@
using Duplicati.Library.Localization.Short;
namespace Duplicati.Library.Compression.Strings {
internal static class FileArchiveZip {
public static string Description { get { return LC.L(@"This module provides the industry standard Zip compression. Files created with this module can be read by any standard-compliant zip application."); } }
public static string DisplayName { get { return LC.L(@"Zip compression"); } }
public static string CompressionlevelDeprecated(string optionname) { return LC.L(@"Please use the {0} option instead.", optionname); }
public static string Description { get { return LC.L(@"This module provides the industry standard ZIP compression. Files created with this module can be read by any standard-compliant ZIP application."); } }
public static string DisplayName { get { return LC.L(@"ZIP compression"); } }
public static string CompressionlevelDeprecated(string optionname) { return LC.L(@"Use the option --{0} instead.", optionname); }
public static string CompressionlevelLong { get { return LC.L(@"This option controls the compression level used. A setting of zero gives no compression, and a setting of 9 gives maximum compression."); } }
public static string CompressionlevelShort { get { return LC.L(@"Sets the Zip compression level"); } }
public static string CompressionmethodLong(string optionname) { return LC.L(@"This option can be used to set an alternative compressor method, such as LZMA. Note that using another value than Deflate will cause the {0} option to be ignored.", optionname); }
public static string CompressionmethodShort { get { return LC.L(@"Sets the Zip compression method"); } }
public static string Compressionzip64Long { get { return LC.L(@"The zip64 format is required for files larger than 4GiB. Use this option to toggle it."); } }
public static string Compressionzip64Short { get { return LC.L(@"Toggles Zip64 support"); } }
public static string CompressionlevelShort { get { return LC.L(@"Set the ZIP compression level"); } }
public static string CompressionmethodLong(string optionname) { return LC.L(@"Use this option to set an alternative compressor method, such as LZMA. Note that using another value than Deflate will cause the option --{0} to be ignored.", optionname); }
public static string CompressionmethodShort { get { return LC.L(@"Set the ZIP compression method"); } }
public static string Compressionzip64Long { get { return LC.L(@"The ZIP64 format is required for files larger than 4GiB. Use this option to toggle it."); } }
public static string Compressionzip64Short { get { return LC.L(@"Toggle ZIP64 support"); } }
public static string FileNotFoundError(string filename) { return LC.L(@"File not found: {0}", filename); }
}
internal static class SevenZipCompression {
@@ -41,8 +41,8 @@ namespace Duplicati.Library.Compression.Strings {
public static string ThreadcountLong { get { return LC.L(@"The number of threads used in LZMA 2 compression. Defaults to the number of processor cores."); } }
public static string ThreadcountShort { get { return LC.L(@"Number of threads used in compression"); } }
public static string CompressionlevelLong { get { return LC.L(@"This option controls the compression level used. A setting of zero gives no compression, and a setting of 9 gives maximum compression."); } }
public static string CompressionlevelShort { get { return LC.L(@"Sets the 7z compression level"); } }
public static string CompressionlevelShort { get { return LC.L(@"Set the 7z compression level"); } }
public static string FastalgoLong { get { return LC.L(@"This option controls the compression algorithm used. Enabling this option will cause 7z to use the fast algorithm, which produces slightly less compression."); } }
public static string FastalgoShort { get { return LC.L(@"Sets the 7z fast algorithm usage"); } }
public static string FastalgoShort { get { return LC.L(@"Set the 7z fast algorithm usage"); } }
}
}
+31 -6
View File
@@ -31,17 +31,31 @@ namespace Duplicati.Library.Encryption
/// </summary>
public class AESEncryption : EncryptionBase
{
/// <summary>
/// The key used to encrypt the data
/// </summary>
private string m_key;
private readonly string m_key;
/// <summary>
/// The cached value for size overhead
/// </summary>
private static long m_cachedsizeoverhead = -1;
/// <summary>
/// Cached set of options for minimal header
/// </summary>
private static readonly SharpAESCrypt.EncryptionOptions m_minimalHeaderOptions = new(InsertCreatedByIdentifier: false, InsertTimeStamp: false, InsertPlaceholder: false);
/// <summary>
/// Cached set of options for decryption
/// </summary>
private static readonly SharpAESCrypt.DecryptionOptions m_decryptionOptions = new(IgnorePaddingBytes: Environment.GetEnvironmentVariable("AES_IGNORE_PADDING_BYTES") == "1");
/// <summary>
/// Options to use for encryption
/// </summary>
private readonly SharpAESCrypt.EncryptionOptions m_encryptionOptions;
/// <summary>
/// Default constructor, used to read file extension and supported commands
/// </summary>
@@ -52,12 +66,23 @@ namespace Duplicati.Library.Encryption
/// <summary>
/// Constructs a new AES encryption/decyption instance
/// </summary>
public AESEncryption(string passphrase, Dictionary<string, string> options)
/// <param name="passphrase">The passphrase to use</param>
/// <param name="minimalheader">Flag controlling if the encryption is done with a minimal header</param>
public AESEncryption(string passphrase, bool minimalheader)
{
if (string.IsNullOrEmpty(passphrase))
throw new ArgumentException(Strings.AESEncryption.EmptyKeyError, nameof(passphrase));
m_key = passphrase;
m_encryptionOptions = minimalheader ? m_minimalHeaderOptions : default;
}
/// <summary>
/// Constructs a new AES encryption/decyption instance
/// </summary>
public AESEncryption(string passphrase, Dictionary<string, string> options)
: this(passphrase, false)
{
}
#region IEncryption Members
@@ -81,7 +106,7 @@ namespace Duplicati.Library.Encryption
/// Dispose the specified disposing.
/// </summary>
/// <param name="disposing">If set to <c>true</c> disposing.</param>
protected override void Dispose(bool disposing) { m_key = null; }
protected override void Dispose(bool disposing) { }
/// <summary>
/// Returns the size in bytes of the overhead that will be added to a file of the given size when encrypted
@@ -106,7 +131,7 @@ namespace Duplicati.Library.Encryption
/// <param name="input">The target stream</param>
/// <returns>An encrypted stream that can be written to</returns>
public override Stream Encrypt(Stream input)
=> new SharpAESCrypt.EncryptingStream(m_key, input);
=> new SharpAESCrypt.EncryptingStream(m_key, input, m_encryptionOptions);
/// <summary>
/// Decrypts the stream to the output stream
@@ -114,7 +139,7 @@ namespace Duplicati.Library.Encryption
/// <param name="input">The encrypted stream</param>
/// <returns>The unencrypted stream</returns>
public override Stream Decrypt(Stream input)
=> new SharpAESCrypt.DecryptingStream(m_key, input, new SharpAESCrypt.DecryptionOptions(IgnorePaddingBytes: Environment.GetEnvironmentVariable("AES_IGNORE_PADDING_BYTES") == "1"));
=> new SharpAESCrypt.DecryptingStream(m_key, input, m_decryptionOptions);
/// <summary>
/// Gets a list of supported commandline arguments
@@ -0,0 +1,66 @@
// 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.IO;
using System.Text;
namespace Duplicati.Library.Encryption;
public static class AESStringEncryption
{
private enum Direction
{
Encryption,
Decryption
}
public static string EncryptToHex(string passphrase, string content)
{
return Transform(passphrase, content, Direction.Encryption);
}
public static string DecryptFromHex(string passphrase, string content)
{
return Transform(passphrase, content, Direction.Decryption);
}
private static string Transform(string passphrase, string content, Direction direction)
{
using var aesprovider = new AESEncryption(passphrase, minimalheader: true);
using var inputStream = new MemoryStream(direction == Direction.Encryption
? Encoding.UTF8.GetBytes(content)
: Utility.Utility.HexStringAsByteArray(content));
using var outputStream = new MemoryStream();
switch (direction)
{
case Direction.Encryption:
aesprovider.Encrypt(inputStream, outputStream);
return Utility.Utility.ByteArrayAsHexString(outputStream.ToArray());
case Direction.Decryption:
aesprovider.Decrypt(inputStream, outputStream);
return Encoding.UTF8.GetString(outputStream.ToArray());
default:
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,225 @@
// 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.
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Encryption;
/// <summary>
/// Class used to encrypt and decrypt settings in a way that is backwards compatible
/// with previous versions of Duplicati.
/// </summary>
public static class EncryptedFieldHelper
{
/// <summary>
/// Key instance, isolating the current key and its hash
/// </summary>
/// <param name="Key">The key to use</param>
/// <param name="Hash">The key hash</param>
/// <param name="IsBlacklisted">If the key is blacklisted</param>
public sealed record KeyInstance(string Key, string Hash, bool IsBlacklisted)
{
/// <summary>
/// Creates a new key instance
/// </summary>
/// <param name="key">The key to use</param>
/// <returns>The key instance</returns>
public static KeyInstance CreateKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentNullException(nameof(key), Strings.EncryptedFieldHelper.KeyEmptyError);
if (key.Length < 8)
throw new ArgumentException(Strings.EncryptedFieldHelper.KeyTooShortError, nameof(key));
using var hasher = HashFactory.CreateHasher(HashFactory.SHA256);
return new KeyInstance(key, key.ComputeHashToHex(hasher), IsKeyBlacklisted(key));
}
/// <summary>
/// Creates a key instance if the key is valid
/// </summary>
/// <param name="key">The key to create</param>
/// <returns>The key instance or <c>null</c> if the key is invalid</returns>
public static KeyInstance? CreateKeyIfValid(string? key)
=> string.IsNullOrWhiteSpace(key) ? null : CreateKey(key);
}
/// <summary>
/// Checks if a key is blacklisted
/// </summary>
/// <param name="key">The key to check</param>
/// <returns><c>true</c> if the key is blacklisted; <c>false</c> otherwise</returns>
public static bool IsKeyBlacklisted(string key)
=> DeviceIDHelper.EMPTY_DEVICE_ID_HASHES.Contains(key);
/// <summary>
/// The key based on the device ID
/// </summary>
private static readonly KeyInstance? DeviceIdKey = KeyInstance.CreateKeyIfValid(DeviceIDHelper.HasTrustedDeviceID ? DeviceIDHelper.GetDeviceIDHash() : null);
/// <summary>
/// The default key to use for encryption
/// </summary>
private static readonly KeyInstance? SuppliedKey = KeyInstance.CreateKeyIfValid(Environment.GetEnvironmentVariable(ENVIROMENT_VARIABLE_NAME));
/// <summary>
/// The default key to use for encryption
/// </summary>
private static readonly KeyInstance? DefaultKey = SuppliedKey ?? DeviceIdKey;
/// <summary>
/// Returns a value indicating if the default key is blacklisted and cannot be used
/// </summary>
public static bool IsDefaultKeyBlacklisted => DefaultKey?.IsBlacklisted ?? false;
/// <summary>
/// Returns a value indicating if the default key is valid
/// </summary>
public static bool HasValidDefaultKey => DefaultKey != null;
/// <summary>
/// Prefix used to identify an encrypted field
/// </summary>
public const string HEADER_PREFIX = "enc-v1:";
/// <summary>
/// The name of the enviroment variable that holds the encryption key
/// </summary>
public const string ENVIROMENT_VARIABLE_NAME = "SETTINGS_ENCRYPTION_KEY";
/// <summary>
/// Checks if a value is an encrypted string
/// </summary>
/// <param name="value">The value to decrypt</param>
/// <returns><c>true</c> if the string is encrypted; <c>false</c> otherwise</returns>
public static bool IsEncryptedString(string value)
=> !string.IsNullOrWhiteSpace(value) && value.StartsWith(HEADER_PREFIX);
/// <summary>
/// Decrypts a value from the database, if it is not encrypted, it will be returned as is.
///
/// If the value is encrypted, it will be decrypted using the key obtained from ActiveKey.
///
/// The check for encryption is done by checking the prefix of the string.
/// An additional check is done by hashing the content and comparing it to the hash
/// </summary>
/// <param name="value">data from the field</param>
/// <returns>Unencrypted data of the field</returns>
[return: NotNullIfNotNull("value")]
public static string? Decrypt(string? value)
=> Decrypt(value, DefaultKey);
/// <summary>
/// Decrypts a value from the database, if it is not encrypted, it will be returned as is.
///
/// If the value is encrypted, it will be decrypted using the key obtained from ActiveKey.
///
/// The check for encryption is done by checking the prefix of the string.
/// An additional check is done by hashing the content and comparing it to the hash
/// </summary>
/// <param name="value">data from the field</param>
/// <param name="key">The key to use for decryption</param>
/// <returns>Unencrypted data of the field</returns>
[return: NotNullIfNotNull("value")]
public static string? Decrypt(string? value, KeyInstance? key)
{
// If the value is not encrypted, it will be returned as is.
if (string.IsNullOrEmpty(value) || !value.StartsWith(HEADER_PREFIX))
return value;
if (key == null)
throw new SettingsEncryptionKeyMissingException();
value = value.Substring(HEADER_PREFIX.Length);
using var hasher = HashFactory.CreateHasher(HashFactory.SHA256);
// For clarity, HashSize is size in bits / 8 for bytes, then times two because an encrypted field
// is prefixed with two hashes before
var hashSizeInBytes = hasher.HashSize / 8 * 2;
// Value may be encrypted, to ensure, we will parse everything after
// the mark of hashesCombinedSize as content, hash it and check if matches prefix.
var contentHash = value.Substring(0, hashSizeInBytes);
var keyHash = value.Substring(hashSizeInBytes, hashSizeInBytes);
var content = value.Substring(hashSizeInBytes * 2);
if (contentHash == content.ComputeHashToHex(hasher))
{
// Content hashes match therefore it is probed as encrypted, the next
// step is to verify the encryption keys hashes match.
if (keyHash != key.Hash)
throw new SettingsEncryptionKeyMismatchException();
// Lets then decrypt it.
return AESStringEncryption.DecryptFromHex(key.Key, content);
}
// if the hashes don't match, the lenght criteria can be ignored,
// and it will be returned as is.
return value;
}
/// <summary>
/// Encrypts a value to be stored in the database.
/// </summary>
/// <param name="value"></param>
/// <returns>The encrypted string</returns>
public static string Encrypt(string value)
=> Encrypt(value, DefaultKey);
/// <summary>
/// Encrypts a value to be stored in the database.
/// </summary>
/// <param name="value"></param>
/// <param name="key">The key to use for encryption</param>
/// <returns>The encrypted string</returns>
public static string Encrypt(string value, KeyInstance? key)
{
if (key == null)
throw new SettingsEncryptionKeyMissingException();
if (key.IsBlacklisted)
throw new InvalidOperationException(Strings.EncryptedFieldHelper.KeyBlacklistedError);
using var hasher = HashFactory.CreateHasher(HashFactory.SHA256);
var encrypted = AESStringEncryption.EncryptToHex(key.Key, value);
var sb = new StringBuilder();
sb.Append(HEADER_PREFIX);
sb.Append(encrypted.ComputeHashToHex(hasher));
sb.Append(key.Hash);
sb.Append(encrypted);
return sb.ToString();
}
}
+9 -3
View File
@@ -30,7 +30,7 @@ namespace Duplicati.Library.Encryption.Strings
public static string EmptyKeyError { get { return LC.L(@"Empty passphrase not allowed"); } }
public static string AessetthreadlevelLong { get { return LC.L(@"Use this option to set the thread level allowed for AES crypt operations."); } }
public static string AessetthreadlevelShort { get { return LC.L(@"Set thread level utilized for crypting"); } }
public static string AessetthreadlevelDeprecated { get { return LC.L(@"This option has no effect and should not be used."); } }
public static string AessetthreadlevelDeprecated { get { return LC.L(@"The option --{0} is no longer used and has been deprecated.", "aes-set-threadlevel"); } }
}
internal static class EncryptionBase
{
@@ -49,9 +49,9 @@ namespace Duplicati.Library.Encryption.Strings
public static string GpgprogrampathShort { get { return LC.L(@"The path to GnuPG"); } }
public static string GpgencryptionenablearmorLong { get { return LC.L(@"Use this option to supply the --armor option to GPG. The files will be larger but can be sent as pure text files."); } }
public static string GpgencryptionenablearmorShort { get { return LC.L(@"Use GPG Armor"); } }
public static string GpgencryptiondecryptioncommandLong { get { return LC.L(@"Overrides the GPG command supplied for decryption."); } }
public static string GpgencryptiondecryptioncommandLong { get { return LC.L(@"Override the GPG command supplied for decryption."); } }
public static string GpgencryptiondecryptioncommandShort { get { return LC.L(@"The GPG decryption command"); } }
public static string GpgencryptionencryptioncommandLong(string commandname, string optionvalue) { return LC.L(@"Overrides the default GPG encryption command ""{0}"". Normal usage is to request asymetric encryption with the setting {1}.", commandname, optionvalue); }
public static string GpgencryptionencryptioncommandLong(string commandname, string optionvalue) { return LC.L(@"Override the default GPG encryption command ""{0}"". Normal usage is to request asymetric encryption with the setting {1}.", commandname, optionvalue); }
public static string GpgencryptionencryptioncommandShort { get { return LC.L(@"The GPG encryption command"); } }
}
internal static class GPGStreamWrapper
@@ -60,4 +60,10 @@ namespace Duplicati.Library.Encryption.Strings
public static string GPGFlushError { get { return LC.L(@"Failure while invoking GnuPG, program won't flush output"); } }
public static string GPGTerminateError { get { return LC.L(@"Failure while invoking GnuPG, program won't terminate"); } }
}
internal static class EncryptedFieldHelper
{
public static string KeyTooShortError { get { return LC.L(@"Key must be at least 8 characters long"); } }
public static string KeyEmptyError { get { return LC.L(@"Key must not be empty"); } }
public static string KeyBlacklistedError { get { return LC.L(@"Refusing to encrypt with blacklisted key"); } }
}
}
+46 -22
View File
@@ -1,23 +1,23 @@
// 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.
// 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 Duplicati.Library.Localization.Short;
@@ -195,10 +195,34 @@ namespace Duplicati.Library.Interface
{
public RemoteListVerificationException(string message, string helpId)
: base(message, helpId)
{}
{ }
public RemoteListVerificationException(string message, string helpId, Exception innerException)
: base(message, helpId, innerException)
{}
{ }
}
/// <summary>
/// An exception indicating that the current encryption key does not match the key
/// used to encrypt the settings.
/// </summary>
[Serializable]
public class SettingsEncryptionKeyMismatchException : UserInformationException
{
public SettingsEncryptionKeyMismatchException()
: base(Strings.Common.SettingsKeyMismatchExceptionError, "SettingsKeyMismatch")
{ }
}
/// <summary>
/// An exception indicating that the current encryption key does not match the key
/// used to encrypt the settings.
/// </summary>
[Serializable]
public class SettingsEncryptionKeyMissingException : UserInformationException
{
public SettingsEncryptionKeyMissingException()
: base(Strings.Common.SettingsKeyMissingExceptionError, "SettingsKeyMissing")
{ }
}
}
+11 -4
View File
@@ -19,14 +19,17 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.Localization.Short;
namespace Duplicati.Library.Interface.Strings {
internal static class CommandLineArgument {
namespace Duplicati.Library.Interface.Strings
{
internal static class CommandLineArgument
{
public static string AliasesHeader { get { return LC.L(@"aliases"); } }
public static string DefaultValueHeader { get { return LC.L(@"default value"); } }
public static string DeprecationMarker { get { return LC.L(@"[DEPRECATED]"); } }
public static string ValuesHeader { get { return LC.L(@"values"); } }
}
internal static class DataTypes {
internal static class DataTypes
{
public static string Boolean { get { return LC.L(@"Boolean"); } }
public static string Enumeration { get { return LC.L(@"Enumeration"); } }
public static string Flags { get { return LC.L(@"Flags"); } }
@@ -37,9 +40,13 @@ namespace Duplicati.Library.Interface.Strings {
public static string Timespan { get { return LC.L(@"Timespan"); } }
public static string Unknown { get { return LC.L(@"Unknown"); } }
}
internal static class Common {
internal static class Common
{
public static string FolderAlreadyExistsError { get { return LC.L(@"The folder cannot be created because it already exists"); } }
public static string FolderMissingError { get { return LC.L(@"The requested folder does not exist"); } }
public static string CancelExceptionError { get { return LC.L(@"Cancelled"); } }
public static string SettingsKeyMismatchExceptionError { get { return LC.L(@"Encryption key used to encrypt target settings does not match current key."); } }
public static string SettingsKeyMissingExceptionError { get { return LC.L(@"Encryption key is missing."); } }
}
}
@@ -1,27 +1,25 @@
// 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.
// 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.Text;
namespace Duplicati.Library.Logging
{
@@ -72,7 +70,7 @@ namespace Duplicati.Library.Logging
m_stream.WriteLine(entry.AsString(true));
}
#endregion
#region IDisposable Members
+27 -23
View File
@@ -1,23 +1,23 @@
// 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.
// 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;
@@ -30,6 +30,7 @@ using Newtonsoft.Json;
using Duplicati.Library.Localization.Short;
using System.Threading;
using System.Net;
using Duplicati.Library.Interface;
namespace Duplicati.Library.Main
{
@@ -235,7 +236,7 @@ namespace Duplicati.Library.Main
{
if (this.LocalTempfile != null)
try { this.LocalTempfile.Dispose(); }
catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "DeleteTemporaryFileError", ex, "Failed to dispose temporary file: {0}", this.LocalTempfile); }
catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "DeleteTemporaryFileError", ex, "Failed to dispose temporary file: {0}", this.LocalTempfile); }
finally { this.LocalTempfile = null; }
}
@@ -418,7 +419,8 @@ namespace Duplicati.Library.Main
}
if (m_taskControl != null)
m_taskControl.StateChangedEvent += (state) => {
m_taskControl.StateChangedEvent += (state) =>
{
if (state == TaskControlState.Abort)
m_thread.Interrupt();
};
@@ -548,7 +550,7 @@ namespace Duplicati.Library.Main
try
{
var names = m_backend.DNSName ?? new string[0];
foreach(var name in names)
foreach (var name in names)
if (!string.IsNullOrWhiteSpace(name))
System.Net.Dns.GetHostEntry(name);
}
@@ -1465,6 +1467,8 @@ namespace Duplicati.Library.Main
throw m_lastException;
}
public IQuotaInfo Quota => (m_backend as IQuotaEnabledBackend)?.Quota;
public bool FlushDbMessages()
{
return m_db.FlushDbMessages(false);
@@ -21,6 +21,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Duplicati.Library.Logging;
namespace Duplicati.Library.Main
@@ -56,7 +57,7 @@ namespace Duplicati.Library.Main
{
if (target == null)
return;
m_targets.Add(new Tuple<ILogDestination, LogMessageType, Library.Utility.IFilter>(target, loglevel, filter ?? new Library.Utility.FilterExpression()));
}
@@ -76,11 +77,17 @@ namespace Duplicati.Library.Main
m_targets.Clear();
}
/// <summary>
/// Gets the minimum log level of all the targets
/// </summary>
public LogMessageType MinimumLevel
=> m_targets.Select(x => x.Item2).DefaultIfEmpty(LogMessageType.Error).Min();
/// <summary>
/// Writes the message to all the destinations.
/// </summary>
/// <param name="entry">Entry.</param>
public void WriteMessage(LogEntry entry)
public void WriteMessage(LogEntry entry)
{
foreach (var e in m_targets)
{
@@ -1343,12 +1343,21 @@ ORDER BY
m_insertIndexBlockLink.ExecuteNonQuery();
}
/// <summary>
/// Returns all unique blocklists for a given volume
/// </summary>
/// <param name="volumeid">The volume ID to get blocklists for</param>
/// <param name="blocksize">The blocksize</param>
/// <param name="hashsize">The size of the hash</param>
/// <param name="transaction">An optional external transaction</param>
/// <returns>An enumerable of tuples containing the blocklist hash, the blocklist data and the length of the data</returns>
public IEnumerable<Tuple<string, byte[], int>> GetBlocklists(long volumeid, long blocksize, int hashsize, System.Data.IDbTransaction transaction = null)
{
using (var cmd = m_connection.CreateCommand(transaction))
{
// Group subquery by hash to ensure that each blocklist hash appears only once in the result
var sql = string.Format(@"SELECT ""A"".""Hash"", ""C"".""Hash"" FROM " +
@"(SELECT ""BlocklistHash"".""BlocksetID"", ""Block"".""Hash"", * FROM ""BlocklistHash"",""Block"" WHERE ""BlocklistHash"".""Hash"" = ""Block"".""Hash"" AND ""Block"".""VolumeID"" = ?) A, " +
@"(SELECT ""BlocklistHash"".""BlocksetID"", ""Block"".""Hash"", ""BlocklistHash"".""Index"" FROM ""BlocklistHash"",""Block"" WHERE ""BlocklistHash"".""Hash"" = ""Block"".""Hash"" AND ""Block"".""VolumeID"" = ? GROUP BY ""Block"".""Hash"", ""Block"".""Size"") A, " +
@" ""BlocksetEntry"" B, ""Block"" C WHERE ""B"".""BlocksetID"" = ""A"".""BlocksetID"" AND " +
@" ""B"".""Index"" >= (""A"".""Index"" * {0}) AND ""B"".""Index"" < ((""A"".""Index"" + 1) * {0}) AND ""C"".""ID"" = ""B"".""BlockID"" " +
@" ORDER BY ""A"".""BlocksetID"", ""B"".""Index""",
@@ -1356,28 +1365,28 @@ ORDER BY
);
string curHash = null;
int index = 0;
int count = 0;
byte[] buffer = new byte[blocksize];
using (var rd = cmd.ExecuteReader(sql, volumeid))
while (rd.Read())
{
var blockhash = rd.GetValue(0).ToString();
if ((blockhash != curHash && curHash != null) || index + hashsize > buffer.Length)
if ((blockhash != curHash && curHash != null) || count + hashsize > buffer.Length)
{
yield return new Tuple<string, byte[], int>(curHash, buffer, index);
yield return new Tuple<string, byte[], int>(curHash, buffer, count);
buffer = new byte[blocksize];
index = 0;
count = 0;
}
var hash = Convert.FromBase64String(rd.GetValue(1).ToString());
Array.Copy(hash, 0, buffer, index, hashsize);
Array.Copy(hash, 0, buffer, count, hashsize);
curHash = blockhash;
index += hashsize;
count += hashsize;
}
if (curHash != null)
yield return new Tuple<string, byte[], int>(curHash, buffer, index);
yield return new Tuple<string, byte[], int>(curHash, buffer, count);
}
}
+117 -74
View File
@@ -22,8 +22,6 @@
using System;
using System.Linq;
using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
using Duplicati.Library.Common;
namespace Duplicati.Library.Main
{
@@ -38,10 +36,83 @@ namespace Duplicati.Library.Main
public string Username;
//public string Passwordhash;
public int Port;
public string Databasepath;
public string Databasepath;
public string ParameterFile;
}
/// <summary>
/// The filename of the file with database configurations
/// </summary>
private const string CONFIG_FILE = "dbconfig.json";
/// <summary>
/// Finds a default storage folder, using the operating system specific locations.
/// The targetfilename is used to detect locations that are used in previous versions.
/// If the targetfilename is found in an old location, but not the current, the old location is used.
/// If running with DEBUG defined, the storage folder is placed in the same folder as the executable
/// </summary>
/// <param name="targetfilename">The filename to look for</param>
/// <param name="appName">The name of the application</param>
/// <returns>The default storage folder</returns>
public static string GetDefaultStorageFolderWithDebugSupport(string targetfilename, string appName = "Duplicati")
{
#if DEBUG
return System.IO.Path.GetDirectoryName(typeof(DatabaseLocator).Assembly.Location) ?? string.Empty;
#else
return GetDefaultStorageFolder(targetfilename, appName);
#endif
}
/// <summary>
/// Finds a default storage folder, using the operating system specific locations.
/// The targetfilename is used to detect locations that are used in previous versions.
/// If the targetfilename is found in an old location, but not the current, the old location is used.
/// </summary>
/// <param name="targetfilename">The filename to look for</param>
/// <param name="appName">The name of the application</param>
/// <returns>The default storage folder</returns>
public static string GetDefaultStorageFolder(string targetfilename, string appName = "Duplicati")
{
//Normal mode uses the systems "(Local) Application Data" folder
// %LOCALAPPDATA% on Windows, ~/.config on Linux
var folder = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), appName);
if (OperatingSystem.IsWindows())
{
// Special handling for Windows:
// - Older versions use %APPDATA%
// - but new versions use %LOCALAPPDATA%
var newlocation = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), appName);
var prevfile = System.IO.Path.Combine(folder, targetfilename);
var curfile = System.IO.Path.Combine(newlocation, targetfilename);
// If the new file exists, we use that
// If the new file does not exist, and the old file exists we use the old
// Otherwise we use the new location
if (System.IO.File.Exists(curfile) || !System.IO.File.Exists(prevfile))
folder = newlocation;
}
if (OperatingSystem.IsMacOS())
{
// Special handling for MacOS:
// - Older versions use ~/.config/
// - but new versions use ~/Library/Application\ Support/
var configfolder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", appName);
var prevfile = System.IO.Path.Combine(configfolder, targetfilename);
var curfile = System.IO.Path.Combine(folder, targetfilename);
// If the old file exists, and not the new file, we use the old
// Otherwise we use the new location
if (!System.IO.File.Exists(curfile) && System.IO.File.Exists(prevfile))
folder = configfolder;
}
return folder;
}
public static string GetDatabasePath(string backend, Options options, bool autoCreate = true, bool anyUsername = false)
{
if (options == null)
@@ -50,40 +121,15 @@ namespace Duplicati.Library.Main
if (!string.IsNullOrEmpty(options.Dbpath))
return options.Dbpath;
//Normal mode uses the systems "(Local) Application Data" folder
// %LOCALAPPDATA% on Windows, ~/.config on Linux
var folder = GetDefaultStorageFolderWithDebugSupport(CONFIG_FILE);
// Special handling for Windows:
// - Older versions use %APPDATA%
// - but new versions use %LOCALAPPDATA%
//
// If we find a new version, lets use that
// otherwise use the older location
//
var folder = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Duplicati");
if (OperatingSystem.IsWindows())
{
var newlocation = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Duplicati");
var prevfile = System.IO.Path.Combine(folder, "dbconfig.json");
var curfile = System.IO.Path.Combine(newlocation, "dbconfig.json");
// If the new file exists, we use that
// If the new file does not exist, and the old file exists we use the old
// Otherwise we use the new location
if (System.IO.File.Exists(curfile) || !System.IO.File.Exists(prevfile))
folder = newlocation;
}
var file = System.IO.Path.Combine(folder, "dbconfig.json");
var file = System.IO.Path.Combine(folder, CONFIG_FILE);
List<BackendEntry> configs;
if (!System.IO.File.Exists(file))
configs = new List<BackendEntry>();
else
configs = Newtonsoft.Json.JsonConvert.DeserializeObject<List<BackendEntry>>(System.IO.File.ReadAllText(file, System.Text.Encoding.UTF8));
var uri = new Library.Utility.Uri(backend);
string server = uri.Host;
string path = uri.Path;
@@ -91,64 +137,64 @@ namespace Duplicati.Library.Main
int port = uri.Port;
string username = uri.Username;
string prefix = options.Prefix;
if (username == null || uri.Password == null)
{
var sopts = DynamicLoader.BackendLoader.GetSupportedCommands(backend);
var ropts = new Dictionary<string, string>(options.RawOptions);
foreach(var k in uri.QueryParameters.AllKeys)
foreach (var k in uri.QueryParameters.AllKeys)
ropts[k] = uri.QueryParameters[k];
if (sopts != null)
{
foreach(var o in sopts)
foreach (var o in sopts)
{
if (username == null && o.Aliases != null && o.Aliases.Contains("auth-username", StringComparer.OrdinalIgnoreCase) && ropts.ContainsKey(o.Name))
username = ropts[o.Name];
}
foreach(var o in sopts)
foreach (var o in sopts)
{
if (username == null && o.Name.Equals("auth-username", StringComparison.OrdinalIgnoreCase) && ropts.ContainsKey("auth-username"))
username = ropts["auth-username"];
}
}
}
//Now find the one that matches :)
var matches = (from n in configs
where
n.Type == type &&
//n.Passwordhash == password &&
n.Username == username &&
n.Port == port &&
n.Server == server &&
n.Path == path &&
n.Prefix == prefix
select n).ToList();
where
n.Type == type &&
//n.Passwordhash == password &&
n.Username == username &&
n.Port == port &&
n.Server == server &&
n.Path == path &&
n.Prefix == prefix
select n).ToList();
if (matches.Count > 1)
throw new Duplicati.Library.Interface.UserInformationException(string.Format("Multiple sources found for: {0}", backend), "MultipleLocalDatabaseSourcesFound");
// Re-select
if (matches.Count == 0 && anyUsername && string.IsNullOrEmpty(username))
{
matches = (from n in configs
where
n.Type == type &&
n.Port == port &&
n.Server == server &&
n.Path == path &&
n.Prefix == prefix
select n).ToList();
where
n.Type == type &&
n.Port == port &&
n.Server == server &&
n.Path == path &&
n.Prefix == prefix
select n).ToList();
if (matches.Count > 1)
throw new Duplicati.Library.Interface.UserInformationException(String.Format("Multiple sources found for \"{0}\", try supplying --{1}", backend, "auth-username"), "MultipleLocalDatabaseSourcesFound");
}
if (matches.Count == 0 && !autoCreate)
return null;
if (matches.Count == 0)
{
var backupname = options.BackupName;
@@ -156,20 +202,21 @@ namespace Duplicati.Library.Main
backupname = GenerateRandomName();
else
{
foreach(var c in System.IO.Path.GetInvalidFileNameChars())
foreach (var c in System.IO.Path.GetInvalidFileNameChars())
backupname = backupname.Replace(c.ToString(), "");
}
var newpath = System.IO.Path.Combine(folder, backupname + ".sqlite");
int max_tries = 100;
while (System.IO.File.Exists(newpath) && max_tries-- > 0)
newpath = System.IO.Path.Combine(folder, GenerateRandomName());
if (System.IO.File.Exists(newpath))
throw new Duplicati.Library.Interface.UserInformationException("Unable to find a unique name for the database, please use --dbpath", "CannotCreateRandomName");
//Create a new one, add it to the list, and save it
configs.Add(new BackendEntry() {
configs.Add(new BackendEntry()
{
Type = type,
Server = server,
Path = path,
@@ -177,7 +224,7 @@ namespace Duplicati.Library.Main
Username = username,
//Passwordhash = password,
Port = port,
Databasepath = newpath,
Databasepath = newpath,
ParameterFile = null
});
@@ -187,16 +234,16 @@ namespace Duplicati.Library.Main
var settings = new Newtonsoft.Json.JsonSerializerSettings();
settings.Formatting = Newtonsoft.Json.Formatting.Indented;
System.IO.File.WriteAllText(file, Newtonsoft.Json.JsonConvert.SerializeObject(configs, settings), System.Text.Encoding.UTF8);
return newpath;
}
else
{
return matches[0].Databasepath;
}
}
public static string GenerateRandomName()
{
var rnd = new Random();
@@ -210,11 +257,7 @@ namespace Duplicati.Library.Main
public static bool IsDatabasePathInUse(string path)
{
var folder = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Duplicati");
if (!System.IO.Directory.Exists(folder))
return false;
var file = System.IO.Path.Combine(folder, "dbconfig.json");
var file = System.IO.Path.Combine(GetDefaultStorageFolderWithDebugSupport(CONFIG_FILE), CONFIG_FILE);
if (!System.IO.File.Exists(file))
return false;
+132 -80
View File
@@ -149,34 +149,124 @@ namespace Duplicati.Library.Main.Operation
return service;
}
private void PreBackupVerify(BackendManager backend, string protectedfile)
private sealed record PreBackupVerifyResult(
LocalBackupDatabase Database,
BackendManager BackendManager,
string LastTempFilelist,
long LastTempFilesetId
);
/// <summary>
/// Verifies the database and backend before starting the backup.
/// The logic here is needed to check that the database is in a state
/// where it can be used for the backup, and that the backend is also
/// in the same state as the database.
///
/// If the auto-repair option is enabled, this method will attempt to
/// call the repair method, which requires that the database is closed
/// and re-opened.
///
/// For efficiency, the database is only closed if the repair is needed,
/// and returned to the caller in an open state in either case.
/// </summary>
/// <returns>Results from the pre-backup verification</returns>
private static async Task<PreBackupVerifyResult> PreBackupVerify(string backendurl, Options options, BackupResults result)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_PreBackupVerify);
result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_PreBackupVerify);
// Setup variables
LocalBackupDatabase database = null;
BackendManager backendManager = null;
// If we have an interrupted backup, grab the fileset
string lastTempFilelist = null;
long lastTempFilesetId = -1;
using (new Logging.Timer(LOGTAG, "PreBackupVerify", "PreBackupVerify"))
{
try
{
if (m_options.NoBackendverification)
{
FilelistProcessor.VerifyLocalList(backend, m_database);
UpdateStorageStatsFromDatabase();
}
else
FilelistProcessor.VerifyRemoteList(backend, m_options, m_database, m_result.BackendWriter, new string[] { protectedfile });
}
catch (RemoteListVerificationException ex)
{
if (m_options.AutoCleanup)
{
Logging.Log.WriteWarningMessage(LOGTAG, "BackendVerifyFailedAttemptingCleanup", ex, "Backend verification failed, attempting automatic cleanup");
m_result.RepairResults = new RepairResults(m_result);
new RepairHandler(backend.BackendUrl, m_options, (RepairResults)m_result.RepairResults).Run();
database = new LocalBackupDatabase(options.Dbpath, options);
backendManager = new BackendManager(backendurl, options, result.BackendWriter, database);
Logging.Log.WriteInformationMessage(LOGTAG, "BackendCleanupFinished", "Backend cleanup finished, retrying verification");
FilelistProcessor.VerifyRemoteList(backend, m_options, m_database, m_result.BackendWriter, new string[] { protectedfile });
result.SetDatabase(database);
result.Dryrun = options.Dryrun;
// Check the database integrity
Utility.UpdateOptionsFromDb(database, options);
Utility.VerifyOptionsAndUpdateDatabase(database, options);
var probe_path = database.GetFirstPath();
if (probe_path != null && Util.GuessDirSeparator(probe_path) != Util.DirectorySeparatorString)
throw new UserInformationException(string.Format("The backup contains files that belong to another operating system. Proceeding with a backup would cause the database to contain paths from two different operation systems, which is not supported. To proceed without losing remote data, delete all filesets and make sure the --{0} option is set, then run the backup again to re-use the existing data on the remote store.", "no-auto-compact"), "CrossOsDatabaseReuseNotSupported");
if (database.PartiallyRecreated)
throw new UserInformationException("The database was only partially recreated. This database may be incomplete and the repair process is not allowed to alter remote files as that could result in data loss.", "DatabaseIsPartiallyRecreated");
if (database.RepairInProgress)
throw new UserInformationException("The database was attempted repaired, but the repair did not complete. This database may be incomplete and the backup process cannot continue. You may delete the local database and attempt to repair it again.", "DatabaseRepairInProgress");
using (var db = new Backup.BackupDatabase(database, options))
{
// Make sure the database is sane
await db.VerifyConsistencyAsync(options.Blocksize, options.BlockhashSize, !options.DisableFilelistConsistencyChecks);
if (!options.DisableSyntheticFilelist)
{
var candidates = (await db.GetIncompleteFilesetsAsync()).OrderBy(x => x.Value).ToArray();
if (candidates.Any())
{
lastTempFilesetId = candidates.Last().Key;
lastTempFilelist = database.GetRemoteVolumeFromFilesetID(lastTempFilesetId).Name;
}
}
}
else
throw;
try
{
if (options.NoBackendverification)
{
FilelistProcessor.VerifyLocalList(backendManager, database);
UpdateStorageStatsFromDatabase(result, database, options, backendManager);
}
else
FilelistProcessor.VerifyRemoteList(backendManager, options, database, result.BackendWriter, new string[] { lastTempFilelist }, logErrors: false);
}
catch (RemoteListVerificationException ex)
{
if (options.AutoCleanup)
{
Logging.Log.WriteWarningMessage(LOGTAG, "BackendVerifyFailedAttemptingCleanup", ex, "Backend verification failed, attempting automatic cleanup");
result.RepairResults = new RepairResults(result);
// Close the database to allow the repair to run, it may create a new database
backendManager.Dispose();
database.Dispose();
database = null;
backendManager = null;
result.SetDatabase(null);
new RepairHandler(backendurl, options, (RepairResults)result.RepairResults).Run();
// Re-open the database and backend manager
database = new LocalBackupDatabase(options.Dbpath, options);
backendManager = new BackendManager(backendurl, options, result.BackendWriter, database);
result.SetDatabase(database);
Logging.Log.WriteInformationMessage(LOGTAG, "BackendCleanupFinished", "Backend cleanup finished, retrying verification");
FilelistProcessor.VerifyRemoteList(backendManager, options, database, result.BackendWriter, new string[] { lastTempFilelist });
}
else
throw;
}
return new PreBackupVerifyResult(database, backendManager, lastTempFilelist, lastTempFilesetId);
}
catch
{
backendManager?.Dispose();
database?.Dispose();
throw;
}
}
}
@@ -322,34 +412,30 @@ namespace Duplicati.Library.Main.Operation
/// <summary>
/// Handler for computing backend statistics, without relying on a remote folder listing
/// </summary>
private void UpdateStorageStatsFromDatabase()
private static void UpdateStorageStatsFromDatabase(BackupResults result, LocalBackupDatabase database, Options options, BackendManager backendManager)
{
if (m_result.BackendWriter != null)
if (result.BackendWriter != null)
{
m_result.BackendWriter.KnownFileCount = m_database.GetRemoteVolumes().Count();
m_result.BackendWriter.KnownFileSize = m_database.GetRemoteVolumes().Select(x => Math.Max(0, x.Size)).Sum();
result.BackendWriter.KnownFileCount = database.GetRemoteVolumes().Count();
result.BackendWriter.KnownFileSize = database.GetRemoteVolumes().Select(x => Math.Max(0, x.Size)).Sum();
m_result.BackendWriter.UnknownFileCount = 0;
m_result.BackendWriter.UnknownFileSize = 0;
result.BackendWriter.UnknownFileCount = 0;
result.BackendWriter.UnknownFileSize = 0;
m_result.BackendWriter.BackupListCount = m_database.FilesetTimes.Count();
m_result.BackendWriter.LastBackupDate = m_database.FilesetTimes.FirstOrDefault().Value.ToLocalTime();
result.BackendWriter.BackupListCount = database.FilesetTimes.Count();
result.BackendWriter.LastBackupDate = database.FilesetTimes.FirstOrDefault().Value.ToLocalTime();
// TODO: If we have a BackendManager, we should query through that
using (var backend = DynamicLoader.BackendLoader.GetBackend(m_backendurl, m_options.RawOptions))
if (!options.QuotaDisable)
{
if (backend is IQuotaEnabledBackend enabledBackend && !m_options.QuotaDisable)
var quota = backendManager.Quota;
if (quota != null)
{
Library.Interface.IQuotaInfo quota = enabledBackend.Quota;
if (quota != null)
{
m_result.BackendWriter.TotalQuotaSpace = quota.TotalQuotaSpace;
m_result.BackendWriter.FreeQuotaSpace = quota.FreeQuotaSpace;
}
result.BackendWriter.TotalQuotaSpace = quota.TotalQuotaSpace;
result.BackendWriter.FreeQuotaSpace = quota.FreeQuotaSpace;
}
}
m_result.BackendWriter.AssignedQuotaSpace = m_options.QuotaSize;
result.BackendWriter.AssignedQuotaSpace = options.QuotaSize;
}
}
@@ -410,27 +496,12 @@ namespace Duplicati.Library.Main.Operation
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_Begin);
// Do a remote verification, unless disabled
var (database, backendManager, lastTempFilelist, lastTempFilesetId) = await PreBackupVerify(m_backendurl, m_options, m_result);
// New isolated scope for each operation
using (new IsolatedChannelScope())
using (m_database = new LocalBackupDatabase(m_options.Dbpath, m_options))
{
m_result.SetDatabase(m_database);
m_result.Dryrun = m_options.Dryrun;
// Check the database integrity
Utility.UpdateOptionsFromDb(m_database, m_options);
Utility.VerifyOptionsAndUpdateDatabase(m_database, m_options);
var probe_path = m_database.GetFirstPath();
if (probe_path != null && Util.GuessDirSeparator(probe_path) != Util.DirectorySeparatorString)
throw new UserInformationException(string.Format("The backup contains files that belong to another operating system. Proceeding with a backup would cause the database to contain paths from two different operation systems, which is not supported. To proceed without losing remote data, delete all filesets and make sure the --{0} option is set, then run the backup again to re-use the existing data on the remote store.", "no-auto-compact"), "CrossOsDatabaseReuseNotSupported");
if (m_database.PartiallyRecreated)
throw new UserInformationException("The database was only partially recreated. This database may be incomplete and the repair process is not allowed to alter remote files as that could result in data loss.", "DatabaseIsPartiallyRecreated");
if (m_database.RepairInProgress)
throw new UserInformationException("The database was attempted repaired, but the repair did not complete. This database may be incomplete and the backup process cannot continue. You may delete the local database and attempt to repair it again.", "DatabaseRepairInProgress");
// If there is no filter, we set an empty filter to simplify the code
// If there is a filter, we make sure that the sources are included
m_filter = filter ?? new Library.Utility.FilterExpression();
@@ -440,9 +511,10 @@ namespace Duplicati.Library.Main.Operation
Task uploaderTask = null;
try
{
// Setup runners and instances here
using (m_database = database)
using (backendManager)
using (var db = new Backup.BackupDatabase(m_database, m_options))
using (var backendManager = new BackendManager(m_backendurl, m_options, m_result.BackendWriter, m_database))
// Setup runners and instances here
using (var filesetvolume = new FilesetVolumeWriter(m_options, m_database.OperationTimestamp))
using (var stats = new Backup.BackupStatsCollector(m_result))
// Keep a reference to these channels to avoid shutdown
@@ -455,29 +527,9 @@ namespace Duplicati.Library.Main.Operation
{
try
{
// Make sure the database is sane
await db.VerifyConsistencyAsync(m_options.Blocksize, m_options.BlockhashSize, !m_options.DisableFilelistConsistencyChecks);
// Start the uploader process
uploaderTask = uploader.Run();
// If we have an interrupted backup, grab the fileset
string lastTempFilelist = null;
long lastTempFilesetId = -1;
if (!m_options.DisableSyntheticFilelist)
{
var candidates = (await db.GetIncompleteFilesetsAsync()).OrderBy(x => x.Value).ToArray();
if (candidates.Any())
{
lastTempFilesetId = candidates.Last().Key;
lastTempFilelist = m_database.GetRemoteVolumeFromFilesetID(lastTempFilesetId).Name;
}
}
// TODO: Rewrite to using the uploader process, or the BackendHandler interface
// Do a remote verification, unless disabled
PreBackupVerify(backendManager, lastTempFilelist);
// If the previous backup was interrupted, send a synthetic list
await Backup.UploadSyntheticFilelist.Run(db, m_options, m_result, m_result.TaskReader, lastTempFilelist, lastTempFilesetId);
@@ -576,7 +628,7 @@ namespace Duplicati.Library.Main.Operation
if (m_result.TaskControlRendevouz() != TaskControlState.Abort)
{
if (m_options.NoBackendverification)
UpdateStorageStatsFromDatabase();
UpdateStorageStatsFromDatabase(m_result, m_database, m_options, backendManager);
else
PostBackupVerification(filesetvolume.RemoteFilename);
}
@@ -43,7 +43,7 @@ namespace Duplicati.Library.Main.Operation
public static void VerifyLocalList(BackendManager backend, LocalDatabase database)
{
var locallist = database.GetRemoteVolumes();
foreach(var i in locallist)
foreach (var i in locallist)
{
switch (i.State)
{
@@ -95,19 +95,20 @@ namespace Duplicati.Library.Main.Operation
/// <param name="database">The database to compare with</param>
/// <param name="log">The log instance to use</param>
/// <param name="protectedFiles">Filenames that should be exempted from deletion</param>
public static void VerifyRemoteList(BackendManager backend, Options options, LocalDatabase database, IBackendWriter log, IEnumerable<string> protectedFiles = null)
/// <param name="logErrors">Disable the logging of errors to prevent spamming the log; exceptions will be thrown regardless</param>
public static void VerifyRemoteList(BackendManager backend, Options options, LocalDatabase database, IBackendWriter log, IEnumerable<string> protectedFiles = null, bool logErrors = true)
{
var tp = RemoteListAnalysis(backend, options, database, log, protectedFiles);
long extraCount = 0;
long missingCount = 0;
foreach(var n in tp.ExtraVolumes)
foreach (var n in tp.ExtraVolumes)
{
Logging.Log.WriteWarningMessage(LOGTAG, "ExtraUnknownFile", null, "Extra unknown file: {0}", n.File.Name);
extraCount++;
}
foreach(var n in tp.MissingVolumes)
foreach (var n in tp.MissingVolumes)
{
Logging.Log.WriteWarningMessage(LOGTAG, "MissingFile", null, "Missing file: {0}", n.Name);
missingCount++;
@@ -116,7 +117,8 @@ namespace Duplicati.Library.Main.Operation
if (extraCount > 0)
{
var s = string.Format("Found {0} remote files that are not recorded in local storage, please run repair", extraCount);
Logging.Log.WriteErrorMessage(LOGTAG, "ExtraRemoteFiles", null, s);
if (logErrors)
Logging.Log.WriteErrorMessage(LOGTAG, "ExtraRemoteFiles", null, s);
throw new RemoteListVerificationException(s, "ExtraRemoteFiles");
}
@@ -126,7 +128,8 @@ namespace Duplicati.Library.Main.Operation
if (doubles.Count > 0)
{
var s = string.Format("Found remote files reported as duplicates, either the backend module is broken or you need to manually remove the extra copies.\nThe following files were found multiple times: {0}", string.Join(", ", doubles));
Logging.Log.WriteErrorMessage(LOGTAG, "DuplicateRemoteFiles", null, s);
if (logErrors)
Logging.Log.WriteErrorMessage(LOGTAG, "DuplicateRemoteFiles", null, s);
throw new RemoteListVerificationException(s, "DuplicateRemoteFiles");
}
@@ -138,7 +141,8 @@ namespace Duplicati.Library.Main.Operation
else
s = string.Format("Found {0} files that are missing from the remote storage, please run repair", missingCount);
Logging.Log.WriteErrorMessage(LOGTAG, "MissingRemoteFiles", null, s);
if (logErrors)
Logging.Log.WriteErrorMessage(LOGTAG, "MissingRemoteFiles", null, s);
throw new RemoteListVerificationException(s, "MissingRemoteFiles");
}
}
@@ -176,11 +180,11 @@ namespace Duplicati.Library.Main.Operation
/// <param name="transaction">An optional transaction object</param>
public static void UploadVerificationFile(string backendurl, Options options, IBackendWriter result, LocalDatabase db, System.Data.IDbTransaction transaction)
{
using(var backend = new BackendManager(backendurl, options, result, db))
using(var tempfile = new Library.Utility.TempFile())
using (var backend = new BackendManager(backendurl, options, result, db))
using (var tempfile = new Library.Utility.TempFile())
{
var remotename = options.Prefix + "-verification.json";
using(var stream = new System.IO.StreamWriter(tempfile, false, System.Text.Encoding.UTF8))
using (var stream = new System.IO.StreamWriter(tempfile, false, System.Text.Encoding.UTF8))
FilelistProcessor.CreateVerificationFile(db, stream);
if (options.Dryrun)
@@ -210,23 +214,24 @@ namespace Duplicati.Library.Main.Operation
protectedFiles = protectedFiles ?? Enumerable.Empty<string>();
var remotelist = (from n in rawlist
let p = Volumes.VolumeBase.ParseFilename(n)
where p != null && p.Prefix == options.Prefix
select p).ToList();
let p = Volumes.VolumeBase.ParseFilename(n)
where p != null && p.Prefix == options.Prefix
select p).ToList();
var otherlist = (from n in rawlist
let p = Volumes.VolumeBase.ParseFilename(n)
where p != null && p.Prefix != options.Prefix
select p).ToList();
let p = Volumes.VolumeBase.ParseFilename(n)
where p != null && p.Prefix != options.Prefix
select p).ToList();
var unknownlist = (from n in rawlist
let p = Volumes.VolumeBase.ParseFilename(n)
where p == null
select n).ToList();
let p = Volumes.VolumeBase.ParseFilename(n)
where p == null
select n).ToList();
var filesets = (from n in remotelist
where n.FileType == RemoteVolumeType.Files orderby n.Time descending
select n).ToList();
where n.FileType == RemoteVolumeType.Files
orderby n.Time descending
select n).ToList();
log.KnownFileCount = remotelist.Count;
long knownFileSize = remotelist.Select(x => Math.Max(0, x.File.Size)).Sum();
@@ -245,7 +250,7 @@ namespace Duplicati.Library.Main.Operation
var missingHash = new List<Tuple<long, RemoteVolumeEntry>>();
var cleanupRemovedRemoteVolumes = new HashSet<string>();
foreach(var e in database.DuplicateRemoteVolumes())
foreach (var e in database.DuplicateRemoteVolumes())
{
if (e.Value == RemoteVolumeState.Uploading || e.Value == RemoteVolumeState.Temporary)
database.UnlinkRemoteVolume(e.Key, e.Value);
@@ -254,7 +259,7 @@ namespace Duplicati.Library.Main.Operation
}
var locallist = database.GetRemoteVolumes();
foreach(var i in locallist)
foreach (var i in locallist)
{
Volumes.IParsedVolume r;
var remoteFound = lookup.TryGetValue(i.Name, out r);
@@ -360,7 +365,7 @@ namespace Duplicati.Library.Main.Operation
// cleanup deleted volumes in DB en block
database.RemoveRemoteVolumes(cleanupRemovedRemoteVolumes, null);
foreach(var i in missingHash)
foreach (var i in missingHash)
Logging.Log.WriteWarningMessage(LOGTAG, "MissingRemoteHash", null, "remote file {1} is listed as {0} with size {2} but should be {3}, please verify the sha256 hash \"{4}\"", i.Item2.State, i.Item2.Name, i.Item1, i.Item2.Size, i.Item2.Hash);
return new RemoteAnalysisResult()
@@ -420,8 +420,28 @@ namespace Duplicati.Library.Main.Operation
}
//If there are blocklists in the index file, add them to the temp blocklist hashes table
int wrongHashes = 0;
foreach (var b in svr.BlockLists)
restoredb.AddTempBlockListHash(b.Hash, b.Blocklist, tr);
{
// Compact might have created undetected invalid blocklist entries in index files due to broken LocalDatabase.GetBlocklists
// If the hash is wrong, recreate will download the dblock volume with the correct file
try
{
// We need to instantiate the list to ensure the verification is
// done before we add it to the database, since we do not have nested transactions
var list = b.Blocklist.ToList();
restoredb.AddTempBlockListHash(b.Hash, list, tr);
}
catch (System.IO.InvalidDataException e)
{
Logging.Log.WriteVerboseMessage(LOGTAG, "InvalidDataBlocklist", e, "Exception while processing blocklists in {0}", sf.Name);
++wrongHashes;
}
}
if (wrongHashes != 0)
{
Logging.Log.WriteWarningMessage(LOGTAG, "WrongBlocklistHashes", null, "{0} had invalid blocklists which could not be used. Consider deleting this index file and run repair to recreate it.", sf.Name);
}
}
}
}
+48 -48
View File
@@ -27,7 +27,7 @@ namespace Duplicati.Library.Main.Strings
{
public static string HashMismatchError(string filename, string recordedhash, string actualhash) { return LC.L(@"Hash mismatch on file ""{0}"", recorded hash: {1}, actual hash {2}", filename, recordedhash, actualhash); }
public static string DownloadedFileSizeError(string filename, long actualsize, long expectedsize) { return LC.L(@"The file {0} was downloaded and had size {1} but the size was expected to be {2}", filename, actualsize, expectedsize); }
public static string DeprecatedOptionUsedWarning(string optionname, string message) { return LC.L(@"The option {0} is deprecated: {1}", optionname, message); }
public static string DeprecatedOptionUsedWarning(string optionname, string message) { return LC.L(@"The option --{0} has been deprecated: {1}", optionname, message); }
public static string DuplicateOptionNameWarning(string optionname) { return LC.L(@"The option --{0} exists more than once. Please report this to the developers", optionname); }
public static string NoSourceFoldersError { get { return LC.L(@"No source folders specified for backup"); } }
public static string SourceIsMissingError(string foldername) { return LC.L(@"Backup aborted since the source path {0} does not exist. Please verify that the source path exists, or remove the source path from the backup configuration, or set the allow-missing-source option.", foldername); }
@@ -62,7 +62,7 @@ namespace Duplicati.Library.Main.Strings
public static string RestorepathLong { get { return LC.L(@"By default, files will be restored in the source folders. Use this option to restore to another folder."); } }
public static string RestorepathShort { get { return LC.L(@"Restore to another folder"); } }
public static string AllowsleepLong { get { return LC.L(@"Allow system to enter sleep power modes for inactivity during backup/restore operations (Windows/OSX only)"); } }
public static string AllowsleepShort { get { return LC.L(@"Toggles system sleep mode"); } }
public static string AllowsleepShort { get { return LC.L(@"Toggle system sleep mode"); } }
public static string ThrottledownloadLong { get { return LC.L(@"By setting this value you can limit how much bandwidth Duplicati consumes for downloads. Setting this limit can make the backups take longer, but will make Duplicati less intrusive."); } }
public static string ThrottledownloadShort { get { return LC.L(@"Max number of kilobytes to download pr. second"); } }
public static string ThrottleuploadLong { get { return LC.L(@"By setting this value you can limit how much bandwidth Duplicati consumes for uploads. Setting this limit can make the backups take longer, but will make Duplicati less intrusive."); } }
@@ -93,26 +93,26 @@ namespace Duplicati.Library.Main.Strings
public static string SkipfilehashchecksShort { get { return LC.L(@"Skip hash checks"); } }
public static string SkipfileslargerthanLong { get { return LC.L(@"This option allows you to exclude files that are larger than the given value. Use this to prevent backups becoming extremely large."); } }
public static string SkipfileslargerthanShort { get { return LC.L(@"Limit the size of files being backed up"); } }
public static string TempdirLong { get { return LC.L(@"This option can be used to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
public static string TempdirLong { get { return LC.L(@"Use this option to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
public static string ThreadpriorityLong { get { return LC.L(@"Selects another thread priority for the process. Use this to set Duplicati to be more or less CPU intensive."); } }
public static string ThreadpriorityLong { get { return LC.L(@"Select another thread priority for the process. Use this to set Duplicati to be more or less CPU intensive."); } }
public static string ThreadpriorityShort { get { return LC.L(@"Thread priority"); } }
public static string DblocksizeLong { get { return LC.L(@"This option can change the maximum size of dblock files. Changing the size can be useful if the backend has a limit on the size of each individual file."); } }
public static string DblocksizeShort { get { return LC.L(@"Limit the size of the volumes"); } }
public static string DisableStreamingLong { get { return LC.L(@"Enabling this option will disallow usage of the streaming interface, which means that transfer progress bars will not show, and bandwidth throttle settings will be ignored."); } }
public static string DisableStreamingShort { get { return LC.L(@"Disables use of the streaming transfer method"); } }
public static string DisableStreamingLong { get { return LC.L(@"Use this option to disallow usage of the streaming interface, which means that transfer progress bars will not show, and bandwidth throttle settings will be ignored."); } }
public static string DisableStreamingShort { get { return LC.L(@"Disable use of the streaming transfer method"); } }
public static string DontreadmanifestsLong { get { return LC.L(@"Use this option to make sure the contents of the manifest file are not read. This also implies that file hashes are not checked either. Use only for disaster recovery."); } }
public static string DontreadmanifestsShort { get { return LC.L(@"Disables manifests verification"); } }
public static string DontreadmanifestsShort { get { return LC.L(@"Disable manifests verification"); } }
public static string CompressionmoduleLong { get { return LC.L(@"Duplicati supports pluggable compression modules. Use this option to select a module to use for compression. This is only applied when creating new volumes, when reading an existing file, the filename is used to select the compression module."); } }
public static string CompressionmoduleShort { get { return LC.L(@"Select what module to use for compression"); } }
public static string EncryptionmoduleLong { get { return LC.L(@"Duplicati supports pluggable encryption modules. Use this option to select a module to use for encryption. This is only applied when creating new volumes, when reading an existing file, the filename is used to select the encryption module."); } }
public static string EncryptionmoduleShort { get { return LC.L(@"Select what module to use for encryption"); } }
public static string DisablemoduleLong { get { return LC.L(@"Supply one or more module names, separated by commas to unload them."); } }
public static string DisablemoduleShort { get { return LC.L(@"Disables one or more modules"); } }
public static string DisablemoduleShort { get { return LC.L(@"Disable one or more modules"); } }
public static string EnablemoduleLong { get { return LC.L(@"Supply one or more module names, separated by commas to load them."); } }
public static string EnablemoduleShort { get { return LC.L(@"Enables one or more modules"); } }
public static string EnablemoduleShort { get { return LC.L(@"Enable one or more modules"); } }
public static string SnapshotpolicyLong { get { return LC.L(@"This setting controls the usage of snapshots, which allows Duplicati to backup files that are locked by other programs. If this is set to ""off"", Duplicati will not attempt to create a disk snapshot. Setting this to ""auto"" makes Duplicati attempt to create a snapshot, and fail silently if that was not allowed or supported (note that the OS may still log system warnings). A setting of ""on"" will also make Duplicati attempt to create a snapshot, but will produce a warning message in the log if it fails. Setting it to ""required"" will make Duplicati abort the backup if the snapshot creation fails. On windows this uses the Volume Shadow Copy Services (VSS) and requires administrative privileges. On Linux this uses Logical Volume Management (LVM) and requires root privileges."); } }
public static string SnapshotpolicyShort { get { return LC.L(@"Controls the use of disk snapshots"); } }
public static string SnapshotpolicyShort { get { return LC.L(@"Control the use of disk snapshots"); } }
public static string AsynchronousuploadfolderLong { get { return LC.L(@"The pre-generated volumes will be placed into the temporary folder by default. This option can set a different folder for placing the temporary volumes. Despite the name, this also works for synchronous runs."); } }
public static string AsynchronousuploadfolderShort { get { return LC.L(@"The path where ready volumes are placed until uploaded"); } }
public static string AsynchronousuploadlimitLong { get { return LC.L(@"When performing asynchronous uploads, Duplicati will create volumes that can be uploaded. To prevent Duplicati from generating too many volumes, this option limits the number of pending uploads. Set to zero to disable the limit."); } }
@@ -120,25 +120,25 @@ namespace Duplicati.Library.Main.Strings
public static string AsynchronousconcurrentuploadlimitLong { get { return LC.L(@"When performing asynchronous uploads, the maximum number of concurrent uploads allowed. Set to zero to disable the limit."); } }
public static string AsynchronousconcurrentuploadlimitShort { get { return LC.L(@"The number of concurrent uploads allowed"); } }
public static string DebugoutputLong { get { return LC.L(@"Activate this option to make some error messages more verbose, which may help you track down a particular issue."); } }
public static string DebugoutputShort { get { return LC.L(@"Enables debugging output"); } }
public static string LogfileLong { get { return LC.L(@"Logs information to the file specified."); } }
public static string DebugoutputShort { get { return LC.L(@"Enable debugging output"); } }
public static string LogfileLong { get { return LC.L(@"Log information to the file specified."); } }
public static string LogfileShort { get { return LC.L(@"Log internal information to a file"); } }
public static string LoglevelLong { get { return LC.L(@"Specifies the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
public static string LoglevelLong { get { return LC.L(@"Specify the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
public static string LoglevelShort { get { return LC.L(@"Log information level"); } }
public static string LogLevelDeprecated(string option1, string option2) { return LC.L("Use the options --{0} and --{1} instead.", option1, option2); }
public static string DisableautocreatefolderLong { get { return LC.L(@"If Duplicati detects that the target folder is missing, it will create it automatically. Activate this option to prevent automatic folder creation."); } }
public static string DisableautocreatefolderShort { get { return LC.L(@"Disables automatic folder creation"); } }
public static string DisableautocreatefolderShort { get { return LC.L(@"Disable automatic folder creation"); } }
public static string VssexcludewritersLong { get { return LC.L(@"Use this option to exclude faulty writers from a snapshot. This is equivalent to the -wx flag of the vshadow.exe tool, except that it only accepts writer class GUIDs, and not component names or instance GUIDs. Multiple GUIDs must be separated with a semicolon, and most forms of GUIDs are allowed, including with and without curly braces."); } }
public static string VssexcludewritersShort { get { return LC.L(@"A semicolon separated list of guids of VSS writers to exclude (Windows only)"); } }
public static string UsnpolicyLong { get { return LC.L(@"This setting controls the usage of NTFS USN numbers, which allows Duplicati to obtain a list of files and folders much faster. If this is set to ""off"", Duplicati will not attempt to use USN. Setting this to ""auto"" makes Duplicati attempt to use USN, and fail silently if that was not allowed or supported. A setting of ""on"" will also make Duplicati attempt to use USN, but will produce a warning message in the log if it fails. Setting it to ""required"" will make Duplicati abort the backup if the USN usage fails. This feature is only supported on Windows and requires administrative privileges."); } }
public static string UsnpolicyShort { get { return LC.L(@"Controls the use of NTFS Update Sequence Numbers"); } }
public static string UsnpolicyShort { get { return LC.L(@"Control the use of NTFS Update Sequence Numbers"); } }
public static string DisabletimetoleranceLong { get { return LC.L(@"When matching timestamps, Duplicati will adjust the times by a small fraction to ensure that minor time differences do not cause unexpected updates. If the option --{0} is set to keep a week of backups, and the backup is made the same time each week, it is possible that the clock drifts slightly, such that full week has just passed, causing Duplicati to delete the older backup earlier than expected. To avoid this, Duplicati inserts a 1% tolerance (max 1 hour). Use this option to disable the tolerance, and use strict time checking.", "keep-time"); } }
public static string DisabletimetoleranceShort { get { return LC.L(@"Deactivates tolerance when comparing times"); } }
public static string DisabletimetoleranceShort { get { return LC.L(@"Deactivate tolerance when comparing times"); } }
public static string ListverifyuploadsLong { get { return LC.L(@"Use this option to verify uploads by listing contents."); } }
public static string ListverifyuploadsShort { get { return LC.L(@"Verify uploads by listing contents"); } }
public static string SynchronousuploadLong { get { return LC.L(@"Duplicati will upload files while scanning the disk and producing volumes, which usually makes the backup faster. Use this option to turn the behavior off, so that Duplicati will wait for each volume to complete."); } }
public static string SynchronousuploadShort { get { return LC.L(@"Upload files synchronously"); } }
public static string NoconnectionreuseLong { get { return LC.L(@"Duplicati will attempt to perform multiple operations on a single connection, as this avoids repeated login attempts, and thus speeds up the process. This option can be used to ensure that each operation is performed on a seperate connection."); } }
public static string NoconnectionreuseLong { get { return LC.L(@"Duplicati will attempt to perform multiple operations on a single connection, as this avoids repeated login attempts, and thus speeds up the process. Use this option to ensure that each operation is performed on a seperate connection."); } }
public static string NoconnectionreuseShort { get { return LC.L(@"Do not re-use connections"); } }
public static string DebugretryerrorsLong { get { return LC.L(@"When an error occurs, Duplicati will silently retry, and only report the number of retries. Enable this option to have the error messages displayed when a retry is performed."); } }
public static string DebugretryerrorsShort { get { return LC.L(@"Show error messages when a retry is performed"); } }
@@ -146,7 +146,7 @@ namespace Duplicati.Library.Main.Strings
public static string UploadUnchangedBackupsShort { get { return LC.L(@"Upload empty backup files"); } }
public static string QuotasizeLong { get { return LC.L(@"Set a limit to the amount of storage used on the backend (by this backup). This is in addition to the full backend quota, if available. Note: Backups will continue past the quota. This only creates warnings and error messages."); } }
public static string QuotasizeShort { get { return LC.L(@"Limit storage use"); } }
public static string QuotaWarningThresholdLong { get { return LC.L(@"Sets a threshold for when to warn about the backend quota being nearly exceeded. It is given as a percentage, and a warning is generated if the amount of available quota is less than this percentage of the total backup size. If the backend does not report the quota information, this value will be ignored."); } }
public static string QuotaWarningThresholdLong { get { return LC.L(@"Set a threshold for when to warn about the backend quota being nearly exceeded. It is given as a percentage, and a warning is generated if the amount of available quota is less than this percentage of the total backup size. If the backend does not report the quota information, this value will be ignored."); } }
public static string QuotaWarningThresholdShort { get { return LC.L(@"Threshold for warning about low quota"); } }
public static string QuotaDisableLong(string optionname) { return LC.L(@"Disable the quota reported by the backend. The option --{0} can still be used to set a manual quota", optionname); }
public static string QuotaDisableShort { get { return LC.L(@"Disable backend quota"); } }
@@ -158,38 +158,38 @@ namespace Duplicati.Library.Main.Strings
public static string ExcludefilesattributesShort { get { return LC.L(@"Exclude files by attribute"); } }
public static string VssusemappingLong { get { return LC.L(@"Activate this option to map VSS snapshots to a drive (similar to SUBST, using Win32 DefineDosDevice). This will create temporary drives that are then used to access the contents of a snapshot. This workaround can speed up file access on Windows XP."); } }
public static string VssusemappingShort { get { return LC.L(@"Map snapshots to a drive (Windows only)"); } }
public static string BackupnameLong { get { return LC.L(@"A display name that is attached to this backup. Can be used to identify the backup when sending mail or running scripts."); } }
public static string BackupnameLong { get { return LC.L(@"A display name that is attached to this backup. This can be used to identify the backup when sending mail or running scripts."); } }
public static string BackupnameShort { get { return LC.L(@"Name of the backup"); } }
public static string BackupidLong { get { return LC.L(@"A unique identification for this backup. Can be used to identify the backup when sending mail or running scripts."); } }
public static string BackupidLong { get { return LC.L(@"A unique identification for this backup. This can be used to identify the backup when sending mail or running scripts."); } }
public static string BackupidShort { get { return LC.L(@"Backup ID"); } }
public static string MachineidLong { get { return LC.L(@"A unique identification of the machine running the backup. Can be used to identify the machine when sending mail or running scripts."); } }
public static string MachineidLong { get { return LC.L(@"A unique identification of the machine running the backup. This can be used to identify the machine when sending mail or running scripts."); } }
public static string MachineidShort { get { return LC.L(@"Machine ID"); } }
public static string CompressionextensionfileLong(string path) { return LC.L(@"This property can be used to point to a text file where each line contains a file extension that indicates a non-compressible file. Files that have an extension found in the file will not be compressed, but simply stored in the archive. The file format ignores any lines that do not start with a period, and considers a space to indicate the end of the extension. A default file is supplied, that also serves as an example. The default file is placed in {0}.", path); }
public static string CompressionextensionfileLong(string path) { return LC.L(@"Use this option to point to a text file where each line contains a file extension that indicates a non-compressible file. Files that have an extension found in the file will not be compressed, but simply stored in the archive. The file format ignores any lines that do not start with a period, and considers a space to indicate the end of the extension. A default file is supplied, that also serves as an example. The default file is placed in {0}.", path); }
public static string CompressionextensionfileShort { get { return LC.L(@"Manage non-compressible file extensions"); } }
public static string BlocksizeLong { get { return LC.L(@"The block size determines how files are fragmented. Choosing a large value will cause a larger overhead on file changes, choosing a small value will cause a large overhead on storage of file lists. Note that the value cannot be changed after remote files are created."); } }
public static string BlocksizeShort { get { return LC.L(@"Block size used in hashing"); } }
public static string ChangedfilesLong { get { return LC.L(@"This option can be used to limit the scan to only files that are known to have changed. This is usually only activated in combination with a filesystem watcher that keeps track of file changes."); } }
public static string ChangedfilesLong { get { return LC.L(@"Use this option to limit the scan to only files that are known to have changed. This is usually only activated in combination with a filesystem watcher that keeps track of file changes."); } }
public static string ChangedfilesShort { get { return LC.L(@"List of files to examine for changes"); } }
public static string DbpathLong { get { return LC.L(@"Path to the file containing the local cache of the remote file database."); } }
public static string DbpathShort { get { return LC.L(@"Path to the local state database"); } }
public static string DeletedfilesLong(string optionname) { return LC.L(@"This option can be used to supply a list of deleted files. This option will be ignored unless the option --{0} is also set.", optionname); }
public static string DeletedfilesLong(string optionname) { return LC.L(@"Use this option to supply a list of deleted files. This option will be ignored unless the option --{0} is also set.", optionname); }
public static string DeletedfilesShort { get { return LC.L(@"List of deleted files"); } }
public static string DisablefilepathcacheLong { get { return LC.L(@"This option can be used to reduce the memory footprint by not keeping paths and modification timestamps in memory."); } }
public static string DisablefilepathcacheLong { get { return LC.L(@"Use this option to reduce the memory footprint by not keeping paths and modification timestamps in memory."); } }
public static string DisablefilepathcacheShort { get { return LC.L(@"Reduce memory footprint by disabling in-memory lookups"); } }
public static string DisablefilepathcacheDeprecated { get { return LC.L(@"The option --{0} is no longer used and has been deprecated.", "disable-filepath-cache"); } }
public static string UseblockcacheLong { get { return LC.L(@"This option can be used to increase speed in exchange for extra memory use."); } }
public static string UseblockcacheLong { get { return LC.L(@"Use this option to increase speed in exchange for extra memory use."); } }
public static string UseblockcacheShort { get { return LC.L(@"Store an in-memory block cache"); } }
public static string NobackendverificationLong { get { return LC.L(@"If this option is set, the local database is not compared to the remote filelist on startup. The intended usage for this option is to work correctly in cases where the filelisting is broken or unavailable."); } }
public static string NobackendverificationShort { get { return LC.L(@"Do not query backend at startup"); } }
public static string IndexfilepolicyLong { get { return LC.L(@"The index files are used to limit the need for downloading dblock files when there is no local database present. The more information is recorded in the index files, the faster operations can proceed without the database. The tradeoff is that larger index files take up more remote space and which may never be used."); } }
public static string IndexfilepolicyShort { get { return LC.L(@"Determines usage of index files"); } }
public static string IndexfilepolicyShort { get { return LC.L(@"Determine usage of index files"); } }
public static string ThresholdLong { get { return LC.L(@"As files are changed, some data stored at the remote destination may not be required. This option controls how much wasted space the destination can contain before being reclaimed. This value is a percentage used on each volume and the total storage."); } }
public static string ThresholdShort { get { return LC.L(@"The maximum wasted space in percent"); } }
public static string DryrunLong { get { return LC.L(@"This option can be used to experiment with different settings and observe the outcome without changing actual files."); } }
public static string DryrunShort { get { return LC.L(@"Does not perform any modifications"); } }
public static string BlockhashalgorithmLong { get { return LC.L(@"This is a very advanced option! This option can be used to select a block hash algorithm with smaller or larger hash size, for performance or storage space reasons."); } }
public static string DryrunLong { get { return LC.L(@"Use this option to experiment with different settings and observe the outcome without changing actual files."); } }
public static string DryrunShort { get { return LC.L(@"Do not perform any modifications"); } }
public static string BlockhashalgorithmLong { get { return LC.L(@"This is a very advanced option! Use this option to select a block hash algorithm with smaller or larger hash size, for performance or storage space reasons."); } }
public static string BlockhashalgorithmShort { get { return LC.L(@"The hash algorithm used on blocks"); } }
public static string FilehashalgorithmLong { get { return LC.L(@"This is a very advanced option! This option can be used to select a file hash algorithm with smaller or larger hash size, for performance or storage space reasons."); } }
public static string FilehashalgorithmLong { get { return LC.L(@"This is a very advanced option! Use this option to select a file hash algorithm with smaller or larger hash size, for performance or storage space reasons."); } }
public static string FilehashalgorithmShort { get { return LC.L(@"The hash algorithm used on files"); } }
public static string NoautocompactLong { get { return LC.L(@"If a large number of small files are detected during a backup, or wasted space is found after deleting backups, the remote data will be compacted. Use this option to disable such automatic compacting and only compact when running the compact command."); } }
public static string NoautocompactShort { get { return LC.L(@"Disable automatic compacting"); } }
@@ -201,7 +201,7 @@ namespace Duplicati.Library.Main.Strings
public static string PatchwithlocalblocksShort { get { return LC.L(@"Use local file data when restoring"); } }
public static string PatchwithlocalblocksDeprecated(string optionname) { return LC.L(@"Use the option --{0} instead.", optionname); }
public static string NolocaldbLong { get { return LC.L(@"When listing contents or when restoring files, the local database can be skipped. This is usually slower, but can be used to verify the actual contents of the remote store."); } }
public static string NolocaldbShort { get { return LC.L(@"Disables the local database"); } }
public static string NolocaldbShort { get { return LC.L(@"Disable the local database"); } }
public static string KeepversionsLong { get { return LC.L(@"Use this option to set number of versions to keep. Supply -1 to keep all versions."); } }
public static string KeepversionsShort { get { return LC.L(@"Keep a number of versions"); } }
public static string KeeptimeLong { get { return LC.L(@"Use this option to set the timespan in which backups are kept."); } }
@@ -214,7 +214,7 @@ namespace Duplicati.Library.Main.Strings
public static string OverwriteShort { get { return LC.L(@"Overwrite files when restoring"); } }
public static string VerboseLong { get { return LC.L(@"Use this option to increase the amount of output generated when running an option. Generally this option will produce a line for each file processed."); } }
public static string VerboseShort { get { return LC.L(@"Output more progress information"); } }
public static string VerboseDeprecated { get { return LC.L(@"Set a log-level for the desired output method instead."); } }
public static string VerboseDeprecated { get { return LC.L("Use the options --{0} and --{1} instead.", "log-file-log-level", "console-log-level"); } }
public static string FullresultLong { get { return LC.L(@"Use this option to increase the amount of output generated as the result of the operation, including all filenames."); } }
public static string FullresultShort { get { return LC.L(@"Output full results"); } }
public static string UploadverificationfileLong { get { return LC.L(@"Use this option to upload a verification file after changing the remote storage. The file is not encrypted and contains the size and SHA256 hashes of all the remote files and can be used to verify the integrity of the files."); } }
@@ -224,7 +224,7 @@ namespace Duplicati.Library.Main.Strings
public static string BackendtestpercentageLong { get { return LC.L(@"After a backup is completed, some (dblock, dindex, dlist) files from the remote backend are selected for verification. Use this option to specify the percentage (between 0 and 100) of files to test. If the option --{0} is also provided, the number of samples tested is the maximum implied by the two options. If the option --{1} is provided, no remote files are verified.", "backup-test-samples", "no-backend-verification"); } }
public static string BackendtestpercentageShort { get { return LC.L(@"The percentage of samples to test after a backup"); } }
public static string FullremoteverificationLong(string optionname) { return LC.L(@"After a backup is completed, some (dblock, dindex, dlist) files from the remote backend are selected for verification. Use this option to turn on full verification, which will decrypt the files and examine the insides of each volume, instead of simply verifying the external hash. If the option --{0} is set, no remote files are verified. This option is automatically set when then verification is performed directly. ListAndIndexes is like True but only dlist and index volumes are handled.", optionname); }
public static string FullremoteverificationShort { get { return LC.L(@"Activates in-depth verification of files"); } }
public static string FullremoteverificationShort { get { return LC.L(@"Activate in-depth verification of files"); } }
public static string FilereadbuffersizeLong { get { return LC.L(@"Use this size to control how many bytes are read from a file before processing."); } }
public static string FilereadbuffersizeShort { get { return LC.L(@"Size of the file read buffer"); } }
public static string FilereadbuffersizeDeprecated { get { return LC.L(@"The option --{0} is no longer used and has been deprecated.", "file-read-buffer-size"); } }
@@ -234,7 +234,7 @@ namespace Duplicati.Library.Main.Strings
public static string ListsetsonlyShort { get { return LC.L(@"List only filesets"); } }
public static string SkipmetadataLong { get { return LC.L(@"Use this option to disable the storage of metadata, such as file timestamps. Disabling metadata storage will speed up the backup and restore operations, but does not affect file size much."); } }
public static string SkipmetadataShort { get { return LC.L(@"Don't store metadata"); } }
public static string SkipmetadataShort { get { return LC.L(@"Do not store metadata"); } }
public static string RestorepermissionsLong { get { return LC.L(@"By default permissions are not restored as they might prevent you from accessing your files. Use this option to restore the permissions as well."); } }
public static string RestorepermissionsShort { get { return LC.L(@"Restore file permissions"); } }
public static string SkiprestoreverificationLong { get { return LC.L(@"After restoring files, the file hash of all restored files are checked to verify that the restore was successful. Use this option to disable the check and avoid waiting for the verification."); } }
@@ -251,10 +251,10 @@ namespace Duplicati.Library.Main.Strings
public static string LogretentionShort { get { return LC.L(@"Clean up old log data"); } }
public static string RepaironlypathsLong { get { return LC.L(@"Use this option to build a searchable local database which only contains path information. This option is usable for quickly building a database to locate certain content without needing to reconstruct all information. The resulting database can be searched, but cannot be used to restore data with."); } }
public static string RepaironlypathsShort { get { return LC.L(@"Repair database with paths"); } }
public static string ForcelocaleLong { get { return LC.L(@"By default, your system locale and culture settings will be used. In some cases you may prefer to run with another locale, for example to get messages in another language. This option can be used to set the locale. Supply a blank string to choose the ""Invariant Culture""."); } }
public static string ForcelocaleLong { get { return LC.L(@"By default, your system locale and culture settings will be used. In some cases you may prefer to run with another locale, for example to get messages in another language. Use this option to set the locale. Supply a blank string to choose the ""Invariant Culture""."); } }
public static string ForcelocaleShort { get { return LC.L(@"Force the locale setting"); } }
public static string ForceActualDateLong { get { return LC.L(@"By default, dates are displayed in the calendar format, meaning ""Today"" or ""Last Thursday"". By setting this option, only the actual dates are displayed, ""Nov 12, 2018, 8:01 AM"" for example."); } }
public static string ForceActualDateShort { get { return LC.L(@"Forces the display of the actual date instead of calendar date"); } }
public static string ForceActualDateShort { get { return LC.L(@"Force the display of the actual date instead of calendar date"); } }
public static string DisablepipingLong { get { return LC.L(@"Use this option to disable multithreaded handling of up- and downloads. That can significantly speed up backend operations depending on the hardware you're running on and the transfer rate of your backend."); } }
public static string DisablepipingShort { get { return LC.L(@"Handle file communication with backend using threaded pipes"); } }
public static string ConcurrencymaxthreadsLong { get { return LC.L(@"Use this option to set the maximum number of threads used. Setting this value to zero or less will dynamically balance the number of active threads to fit the hardware."); } }
@@ -264,11 +264,11 @@ namespace Duplicati.Library.Main.Strings
public static string ConcurrencycompressorsLong { get { return LC.L(@"Use this option to set the number of processes that perform compression of output data."); } }
public static string ConcurrencycompressorsShort { get { return LC.L(@"Specify the number of concurrent compression processes"); } }
public static string DisablesyntehticfilelistLong { get { return LC.L(@"If Duplicati detects that the previous backup did not complete, it will generate a filelist that is a merge of the last completed backup and the contents that were uploaded in the incomplete backup session."); } }
public static string DisablesyntheticfilelistShort { get { return LC.L(@"Disables synthetic filelist"); } }
public static string DisablesyntheticfilelistShort { get { return LC.L(@"Disable synthetic filelist"); } }
public static string CheckfiletimeonlyLong { get { return LC.L(@"This option instructs Duplicati to not look at metadata or filesize when deciding to scan a file for changes. Use this option if you have a large number of files and notice that the scanning takes a long time with unmodified files."); } }
public static string CheckfiletimeonlyShort { get { return LC.L(@"Checks only file lastmodified"); } }
public static string CheckfiletimeonlyShort { get { return LC.L(@"Check only file lastmodified"); } }
public static string DontcompressrestorepathsLong { get { return LC.L(@"When restore a subset of a backup into a new folder, the shortest possible path is used to avoid generating deep paths with empty folders. Use this option to skip this compression, such that the entire original folder structure is preserved, including upper level empty folders."); } }
public static string DontcompressrestorepathsShort { get { return LC.L(@"Disables path compression on restore"); } }
public static string DontcompressrestorepathsShort { get { return LC.L(@"Disable path compression on restore"); } }
public static string AllowfullremovalLong { get { return LC.L(@"By default, the last fileset cannot be removed. This is a safeguard to make sure that all remote data is not deleted by a configuration mistake. Use this option to disable that protection, such that all filesets can be deleted."); } }
public static string AllowfullremovalShort { get { return LC.L(@"Allow removing all filesets"); } }
public static string AutoVacuumLong { get { return LC.L(@"Some operations that manipulate the local database leave unused entries behind. These entries are not deleted from a hard drive until a VACUUM operation is run. This operation saves disk space in the long run but needs to temporarily create a copy of all valid entries in the database. Setting this to true will allow Duplicati to perform VACUUM operations at its discretion."); } }
@@ -277,23 +277,23 @@ namespace Duplicati.Library.Main.Strings
public static string DisablefilescannerShort { get { return LC.L(@"Disable the read-ahead scanner"); } }
public static string DisablefilelistconsistencychecksLong { get { return LC.L(@"In backups with a large number of filesets, the verification can take up a large part of the backup time. If you disable the checks, make sure you run regular check commands to ensure that everything is working as expected."); } }
public static string DisablefilelistconsistencychecksShort { get { return LC.L(@"Disable filelist consistency checks"); } }
public static string DisableOnBatteryLong { get { return LC.L("Use this option to run a scheduled backup if the system is detected to be running on battery power (manual or command line backups will still be run). If the detected power source is mains (e.g., AC) or unknown, then scheduled backups will proceed as normal."); } }
public static string DisableOnBatteryLong { get { return LC.L("Use this option to disable a scheduled backup if the system is detected to be running on battery power (manual or command line backups will still be run). If the detected power source is mains (e.g., AC) or unknown, then scheduled backups will proceed as normal."); } }
public static string DisableOnBatteryShort { get { return LC.L("Disable the backup when on battery power"); } }
public static string LogfileloglevelLong { get { return LC.L(@"Specifies the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
public static string LogfileloglevelLong { get { return LC.L(@"Specify the amount of log information to write into the file specified by the option --{0}.", "log-file"); } }
public static string LogfileloglevelShort { get { return LC.L(@"Log file information level"); } }
public static string LogfilelogfiltersLong(string delimiter) { return LC.L(@"This option accepts filters that removes or includes messages regardless of their log level. Multiple filters are supported by separating with {0}. Filters are matched against the log tag and assumed to be including, unless they start with '-'. Regular expressions are supported within hard braces. Example: ""+Path*{0}+*Mail*{0}-[.*DNS]"" ", delimiter); }
public static string LogfilelogfiltersShort { get { return LC.L(@"Applies filters to the file log data"); } }
public static string ConsoleloglevelLong { get { return LC.L(@"Specifies the amount of log information to output to the console."); } }
public static string LogfilelogfiltersShort { get { return LC.L(@"Apply filters to the file log data"); } }
public static string ConsoleloglevelLong { get { return LC.L(@"Specify the amount of log information to output to the console."); } }
public static string ConsoleloglevelShort { get { return LC.L(@"Console information level"); } }
public static string ConsolelogfiltersLong(string delimiter) { return LogfilelogfiltersLong(delimiter); }
public static string ConsolelogfiltersShort { get { return LC.L(@"Applies filters to the console log data"); } }
public static string ConsolelogfiltersShort { get { return LC.L(@"Apply filters to the console log data"); } }
public static string UsebackgroundiopriorityLong { get { return LC.L("This option instructs the operating system to set the current process to use the lowest IO priority level, which can make operations run slower but will interfere less with other operations running at the same time."); } }
public static string UsebackgroundiopriorityShort { get { return LC.L("Sets the process to use low IO priority"); } }
public static string UsebackgroundiopriorityShort { get { return LC.L("Set the process to use low IO priority"); } }
public static string ExcludeemptyfoldersLong { get { return LC.L("Use this option to remove all empty folders from a backup."); } }
public static string ExcludeemptyfoldersShort { get { return LC.L("Excludes empty folders"); } }
public static string ExcludeemptyfoldersShort { get { return LC.L("Exclude empty folders"); } }
public static string IgnorefilenamesLong { get { return LC.L("Use this option to set a filename, or list of filenames, that indicate exclusion of a folder which contains it. A common use would be to have a file named something like \".nobackup\" and place this file into folders that should not be backed up."); } }
public static string IgnorefilenamesShort { get { return LC.L("List of filenames that exclude folders"); } }
public static string RestoresymlinkmetadataLong { get { return LC.L("If symlink metadata is applied, it will usually mean changing the symlink target, instead of the symlink itself. For this reason, metadata is not applied to symlinks, but this option can be used to override this, such that metadata is applied to symlinks as well."); } }
@@ -302,7 +302,7 @@ namespace Duplicati.Library.Main.Strings
public static string UnittestmodeShort { get { return LC.L("Activate unittest mode"); } }
public static string ProfilealldatabasequeriesLong { get { return LC.L("To improve performance of the backups, frequent database queries are not logged by default. Enable this option to log all database queries, and remember to set either --{0}={2} or --{1}={2} to report the additional log data", "console-log-level", "log-file-log-level", nameof(Logging.LogMessageType.Profiling)); } }
public static string ProfilealldatabasequeriesShort { get { return LC.L("Activates logging of all database queries"); } }
public static string ProfilealldatabasequeriesShort { get { return LC.L("Activate logging of all database queries"); } }
public static string RebuildmissingdblockfilesLong { get { return LC.L("If dblock files are missing from the destination, you can attempt to rebuild them using local source data. However, since the local data may have changed, it may not be possible to retrieve all the required data and the process may be slow. Use this option to attempt to rebuild missing dblock files."); } }
public static string RebuildmissingdblockfilesShort { get { return LC.L("Rebuild dblock files when missing"); } }
@@ -1,23 +1,23 @@
// 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.
// 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;
@@ -47,7 +47,7 @@ namespace Duplicati.Library.Main.Volumes
public IEnumerable<string> ReadBlocklist(string hash, long hashsize)
{
return ReadBlocklist(m_compression, Library.Utility.Utility.Base64PlainToBase64Url(hash), hashsize);
return ReadBlocklistUnverified(m_compression, Library.Utility.Utility.Base64PlainToBase64Url(hash), hashsize);
}
public Stream ReadBlocklistRaw(string hash)
@@ -1,27 +1,26 @@
// 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.
// 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.Text;
using System.IO;
using Newtonsoft.Json;
using Duplicati.Library.Interface;
@@ -245,7 +244,7 @@ namespace Duplicati.Library.Main.Volumes
var n = new BlockEnumerable(m_compression, m_filename);
if (m_blocks == null)
m_blocks = n;
return n;
}
}
@@ -306,11 +305,14 @@ namespace Duplicati.Library.Main.Volumes
{
private readonly ICompression m_compression;
private readonly long m_hashsize;
private readonly string m_blockHashAlgorithm;
public IndexBlocklistEnumerable(ICompression compression, long hashsize)
public IndexBlocklistEnumerable(ICompression compression, long hashsize, string blockHashAlgorithm)
{
m_compression = compression;
m_hashsize = hashsize;
m_blockHashAlgorithm = blockHashAlgorithm;
}
private class IndexBlocklistEnumerator : IEnumerator<IIndexBlocklist>
@@ -319,15 +321,17 @@ namespace Duplicati.Library.Main.Volumes
{
private readonly ICompression m_compression;
private readonly string m_filename;
private readonly string m_blockHashAlgorithm;
private readonly long m_size;
private readonly long m_hashsize;
public IndexBlocklist(ICompression compression, string filename, long size, long hashsize)
public IndexBlocklist(ICompression compression, string filename, long size, long hashsize, string blockHashAlgorithm)
{
m_compression = compression;
m_filename = filename;
m_size = size;
m_hashsize = hashsize;
m_blockHashAlgorithm = blockHashAlgorithm;
}
public string Hash
@@ -341,16 +345,14 @@ namespace Duplicati.Library.Main.Volumes
{
get { return m_size; }
}
public Stream Data
{
get { return m_compression.OpenRead(m_filename); }
}
public IEnumerable<string> Blocklist
{
get { return VolumeReaderBase.ReadBlocklist(m_compression, m_filename, m_hashsize); }
}
=> ReadBlocklistVerified(m_compression, m_filename, m_hashsize, Hash, m_blockHashAlgorithm);
}
private readonly ICompression m_compression;
@@ -358,11 +360,13 @@ namespace Duplicati.Library.Main.Volumes
private KeyValuePair<string, long>[] m_files;
private IndexBlocklist m_current;
private readonly long m_hashsize;
private readonly string m_blockHashAlgorithm;
public IndexBlocklistEnumerator(ICompression compression, long hashsize)
public IndexBlocklistEnumerator(ICompression compression, long hashsize, string blockHashAlgorithm)
{
m_compression = compression;
m_hashsize = hashsize;
m_blockHashAlgorithm = blockHashAlgorithm;
this.Reset();
}
@@ -389,7 +393,7 @@ namespace Duplicati.Library.Main.Volumes
while (m_index < m_files.Length && IsValidBase64Hash(m_files[m_index].Key, m_hashsize))
m_index++;
m_current = new IndexBlocklist(m_compression, m_files[m_index].Key, m_files[m_index].Value, m_hashsize);
m_current = new IndexBlocklist(m_compression, m_files[m_index].Key, m_files[m_index].Value, m_hashsize, m_blockHashAlgorithm);
return true;
}
@@ -402,7 +406,7 @@ namespace Duplicati.Library.Main.Volumes
}
}
public IEnumerator<IIndexBlocklist> GetEnumerator() { return new IndexBlocklistEnumerator(m_compression, m_hashsize); }
public IEnumerator<IIndexBlocklist> GetEnumerator() { return new IndexBlocklistEnumerator(m_compression, m_hashsize, m_blockHashAlgorithm); }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); }
}
@@ -421,6 +425,6 @@ namespace Duplicati.Library.Main.Volumes
}
public IEnumerable<IIndexBlockVolume> Volumes { get { return new IndexBlockVolumeEnumerable(m_compression); } }
public IEnumerable<IIndexBlocklist> BlockLists { get { return new IndexBlocklistEnumerable(m_compression, m_hashsize); } }
public IEnumerable<IIndexBlocklist> BlockLists { get { return new IndexBlocklistEnumerable(m_compression, m_hashsize, m_blockhash); } }
}
}
@@ -19,11 +19,11 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Main.Volumes
{
@@ -170,7 +170,14 @@ namespace Duplicati.Library.Main.Volumes
}
}
public static IEnumerable<string> ReadBlocklist(ICompression compression, string filename, long hashsize)
/// <summary>
/// Reads the blocklist from the file, not checking if the hash is correct
/// </summary>
/// <param name="compression">The compression to use</param>
/// <param name="filename">The file to read the blocklist from</param>
/// <param name="hashsize">The size of the hash</param>
/// <returns>The blocklist</returns>
public static IEnumerable<string> ReadBlocklistUnverified(ICompression compression, string filename, long hashsize)
{
var buffer = new byte[hashsize];
using (var fs = compression.OpenRead(filename))
@@ -188,6 +195,38 @@ namespace Duplicati.Library.Main.Volumes
}
}
/// <summary>
/// Read blocklist and check the hash. Throws InvalidDataException if not matching
/// </summary>
/// <param name="compression">The compression to use</param>
/// <param name="filename">The file to read the blocklist from</param>
/// <param name="hashsize">The size of the hash</param>
/// <param name="hash">The hash to check against</param>
/// <param name="blockHashAlgorithm">The block hash algorithm to use</param>
public static IEnumerable<string> ReadBlocklistVerified(ICompression compression, string filename, long hashsize, string hash, string blockHashAlgorithm)
{
var buffer = new byte[hashsize];
using var hashalg = HashFactory.CreateHasher(blockHashAlgorithm);
using var fs = compression.OpenRead(filename);
int s;
var read = 0L;
while ((s = Library.Utility.Utility.ForceStreamRead(fs, buffer, buffer.Length)) != 0)
{
if (s != buffer.Length)
throw new InvalidDataException($"Premature End-of-stream encountered while reading blocklist hashes for {filename}. Got {s} bytes of {buffer.Length} at offset {read * buffer.Length}");
read++;
hashalg.TransformBlock(buffer, 0, s, buffer, 0);
yield return Convert.ToBase64String(buffer);
}
hashalg.TransformFinalBlock(buffer, 0, 0);
var calculatedHash = Convert.ToBase64String(hashalg.Hash);
if (hash != calculatedHash)
throw new InvalidDataException($"Blocklist hash does not match: expected {hash}, got {calculatedHash}");
}
protected static object SkipJsonToken(JsonReader reader, JsonToken type)
{
if (!reader.Read() || reader.TokenType != type)
@@ -37,6 +37,7 @@ public static class GenericModules
new RunScript(),
new SendHttpMessage(),
new SendJabberMessage(),
new SendTelegramMessage(),
new SendMail(),
];
}
@@ -212,7 +212,7 @@ namespace Duplicati.Library.Modules.Builtin
if (!commandlineOptions.TryGetValue(OPTION_RESULT_FORMAT, out var format))
format = ResultExportFormat.Duplicati.ToString();
if (!Enum.TryParse<ResultExportFormat>(format, out var exportFormat))
if (!Enum.TryParse<ResultExportFormat>(format, true, out var exportFormat))
exportFormat = ResultExportFormat.Duplicati;
commandlineOptions.TryGetValue(OPTION_VERB, out var verb);
@@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Duplicati.Library.Interface;
namespace Duplicati.Library.Modules.Builtin;
public class SendTelegramMessage : ReportHelper
{
/// <summary>
/// The tag used for log messages
/// </summary>
private static readonly string LOGTAG = Logging.Log.LogTagFromType<SendTelegramMessage>();
/// <summary>
/// The timeout for the HTTP request
/// </summary>
private static readonly TimeSpan REQUEST_TIMEOUT = TimeSpan.FromSeconds(10);
#region Option names
/// <summary>
/// Option used to specify Telegram bot ID
/// </summary>
private const string OPTION_BOTID = "send-telegram-bot-id";
/// <summary>
/// Option used to specify Telegram bot API key
/// </summary>
private const string OPTION_APIKEY = "send-telegram-api-key";
/// <summary>
/// Option used to specify channel to send to
/// </summary>
private const string OPTION_CHANNEL = "send-telegram-channel-id";
/// <summary>
/// Option used to specify report body
/// </summary>
private const string OPTION_MESSAGE = "send-telegram-message";
/// <summary>
/// Option used to specify report level
/// </summary>
private const string OPTION_SENDLEVEL = "send-telegram-level";
/// <summary>
/// Option used to specify if reports are sent for other operations than backups
/// </summary>
private const string OPTION_SENDALL = "send-telegram-any-operation";
/// <summary>
/// Option used to specify what format the result is sent in.
/// </summary>
private const string OPTION_RESULT_FORMAT = "send-telegram-result-output-format";
/// <summary>
/// Option used to set the log level
/// </summary>
private const string OPTION_LOG_LEVEL = "send-telegram-log-level";
/// <summary>
/// Option used to set the log level
/// </summary>
private const string OPTION_LOG_FILTER = "send-telegram-log-filter";
/// <summary>
/// Option used to set the maximum number of log lines
/// </summary>
private const string OPTION_MAX_LOG_LINES = "send-telegram-max-log-lines";
#endregion
#region Option defaults
/// <summary>
/// The default message body
/// </summary>
protected override string DEFAULT_BODY => string.Format("Duplicati %OPERATIONNAME% report for %backup-name%{0}{0} %RESULT%", Environment.NewLine);
/// <summary>
/// Don't use the subject for telegram
/// </summary>
protected override string DEFAULT_SUBJECT => string.Empty;
#endregion
#region Implementation of IGenericModule
/// <summary>
/// The module key, used to activate or deactivate the module on the commandline
/// </summary>
public override string Key => "sendtelegram";
/// <summary>
/// A localized string describing the module with a friendly name
/// </summary>
public override string DisplayName => Strings.SendTelegramMessage.DisplayName;
/// <summary>
/// A localized description of the module
/// </summary>
public override string Description => Strings.SendTelegramMessage.Description;
/// <summary>
/// A boolean value that indicates if the module should always be loaded.
/// If true, the user can choose to not load the module by entering the appropriate commandline option.
/// If false, the user can choose to load the module by entering the appropriate commandline option.
/// </summary>
public override bool LoadAsDefault => true;
/// <summary>
/// Gets a list of supported commandline arguments
/// </summary>
public override IList<ICommandLineArgument> SupportedCommands
=> [
new CommandLineArgument(OPTION_CHANNEL, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegramchannelShort, Strings.SendTelegramMessage.SendtelegramchannelLong),
new CommandLineArgument(OPTION_MESSAGE, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegrammessageShort, Strings.SendTelegramMessage.SendtelegrammessageLong, DEFAULT_BODY),
new CommandLineArgument(OPTION_BOTID, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegrambotidShort, Strings.SendTelegramMessage.SendtelegrambotidLong),
new CommandLineArgument(OPTION_APIKEY, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegramapikeyShort, Strings.SendTelegramMessage.SendtelegramapikeyLong),
new CommandLineArgument(OPTION_SENDLEVEL, CommandLineArgument.ArgumentType.String, Strings.SendTelegramMessage.SendtelegramlevelShort, Strings.SendTelegramMessage.SendtelegramlevelLong(ParsedResultType.Success.ToString(), ParsedResultType.Warning.ToString(), ParsedResultType.Error.ToString(), ParsedResultType.Fatal.ToString(), "All"), DEFAULT_LEVEL, null, Enum.GetNames(typeof(ParsedResultType)).Union(new string[] { "All" } ).ToArray()),
new CommandLineArgument(OPTION_SENDALL, CommandLineArgument.ArgumentType.Boolean, Strings.SendTelegramMessage.SendtelegramanyoperationShort, Strings.SendTelegramMessage.SendtelegramanyoperationLong),
new CommandLineArgument(OPTION_LOG_LEVEL, CommandLineArgument.ArgumentType.Enumeration, Strings.ReportHelper.OptionLoglevelShort, Strings.ReportHelper.OptionLoglevelLong, DEFAULT_LOG_LEVEL.ToString(), null, Enum.GetNames(typeof(Logging.LogMessageType))),
new CommandLineArgument(OPTION_LOG_FILTER, CommandLineArgument.ArgumentType.String, Strings.ReportHelper.OptionLogfilterShort, Strings.ReportHelper.OptionLogfilterLong),
new CommandLineArgument(OPTION_MAX_LOG_LINES, CommandLineArgument.ArgumentType.Integer, Strings.ReportHelper.OptionmaxloglinesShort, Strings.ReportHelper.OptionmaxloglinesLong, DEFAULT_LOGLINES.ToString()),
new CommandLineArgument(OPTION_RESULT_FORMAT, CommandLineArgument.ArgumentType.Enumeration, Strings.ReportHelper.ResultFormatShort, Strings.ReportHelper.ResultFormatLong(Enum.GetNames(typeof(ResultExportFormat))), DEFAULT_EXPORT_FORMAT.ToString(), null, Enum.GetNames(typeof(ResultExportFormat))),
];
protected override string SubjectOptionName => OPTION_MESSAGE;
protected override string BodyOptionName => OPTION_MESSAGE;
protected override string ActionLevelOptionName => OPTION_SENDLEVEL;
protected override string ActionOnAnyOperationOptionName => OPTION_SENDALL;
protected override string LogLevelOptionName => OPTION_LOG_LEVEL;
protected override string LogFilterOptionName => OPTION_LOG_FILTER;
protected override string LogLinesOptionName => OPTION_MAX_LOG_LINES;
protected override string ResultFormatOptionName => OPTION_RESULT_FORMAT;
/// <summary>
/// The server username
/// </summary>
private string m_botid;
/// <summary>
/// The server password
/// </summary>
private string m_apikey;
/// <summary>
/// The Telegram ChannelID
/// </summary>
private string m_channelId;
/// <summary>
/// This method is the interception where the module can interact with the execution environment and modify the settings.
/// </summary>
/// <param name="commandlineOptions">A set of commandline options passed to Duplicati</param>
protected override bool ConfigureModule(IDictionary<string, string> commandlineOptions)
{
//We need at least a recipient
commandlineOptions.TryGetValue(OPTION_CHANNEL, out m_channelId);
if (string.IsNullOrEmpty(m_channelId))
return false;
commandlineOptions.TryGetValue(OPTION_BOTID, out m_botid);
commandlineOptions.TryGetValue(OPTION_APIKEY, out m_apikey);
return true;
}
#endregion
protected override string ReplaceTemplate(string input, object result, Exception exception, bool subjectline)
{
// No need to do the expansion as we throw away the result
if (subjectline)
return string.Empty;
return base.ReplaceTemplate(input, result, exception, subjectline);
}
protected override async void SendMessage(string subject, string body)
{
try
{
var p = new
{
chat_id = Uri.EscapeDataString(m_channelId),
parse_mode = "Text",
text = Uri.EscapeDataString(body),
botId = Uri.EscapeDataString(m_botid),
apiKey = Uri.EscapeDataString(m_apikey)
};
var url = $"https://api.telegram.org/bot{p.botId}:{p.apiKey}/sendMessage?chat_id={p.chat_id}&parse_mode={p.parse_mode}&text={p.text}";
using var client = new HttpClient { Timeout = REQUEST_TIMEOUT };
var response = await client.GetAsync(url);
var responseContent = await response.Content.ReadAsStringAsync();
if (responseContent.Contains("\"ok\":true"))
return;
Logging.Log.WriteWarningMessage(LOGTAG, "telegramSendError", null, "Failed to send to telegram messages: {0}", responseContent);
}
catch (Exception e)
{
Logging.Log.WriteWarningMessage(LOGTAG, "telegramSendError", e, "Failed to send to telegram messages: {0}", e.Message);
}
}
}
+59 -30
View File
@@ -50,13 +50,13 @@ namespace Duplicati.Library.Modules.Builtin.Strings
public static string OauthurlLong { get { return LC.L(@"Duplicati uses an external server to support the OAuth authentication flow. If you have set up your own Duplicati OAuth server, you can supply the refresh URL."); } }
public static string OauthurlShort { get { return LC.L(@"Alternate OAuth URL"); } }
public static string SslversionsLong { get { return LC.L(@"This option changes the default SSL versions allowed. This is an advanced option and should only be used if you want to enhance security or work around an issue with a particular SSL protocol."); } }
public static string SslversionsShort { get { return LC.L(@"Sets allowed SSL versions"); } }
public static string SslversionsShort { get { return LC.L(@"Set allowed SSL versions"); } }
public static string OperationtimeoutLong { get { return LC.L(@"This option changes the default timeout for any HTTP request, the time covers the entire operation from initial packet to shutdown."); } }
public static string OperationtimeoutShort { get { return LC.L(@"Sets the default operation timeout"); } }
public static string OperationtimeoutShort { get { return LC.L(@"Set the default operation timeout"); } }
public static string ReadwritetimeoutLong { get { return LC.L(@"This option changes the default read-write timeout. Read-write timeouts are used to detect a stalled requests, and this option configures the maximum time between activity on a connection."); } }
public static string ReadwritetimeoutShort { get { return LC.L(@"Sets readwrite"); } }
public static string ReadwritetimeoutShort { get { return LC.L(@"Set readwrite"); } }
public static string BufferrequestsLong { get { return LC.L(@"This option sets the HTTP buffering. Setting this to ""{0}"" can cause memory leaks, but can also improve performance in some cases.", "true"); } }
public static string BufferrequestsShort { get { return LC.L(@"Sets HTTP buffering"); } }
public static string BufferrequestsShort { get { return LC.L(@"Set HTTP buffering"); } }
}
internal static class HyperVOptions
{
@@ -70,23 +70,23 @@ namespace Duplicati.Library.Modules.Builtin.Strings
}
internal static class RunScript
{
public static string Description { get { return LC.L(@"Executes a script before starting an operation, and again on completion"); } }
public static string Description { get { return LC.L(@"Execute a script before starting an operation, and again on completion"); } }
public static string DisplayName { get { return LC.L(@"Run script"); } }
public static string FinishoptionLong { get { return LC.L(@"Executes a script after performing an operation. The script will receive the operation results written to stdout."); } }
public static string FinishoptionLong { get { return LC.L(@"Execute a script after performing an operation. The script will receive the operation results written to stdout."); } }
public static string FinishoptionShort { get { return LC.L(@"Run a script on exit"); } }
public static string InvalidExitCodeError(string script, int exitcode) { return LC.L(@"The script ""{0}"" returned with exit code {1}", script, exitcode); }
public static string ExitCodeError(string script, int exitcode, string message) { return LC.L(@"The script ""{0}"" returned with exit code {1}{2}", script, exitcode, string.IsNullOrWhiteSpace(message) ? string.Empty : string.Format(": {0}", message)); }
public static string RequiredoptionLong { get { return LC.L(@"Executes a script before performing an operation. The operation will block until the script has completed or timed out. If the script returns a non-zero error code or times out, the operation will be aborted."); } }
public static string RequiredoptionLong { get { return LC.L(@"Execute a script before performing an operation. The operation will block until the script has completed or timed out. If the script returns a non-zero error code or times out, the operation will be aborted."); } }
public static string RequiredoptionShort { get { return LC.L(@"Run a required script on startup"); } }
public static string ResultFormatLong(IEnumerable<string> options) { return LC.L(@"Selects the output format for results. Available formats: {0}", string.Join(", ", options)); }
public static string ResultFormatShort { get { return LC.L(@"Selects the output format for results"); } }
public static string ResultFormatLong(IEnumerable<string> options) { return LC.L(@"Use this option to select the output format for results. Available formats: {0}", string.Join(", ", options)); }
public static string ResultFormatShort { get { return LC.L(@"Select the output format for results"); } }
public static string ScriptExecuteError(string script, string message) { return LC.L(@"Error while executing script ""{0}"": {1}", script, message); }
public static string ScriptTimeoutError(string script) { return LC.L(@"Execution of the script ""{0}"" timed out", script); }
public static string StartupoptionLong { get { return LC.L(@"Executes a script before performing an operation. The operation will block until the script has completed or timed out."); } }
public static string StartupoptionLong { get { return LC.L(@"Execute a script before performing an operation. The operation will block until the script has completed or timed out."); } }
public static string StartupoptionShort { get { return LC.L(@"Run a script on startup"); } }
public static string StdErrorReport(string script, string message) { return LC.L(@"The script ""{0}"" reported error messages: {1}", script, message); }
public static string TimeoutoptionLong { get { return LC.L(@"Sets the maximum time a script is allowed to execute. If the script has not completed within this time, it will continue to execute but the operation will continue too, and no script output will be processed."); } }
public static string TimeoutoptionShort { get { return LC.L(@"Sets the script timeout"); } }
public static string TimeoutoptionLong { get { return LC.L(@"Set the maximum time a script is allowed to execute. If the script has not completed within this time, it will continue to execute but the operation will continue too, and no script output will be processed."); } }
public static string TimeoutoptionShort { get { return LC.L(@"Set the script timeout"); } }
public static string EnableArgumentsLong { get { return LC.L(@"This option enables the use of script arguments. If this option is set, the script arguments are treated as commandline strings. Use single or double quotes to separate arguments."); } }
public static string EnableArgumentsShort { get { return LC.L(@"Enable script arguments"); } }
}
@@ -94,7 +94,7 @@ namespace Duplicati.Library.Modules.Builtin.Strings
{
public static string Description { get { return LC.L(@"This module can send email after an operation completes"); } }
public static string Displayname { get { return LC.L(@"Send mail"); } }
public static string FailedToLookupMXServer(string optionname) { return LC.L(@"Unable to find the destination mail server through MX lookup. Please use the option {0} to specify what smtp server to use.", optionname); }
public static string FailedToLookupMXServer(string optionname) { return LC.L(@"Unable to find the destination mail server through MX lookup. Please use the option --{0} to specify what SMTP server to use.", optionname); }
public static string OptionBodyLong { get { return LC.L(@"This value can be a filename. If the file exists, the file contents will be used as the message body.
In the message body, certain tokens are replaced:
@@ -105,14 +105,14 @@ In the message body, certain tokens are replaced:
All command line options are also reported within %value%, e.g. %volsize%. Any unknown/unset value is removed."); } }
public static string OptionBodyShort { get { return LC.L(@"The message body"); } }
public static string OptionPasswordLong { get { return LC.L(@"The password used to authenticate with the SMTP server if required."); } }
public static string OptionPasswordLong { get { return LC.L(@"Use this option to set the password used to authenticate with the SMTP server if required."); } }
public static string OptionPasswordShort { get { return LC.L(@"SMTP Password"); } }
public static string OptionRecipientLong { get { return LC.L(@"This setting is required if mail should be sent, all other settings have default values. You can supply multiple email addresses separated with commas, and you can use the normal address format as specified by RFC2822 section 3.4.
Example with 3 recipients:
Peter Sample <peter@example.com>, John Sample <john@example.com>, admin@example.com"); } }
public static string OptionRecipientShort { get { return LC.L(@"Email recipient(s)"); } }
public static string OptionSenderLong { get { return LC.L(@"Address of the email sender. If no host is supplied, the hostname of the first recipient is used. Examples of allowed formats:
public static string OptionSenderLong { get { return LC.L(@"Use this option to set an address of the email sender. If no host is supplied, the hostname of the first recipient is used. Examples of allowed formats:
sender
sender@example.com
@@ -122,14 +122,14 @@ Mail Sender <sender@example.com>"); } }
public static string OptionSendlevelLong(string success, string warning, string error, string fatal, string all) { return LC.L(@"You can specify one of ""{0}"", ""{1}"", ""{2}"", ""{3}"".
You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The special value ""{4}"" is a shorthand for ""{0},{1},{2},{3}"" and will cause all backup operations to send an email.", success, warning, error, fatal, all); }
public static string OptionSendlevelShort { get { return LC.L(@"The messages to send"); } }
public static string OptionServerLong { get { return LC.L(@"A URL for the SMTP server, e.g. smtp://example.com:25. Multiple servers can be supplied in a prioritized list, separated with semicolon. If a server fails, the next server in the list is tried, until the message has been sent.
public static string OptionServerLong { get { return LC.L(@"Use this option to set a URL for the SMTP server, e.g. smtp://example.com:25. Multiple servers can be supplied in a prioritized list, separated with semicolon. If a server fails, the next server in the list is tried, until the message has been sent.
If no server is supplied, a DNS lookup is performed to find the first recipient's MX record, and all SMTP servers are tried in their priority order until the message is sent.
To enable SMTP over SSL, use the format smtps://example.com. To enable SMTP STARTTLS, use the format smtp://example.com:25/?starttls=when-available or smtp://example.com:25/?starttls=always. If no port is specified, port 25 is used for non-ssl, and 465 for SSL connections. To force not to use STARTTLS use smtp://example.com:25/?starttls=never."); } }
public static string OptionServerShort { get { return LC.L(@"SMTP Url"); } }
public static string OptionSubjectLong(string optionname) { return LC.L(@"This setting supplies the email subject. Values are replaced as described in the description for --{0}.", optionname); }
public static string OptionSubjectShort { get { return LC.L(@"The email subject"); } }
public static string OptionUsernameLong { get { return LC.L(@"The username used to authenticate with the SMTP server if required."); } }
public static string OptionUsernameLong { get { return LC.L(@"Use this option to set the username used to authenticate with the SMTP server if required."); } }
public static string OptionUsernameShort { get { return LC.L(@"SMTP Username"); } }
public static string SendMailLog(string message) { return LC.L(@"Whole SMTP communication: {0}", message); }
public static string SendMailFailedRetryError(string failedserver, string message, string retryserver) { return LC.L(@"Failed to send email with server: {0}, message: {1}, retrying with {2}", failedserver, message, retryserver); }
@@ -139,7 +139,7 @@ To enable SMTP over SSL, use the format smtps://example.com. To enable SMTP STAR
{
public static string Description { get { return LC.L(@"This module provides support for sending status reports via XMPP messages"); } }
public static string DisplayName { get { return LC.L(@"XMPP report module"); } }
public static string SendxmpptoLong { get { return LC.L(@"The users who should have the messages sent. You can specify multiple users separated with commas."); } }
public static string SendxmpptoLong { get { return LC.L(@"Use this option to set the users who should have the messages sent. You can specify multiple users separated with commas."); } }
public static string SendxmpptoShort { get { return LC.L(@"XMPP recipient email"); } }
public static string SendxmppmessageLong { get { return LC.L(@"This value can be a filename. If the file exists, the file contents will be used as the message.
@@ -151,9 +151,9 @@ In the message, certain tokens are replaced:
All command line options are also reported within %value%, e.g. %volsize%. Any unknown/unset value is removed."); } }
public static string SendxmppmessageShort { get { return LC.L(@"The message template"); } }
public static string SendxmppusernameLong { get { return LC.L(@"The username for the account that will send the message, including the hostname, e.g. ""account@jabber.org/Home"""); } }
public static string SendxmppusernameLong { get { return LC.L(@"Use this option to set a username for the account that will send the message, including the hostname, e.g. ""account@jabber.org/Home"""); } }
public static string SendxmppusernameShort { get { return LC.L(@"The XMPP username"); } }
public static string SendxmpppasswordLong { get { return LC.L(@"The password for the account that will send the message."); } }
public static string SendxmpppasswordLong { get { return LC.L(@"Use this option to set a password for the account that will send the message."); } }
public static string SendxmpppasswordShort { get { return LC.L(@"The XMPP password"); } }
public static string SendxmpplevelLong(string success, string warning, string error, string fatal, string all) { return LC.L(@"You can specify one of ""{0}"", ""{1}"", ""{2}"", ""{3}"".
You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The special value ""{4}"" is a shorthand for ""{0},{1},{2},{3}"" and will cause all backup operations to send a message.", success, warning, error, fatal, all); }
@@ -163,11 +163,40 @@ You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The sp
public static string LoginTimeoutError { get { return LC.L(@"Timeout occurred while logging in to jabber server"); } }
}
internal static class SendTelegramMessage
{
public static string Description { get { return LC.L(@"This module provides support for sending status reports via Telegram messages"); } }
public static string DisplayName { get { return LC.L(@"Telegram report module"); } }
public static string SendtelegramchannelLong { get { return LC.L(@"Use this option to set the channel ID."); } }
public static string SendtelegramchannelShort { get { return LC.L(@"Telegram channel ID"); } }
public static string SendtelegrammessageLong { get { return LC.L(@"This value can be a filename. If the file exists, the file contents will be used as the message.
In the message, certain tokens are replaced:
%OPERATIONNAME% - The name of the operation, normally ""Backup""
%REMOTEURL% - Remote server URL
%LOCALPATH% - The path to the local files or folders involved in the operation (if any)
%PARSEDRESULT% - The parsed result, if the operation is a backup. Possible values are: Error, Warning, Success
All command line options are also reported within %value%, e.g. %volsize%. Any unknown/unset value is removed."); } }
public static string SendtelegrammessageShort { get { return LC.L(@"The message template"); } }
public static string SendtelegrambotidLong { get { return LC.L(@"Use this option to set a bot ID for the bot that will send the message."); } }
public static string SendtelegrambotidShort { get { return LC.L(@"The Telegram bot ID"); } }
public static string SendtelegramapikeyLong { get { return LC.L(@"Use this option to set a API key for the bot that will send the message."); } }
public static string SendtelegramapikeyShort { get { return LC.L(@"The Telegram API key"); } }
public static string SendtelegramlevelLong(string success, string warning, string error, string fatal, string all) { return LC.L(@"You can specify one of ""{0}"", ""{1}"", ""{2}"", ""{3}"".
You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The special value ""{4}"" is a shorthand for ""{0},{1},{2},{3}"" and will cause all backup operations to send a message.", success, warning, error, fatal, all); }
public static string SendtelegramlevelShort { get { return LC.L(@"The messages to send"); } }
public static string SendtelegramanyoperationLong { get { return LC.L(@"By default, messages will only be sent after a backup operation. Use this option to send messages for all operations."); } }
public static string SendtelegramanyoperationShort { get { return LC.L(@"Send messages for all operations"); } }
public static string LoginTimeoutError { get { return LC.L(@"Timeout occurred while sending to Telegram server"); } }
}
internal static class SendHttpMessage
{
public static string Description { get { return LC.L(@"This module provides support for sending status reports via HTTP messages"); } }
public static string DisplayName { get { return LC.L(@"HTTP report module"); } }
public static string SendhttpurlLong { get { return LC.L(@"HTTP report URL."); } }
public static string SendhttpurlLong { get { return LC.L(@"Use this option to set a HTTP report URL."); } }
public static string SendhttpurlShort { get { return LC.L(@"HTTP report URL"); } }
public static string SendhttpmessageLong { get { return LC.L(@"This value can be a filename. If the file exists, the file contents will be used as the message.
@@ -179,9 +208,9 @@ In the message, certain tokens are replaced:
All command line options are also reported within %value%, e.g. %volsize%. Any unknown/unset value is removed."); } }
public static string SendhttpmessageShort { get { return LC.L(@"The message template"); } }
public static string SendhttpmessageparameternameLong { get { return LC.L(@"The name of the parameter to send the message as."); } }
public static string SendhttpmessageparameternameLong { get { return LC.L(@"Use this option to set a name of the parameter to send the message as."); } }
public static string SendhttpmessageparameternameShort { get { return LC.L(@"The name of the parameter to send the message as"); } }
public static string SendhttpextraparametersLong { get { return LC.L(@"Extra parameters to add to the http message, e.g. ""parameter1=value1&parameter2=value2"""); } }
public static string SendhttpextraparametersLong { get { return LC.L(@"Use this option to set extra parameters to add to the http message, e.g. ""parameter1=value1&parameter2=value2"""); } }
public static string SendhttpextraparametersShort { get { return LC.L(@"Extra parameters to add to the http message"); } }
public static string SendhttplevelLong(string success, string warning, string error, string fatal, string all) { return LC.L(@"You can specify one of ""{0}"", ""{1}"", ""{2}"", ""{3}"".
You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The special value ""{4}"" is a shorthand for ""{0},{1},{2},{3}"" and will cause all backup operations to send a message.", success, warning, error, fatal, all); }
@@ -189,10 +218,10 @@ You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The sp
public static string SendhttpanyoperationLong { get { return LC.L(@"By default, messages will only be sent after a backup operation. Use this option to send messages for all operations."); } }
public static string SendhttpanyoperationShort { get { return LC.L(@"Send messages for all operations"); } }
public static string HttpverbLong { get { return LC.L(@"Use this option to change the default HTTP verb used to submit a report."); } }
public static string HttpverbShort { get { return LC.L(@"Sets the HTTP verb to use"); } }
public static string SendhttpurlsformLong { get { return LC.L(@"HTTP report URLs for sending form-encoded data. This property accepts multiple URLs, seperated by a semi-colon. All URLs will receive the same data. Note that this option ignores the format and verb settings."); } }
public static string HttpverbShort { get { return LC.L(@"Set the HTTP verb to use"); } }
public static string SendhttpurlsformLong { get { return LC.L(@"Use this option to set HTTP report URLs for sending form-encoded data. This option accepts multiple URLs, seperated by a semi-colon. All URLs will receive the same data. Note that this option ignores the format and verb settings."); } }
public static string SendhttpurlsformShort { get { return LC.L(@"HTTP report URLs for sending form data"); } }
public static string SendhttpurlsjsonLong { get { return LC.L(@"HTTP report URLs for sending JSON data. This property accepts multiple URLs, seperated by a semi-colon. All URLs will receive the same data. Note that this option ignores the format and verb settings."); } }
public static string SendhttpurlsjsonLong { get { return LC.L(@"Use this option to set HTTP report URLs for sending JSON data. This option accepts multiple URLs, seperated by a semi-colon. All URLs will receive the same data. Note that this option ignores the format and verb settings."); } }
public static string SendhttpurlsjsonShort { get { return LC.L(@"HTTP report URLs for sending JSON data"); } }
}
@@ -200,12 +229,12 @@ You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The sp
{
public static string SendMessageFailedError(string message) { return LC.L(@"Failed to send message: {0}", message); }
public static string OptionLoglevelLong { get { return LC.L("Use this option to set the log level for messages to include in the report."); } }
public static string OptionLoglevelShort { get { return LC.L("Defines a log level for messages"); } }
public static string OptionLoglevelShort { get { return LC.L("Define a log level for messages"); } }
public static string OptionLogfilterLong { get { return LC.L("Use this option to set a filter expression that defines what options are included in the report."); } }
public static string OptionLogfilterShort { get { return LC.L("Log message filter"); } }
public static string OptionmaxloglinesLong { get { return LC.L("Use this option to set the maximum number of log lines to include in the report. Zero or negative values means unlimited."); } }
public static string OptionmaxloglinesShort { get { return LC.L("Limits log lines"); } }
public static string ResultFormatLong(IEnumerable<string> options) { return LC.L(@"Selects the output format for results. Available formats: {0}", string.Join(", ", options)); }
public static string ResultFormatShort { get { return LC.L(@"Selects the output format for results"); } }
public static string OptionmaxloglinesShort { get { return LC.L("Limit log lines"); } }
public static string ResultFormatLong(IEnumerable<string> options) { return LC.L(@"Use this option to select the output format for results. Available formats: {0}", string.Join(", ", options)); }
public static string ResultFormatShort { get { return LC.L(@"Select the output format for results"); } }
}
}
@@ -14,8 +14,16 @@ public interface IScheduler
/// <param name="worker">The worker thread</param>
void Init(WorkerThread<Runner.IRunnerData> worker);
/// <summary>
/// Gets the current ids in the scheduler queue
/// </summary>
IList<Tuple<long, string>> GetSchedulerQueueIds();
/// <summary>
/// Gets the current proposed schedule
/// </summary>
IList<Tuple<string, DateTime>> GetProposedSchedule();
/// <summary>
/// Terminates the thread. Any items still in queue will be removed
/// </summary>
@@ -48,29 +48,25 @@ public static class BackupImportExportHandler
return data;
}
public static Server.Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func<string> getPassword, Dictionary<string, string> advancedOptions)
public static Server.Serializable.ImportExportStructure ImportBackup(Connection connection, string configurationFile, bool importMetadata, Func<string> getPassword)
{
// This removes the ID and DBPath from the backup configuration.
Server.Serializable.ImportExportStructure importedStructure = LoadConfiguration(configurationFile, importMetadata, getPassword);
// This will create the Duplicati-server.sqlite database file if it doesn't exist.
using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions))
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
{
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists.");
}
string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule);
if (!string.IsNullOrWhiteSpace(error))
{
throw new InvalidOperationException(error);
}
// This creates a new ID and DBPath.
connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule);
throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists.");
}
string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule);
if (!string.IsNullOrWhiteSpace(error))
{
throw new InvalidOperationException(error);
}
// This creates a new ID and DBPath.
connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule);
return importedStructure;
}
@@ -25,20 +25,40 @@ using System.Linq;
using Duplicati.Server.Serialization.Interface;
using System.Text;
using Duplicati.Library.RestAPI;
using Duplicati.Library.Encryption;
using Duplicati.Library.DynamicLoader;
using Duplicati.Library.Main;
namespace Duplicati.Server.Database
{
public class Connection : IDisposable
{
private readonly System.Data.IDbConnection m_connection;
private System.Data.IDbCommand m_errorcmd;
private readonly System.Data.IDbCommand m_errorcmd;
public readonly object m_lock = new object();
public const int ANY_BACKUP_ID = -1;
public const int SERVER_SETTINGS_ID = -2;
private readonly Dictionary<string, Backup> m_temporaryBackups = new Dictionary<string, Backup>();
private readonly bool m_encryptSensitiveFields;
private static readonly HashSet<string> _encryptedFields =
BackendLoader.Backends.SelectMany(x => x.SupportedCommands ?? [])
.Concat(EncryptionLoader.Modules.SelectMany(x => x.SupportedCommands ?? []))
.Concat(CompressionLoader.Modules.SelectMany(x => x.SupportedCommands ?? []))
.Concat(GenericLoader.Modules.SelectMany(x => x.SupportedCommands ?? []))
.Concat(WebLoader.Modules.SelectMany(x => x.SupportedCommands ?? []))
.Concat(new Options(new Dictionary<string, string>()).SupportedCommands)
.Where(x => x.Type == Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password)
.SelectMany(x => new string[] { x.Name }.Concat(x.Aliases ?? []))
.SelectMany(x => new string[] { x, $"--{x}" })
.Concat([
ServerSettings.CONST.JWT_CONFIG,
ServerSettings.CONST.PBKDF_CONFIG
])
.ToHashSet(StringComparer.OrdinalIgnoreCase);
public Connection(System.Data.IDbConnection connection)
public Connection(System.Data.IDbConnection connection, bool disableFieldEncryption)
{
m_encryptSensitiveFields = !disableFieldEncryption;
m_connection = connection;
m_errorcmd = m_connection.CreateCommand();
m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)";
@@ -48,6 +68,53 @@ namespace Duplicati.Server.Database
this.ApplicationSettings = new ServerSettings(this);
}
public void ReWriteAllFieldsIfEncryptionChanged()
{
// The token is automatically decrypted when the settings are loaded
// In case the password has changed, this will fail and return the encrypted
// hex-string, but will crash before reaching this point
if (this.ApplicationSettings.EncryptedFields != m_encryptSensitiveFields)
{
var backups = this.Backups;
foreach (var b in backups)
{
((Backup)b).LoadChildren(this);
AddOrUpdateBackup(b, false, null);
}
this.SetSettings(this.GetSettings(ANY_BACKUP_ID), ANY_BACKUP_ID);
this.ApplicationSettings.EncryptedFields = m_encryptSensitiveFields;
}
}
public void SetPreloadSettingsIfChanged(Dictionary<string, string> newsettings)
{
if (newsettings == null || newsettings.Count == 0)
return;
var settingsHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(newsettings.OrderBy(x => x.Key)))));
if (settingsHash == this.ApplicationSettings.PreloadSettingsHash)
return;
newsettings = newsettings
.ToDictionary(x => x.Key.StartsWith("--") ? x.Key : $"--{x.Key}", x => x.Value);
var currentSettings = this.Settings;
var filters = currentSettings.Where(x => x.Filter != null).ToDictionary(x => x.Name, x => x.Filter);
var updatedSettings = currentSettings
.Where(x => !newsettings.ContainsKey(x.Name))
.Concat(newsettings.Where(x => x.Value != null).Select(x => new Setting
{
Name = x.Key,
Value = x.Value,
Filter = filters.GetValueOrDefault(x.Key) ?? ""
}));
this.Settings = updatedSettings.ToArray();
this.ApplicationSettings.PreloadSettingsHash = settingsHash;
}
public void LogError(string backupid, string message, Exception ex)
{
lock (m_lock)
@@ -194,7 +261,7 @@ namespace Duplicati.Server.Database
{
Filter = ConvertToString(rd, 0) ?? "",
Name = ConvertToString(rd, 1) ?? "",
Value = ConvertToString(rd, 2) ?? ""
Value = DecryptSensitiveFields(ConvertToString(rd, 2) ?? "")
//TODO: Attach the argument information
},
@"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = ?", id)
@@ -206,6 +273,14 @@ namespace Duplicati.Server.Database
lock (m_lock)
using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
{
if (m_encryptSensitiveFields)
values = values.Select(x => new Setting
{
Filter = x.Filter,
Name = x.Name,
Value = EncryptSensitiveFields(x.Name, x.Value)
}).ToList();
OverwriteAndUpdateDb(
tr,
@"DELETE FROM ""Option"" WHERE ""BackupID"" = ?", new object[] { id },
@@ -300,7 +375,7 @@ namespace Duplicati.Server.Database
Name = ConvertToString(rd, 1),
Description = ConvertToString(rd, 2),
Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
TargetURL = ConvertToString(rd, 4),
TargetURL = EncryptedFieldHelper.Decrypt(ConvertToString(rd, 4)),
DBPath = ConvertToString(rd, 5),
},
@"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = ?", id)
@@ -558,7 +633,7 @@ namespace Duplicati.Server.Database
n.Name,
n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome
string.Join(",", n.Tags ?? new string[0]),
n.TargetURL,
m_encryptSensitiveFields ? EncryptedFieldHelper.Encrypt(n.TargetURL) : n.TargetURL,
update ? item.ID : n.DBPath
};
});
@@ -728,7 +803,7 @@ namespace Duplicati.Server.Database
Name = ConvertToString(rd, 1),
Description = ConvertToString(rd, 2),
Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
TargetURL = ConvertToString(rd, 4),
TargetURL = EncryptedFieldHelper.Decrypt(ConvertToString(rd, 4)),
DBPath = ConvertToString(rd, 5),
},
@"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" ")
@@ -1254,23 +1329,45 @@ namespace Duplicati.Server.Database
}
}
/// <summary>
/// Encrypts sensitive fields
/// </summary>
/// <param name="fieldName">The fieldname used to determine if it will be encrypted</param>
/// <param name="fieldValue">The field value</param>
/// <returns>The encrypted string or the original value</returns>
private static string EncryptSensitiveFields(string fieldName, string fieldValue)
{
if (fieldValue != null)
return _encryptedFields.Contains(fieldName)
? EncryptedFieldHelper.Encrypt(fieldValue)
: fieldValue;
return null;
}
/// <summary>
/// Decrypts sensitive fields
/// </summary>
/// <param name="fieldValue">The field value</param>
/// <returns>The decrypted string</returns>
private static string DecryptSensitiveFields(string fieldValue)
{
if (fieldValue != null)
return EncryptedFieldHelper.IsEncryptedString(fieldValue)
? EncryptedFieldHelper.Decrypt(fieldValue)
: fieldValue;
return null;
}
#region IDisposable implementation
public void Dispose()
{
if (m_errorcmd != null)
try { if (m_errorcmd != null) m_errorcmd.Dispose(); }
catch { }
finally { m_errorcmd = null; }
try { m_errorcmd?.Dispose(); }
catch { }
try
{
if (m_connection != null)
m_connection.Dispose();
}
catch
{
}
try { m_connection?.Dispose(); }
catch { }
}
#endregion
}

Some files were not shown because too many files have changed in this diff Show More