mirror of
https://github.com/duplicati/duplicati.git
synced 2026-05-06 07:16:38 -04:00
Remove Sia backend
The Sia backend implementation is based on the previous version of Sia and will no longer work after the hard fork later this year.
This commit is contained in:
@@ -83,8 +83,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Tools", "Duplicat
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Jottacloud", "Duplicati\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj", "{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Sia", "Duplicati\Library\Backend\Sia\Duplicati.Library.Backend.Sia.csproj", "{32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Rclone", "Duplicati\Library\Backend\Rclone\Duplicati.Library.Backend.Rclone.csproj", "{851A1CB8-3CEB-41B4-956F-34D760D2A8E5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Common", "Duplicati\Library\Common\Duplicati.Library.Common.csproj", "{D63E53E4-A458-4C2F-914D-92F715F58ACF}"
|
||||
@@ -356,10 +354,6 @@ Global
|
||||
{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -567,7 +561,6 @@ Global
|
||||
{59C8BBC5-6E42-46FB-AB3E-6C183A82459A} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{B20A7CEE-9C5B-47B9-8B76-BC85ADFE8493} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{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}
|
||||
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{545DD6D4-9476-42D6-B51C-A28E000C489E} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<Copyright>Copyright © 2025 Team Duplicati, MIT license</Copyright>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Interface\Duplicati.Library.Interface.csproj" />
|
||||
<ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj" />
|
||||
<ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj" />
|
||||
<ProjectReference Include="..\..\Backend\OAuthHelper\Duplicati.Library.OAuthHelper.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,478 +0,0 @@
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Library.Utility.Options;
|
||||
using Newtonsoft.Json;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Duplicati.Library.Backend.Sia
|
||||
{
|
||||
public class Sia : IBackend
|
||||
{
|
||||
private const string SIA_PASSWORD = "sia-password";
|
||||
private const string SIA_TARGETPATH = "sia-targetpath";
|
||||
private const string SIA_REDUNDANCY = "sia-redundancy";
|
||||
|
||||
private readonly string m_apihost;
|
||||
private readonly int m_apiport;
|
||||
private readonly string m_targetpath;
|
||||
private readonly float m_redundancy;
|
||||
private readonly string? m_authorization;
|
||||
private readonly TimeoutOptionsHelper.Timeouts m_timeouts;
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
// This constructor is needed by the BackendLoader.
|
||||
public Sia()
|
||||
{
|
||||
m_apihost = null!;
|
||||
m_targetpath = null!;
|
||||
m_timeouts = null!;
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
// This constructor is needed by the BackendLoader.
|
||||
public Sia(string url, Dictionary<string, string?> options)
|
||||
{
|
||||
var uri = new Utility.Uri(url);
|
||||
|
||||
m_apihost = uri.Host;
|
||||
m_apiport = uri.Port;
|
||||
m_targetpath = uri.Path;
|
||||
m_timeouts = TimeoutOptionsHelper.Parse(options);
|
||||
|
||||
m_redundancy = 1.5F;
|
||||
var redundancyString = options.GetValueOrDefault(SIA_REDUNDANCY);
|
||||
if (!string.IsNullOrWhiteSpace(redundancyString))
|
||||
m_redundancy = (float)decimal.Parse(redundancyString, CultureInfo.InvariantCulture);
|
||||
|
||||
if (m_apiport <= 0)
|
||||
m_apiport = 9980;
|
||||
|
||||
var targetPathString = options.GetValueOrDefault(SIA_TARGETPATH);
|
||||
if (!string.IsNullOrWhiteSpace(targetPathString))
|
||||
m_targetpath = targetPathString;
|
||||
|
||||
while (m_targetpath.Contains("//"))
|
||||
m_targetpath = m_targetpath.Replace("//", "/");
|
||||
while (m_targetpath.StartsWith("/", StringComparison.Ordinal))
|
||||
m_targetpath = m_targetpath.Substring(1);
|
||||
while (m_targetpath.EndsWith("/", StringComparison.Ordinal))
|
||||
m_targetpath = m_targetpath.Remove(m_targetpath.Length - 1);
|
||||
|
||||
if (m_targetpath.Length == 0)
|
||||
m_targetpath = "backup";
|
||||
|
||||
m_authorization = options.ContainsKey(SIA_PASSWORD) && !string.IsNullOrEmpty(options[SIA_PASSWORD])
|
||||
? "Basic " + Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(":" + options[SIA_PASSWORD]))
|
||||
: null;
|
||||
}
|
||||
|
||||
private HttpWebRequest CreateRequest(string endpoint)
|
||||
{
|
||||
string baseurl = "http://" + m_apihost + ":" + m_apiport;
|
||||
var req = (HttpWebRequest)WebRequest.Create(baseurl + endpoint);
|
||||
|
||||
if (m_authorization != null)
|
||||
{
|
||||
// Manually set Authorization header, since System.Net.NetworkCredential ignores credentials with empty usernames
|
||||
req.Headers.Add("Authorization", m_authorization);
|
||||
}
|
||||
|
||||
req.KeepAlive = false;
|
||||
req.UserAgent = string.Format("Sia-Agent (Duplicati SIA client {0})", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
private async Task<string> GetResponseBodyOnError(string context, WebException wex, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = wex.Response as HttpWebResponse;
|
||||
if (response is null)
|
||||
return $"{context} failed with error: {wex.Message}";
|
||||
|
||||
var body = await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancellationToken, _ =>
|
||||
{
|
||||
using (var data = response.GetResponseStream())
|
||||
using (var reader = new StreamReader(data))
|
||||
return reader.ReadToEnd();
|
||||
}).ConfigureAwait(false);
|
||||
return string.Format("{0} failed, response: {1}", context, body);
|
||||
}
|
||||
|
||||
public class SiaFile
|
||||
{
|
||||
[JsonProperty("siapath")]
|
||||
public string? Siapath { get; set; }
|
||||
[JsonProperty("available")]
|
||||
public bool Available { get; set; }
|
||||
[JsonProperty("filesize")]
|
||||
public long Filesize { get; set; }
|
||||
[JsonProperty("uploadprogress")]
|
||||
public float Uploadprogress { get; set; }
|
||||
[JsonProperty("redundancy")]
|
||||
public float Redundancy { get; set; }
|
||||
}
|
||||
|
||||
public class SiaFileList
|
||||
{
|
||||
[JsonProperty("files")]
|
||||
public SiaFile[]? Files { get; set; }
|
||||
}
|
||||
|
||||
public class SiaDownloadFile
|
||||
{
|
||||
[JsonProperty("siapath")]
|
||||
public string? Siapath { get; set; }
|
||||
[JsonProperty("destination")]
|
||||
public string? Destination { get; set; }
|
||||
[JsonProperty("filesize")]
|
||||
public long Filesize { get; set; }
|
||||
[JsonProperty("received")]
|
||||
public long Received { get; set; }
|
||||
[JsonProperty("starttime")]
|
||||
public string? Starttime { get; set; }
|
||||
[JsonProperty("error")]
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
|
||||
public class SiaDownloadList
|
||||
{
|
||||
[JsonProperty("downloads")]
|
||||
public SiaDownloadFile[]? Files { get; set; }
|
||||
}
|
||||
|
||||
private async Task<SiaFileList> GetFiles(CancellationToken cancelToken)
|
||||
{
|
||||
// Remove warning until this is rewritten to use HttpClient
|
||||
await Task.CompletedTask;
|
||||
|
||||
var fl = new SiaFileList();
|
||||
var endpoint = "/renter/files";
|
||||
|
||||
try
|
||||
{
|
||||
var req = CreateRequest(endpoint);
|
||||
req.Method = WebRequestMethods.Http.Get;
|
||||
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
|
||||
using (var resp = await Utility.Utility.WithTimeout(m_timeouts.ListTimeout, cancelToken, _ => (HttpWebResponse)areq.GetResponse()).ConfigureAwait(false))
|
||||
{
|
||||
int code = (int)resp.StatusCode;
|
||||
if (code < 200 || code >= 300)
|
||||
throw new WebException(resp.StatusDescription, null, WebExceptionStatus.ProtocolError, resp);
|
||||
|
||||
var serializer = new JsonSerializer();
|
||||
|
||||
using (var rs = await Utility.Utility.WithTimeout(m_timeouts.ListTimeout, cancelToken, _ => areq.GetResponseStream()).ConfigureAwait(false))
|
||||
using (var sr = new StreamReader(rs))
|
||||
using (var jr = new JsonTextReader(sr))
|
||||
fl = serializer.Deserialize<SiaFileList>(jr)
|
||||
?? throw new Exception("Failed to deserialize response");
|
||||
}
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
throw new Exception(await GetResponseBodyOnError(endpoint, wex, cancelToken).ConfigureAwait(false));
|
||||
}
|
||||
return fl;
|
||||
}
|
||||
|
||||
private async Task<bool> IsUploadComplete(string siafilename, CancellationToken cancelToken)
|
||||
{
|
||||
var fl = await GetFiles(cancelToken).ConfigureAwait(false);
|
||||
if (fl.Files == null)
|
||||
return false;
|
||||
|
||||
foreach (var f in fl.Files)
|
||||
{
|
||||
if (f.Siapath == siafilename)
|
||||
{
|
||||
if (f.Available == true && f.Redundancy >= m_redundancy /* && f.Uploadprogress >= 100 */ )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<SiaDownloadList> GetDownloads(CancellationToken cancelToken)
|
||||
{
|
||||
var fl = new SiaDownloadList();
|
||||
string endpoint = "/renter/downloads";
|
||||
|
||||
try
|
||||
{
|
||||
var req = CreateRequest(endpoint);
|
||||
req.Method = WebRequestMethods.Http.Get;
|
||||
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
|
||||
using (var resp = await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, _ => (HttpWebResponse)areq.GetResponse()).ConfigureAwait(false))
|
||||
{
|
||||
int code = (int)resp.StatusCode;
|
||||
if (code < 200 || code >= 300)
|
||||
throw new WebException(resp.StatusDescription, null, WebExceptionStatus.ProtocolError, resp);
|
||||
|
||||
var serializer = new JsonSerializer();
|
||||
|
||||
using (var rs = await Utility.Utility.WithTimeout(m_timeouts.ListTimeout, cancelToken, _ => areq.GetResponseStream()))
|
||||
using (var sr = new StreamReader(rs))
|
||||
using (var jr = new JsonTextReader(sr))
|
||||
fl = serializer.Deserialize<SiaDownloadList>(jr)
|
||||
?? throw new Exception("Failed to deserialize response");
|
||||
}
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
throw new Exception(await GetResponseBodyOnError(endpoint, wex, cancelToken).ConfigureAwait(false));
|
||||
}
|
||||
return fl;
|
||||
}
|
||||
|
||||
private async Task<bool> IsDownloadComplete(string siafilename, string localname, CancellationToken cancelToken)
|
||||
{
|
||||
var fl = await GetDownloads(cancelToken).ConfigureAwait(false);
|
||||
if (fl.Files == null)
|
||||
return false;
|
||||
|
||||
foreach (var f in fl.Files)
|
||||
{
|
||||
if (f.Siapath == siafilename)
|
||||
{
|
||||
if (f.Error != "")
|
||||
{
|
||||
throw new Exception("failed to download " + siafilename + "err: " + f.Error);
|
||||
}
|
||||
if (f.Filesize == f.Received)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Sia seems to keep the file open/locked for a while, make sure we can open it
|
||||
using (var fs = new FileStream(localname, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
fs.Close();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#region IBackend Members
|
||||
|
||||
public Task TestAsync(CancellationToken cancelToken)
|
||||
=> this.TestListAsync(cancelToken);
|
||||
|
||||
public Task CreateFolderAsync(CancellationToken cancelToken)
|
||||
{
|
||||
// Dummy method, Sia doesn't have folders
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string DisplayName => Strings.Sia.DisplayName;
|
||||
|
||||
public string ProtocolKey => "sia";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<IFileEntry> ListAsync([EnumeratorCancellation] CancellationToken cancelToken)
|
||||
{
|
||||
SiaFileList fl;
|
||||
try
|
||||
{
|
||||
fl = await GetFiles(cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
throw new Exception("failed to call /renter/files " + wex.Message);
|
||||
}
|
||||
|
||||
if (fl.Files != null)
|
||||
{
|
||||
foreach (var f in fl.Files)
|
||||
{
|
||||
// Sia returns a complete file list, but we're only interested in files that are
|
||||
// in our target path
|
||||
if (f.Siapath != null && f.Siapath.StartsWith(m_targetpath, StringComparison.Ordinal))
|
||||
{
|
||||
var fe = new FileEntry(f.Siapath.Substring(m_targetpath.Length + 1))
|
||||
{
|
||||
Size = f.Filesize,
|
||||
IsFolder = false
|
||||
};
|
||||
yield return fe;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
|
||||
{
|
||||
var endpoint = "";
|
||||
var siafile = m_targetpath + "/" + remotename;
|
||||
|
||||
try
|
||||
{
|
||||
endpoint = string.Format("/renter/upload/{0}/{1}?source={2}",
|
||||
m_targetpath,
|
||||
Utility.Uri.UrlEncode(remotename).Replace("+", "%20"),
|
||||
Utility.Uri.UrlEncode(filename).Replace("+", "%20")
|
||||
);
|
||||
|
||||
var req = CreateRequest(endpoint);
|
||||
req.Method = WebRequestMethods.Http.Post;
|
||||
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
|
||||
using (var resp = (HttpWebResponse)areq.GetResponse())
|
||||
{
|
||||
int code = (int)resp.StatusCode;
|
||||
if (code < 200 || code >= 300)
|
||||
throw new WebException(resp.StatusDescription, null, WebExceptionStatus.ProtocolError, resp);
|
||||
|
||||
while (!await IsUploadComplete(siafile, cancelToken).ConfigureAwait(false))
|
||||
await Task.Delay(5000, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
throw new Exception(await GetResponseBodyOnError(endpoint, wex, cancelToken).ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GetAsync(string remotename, string localname, CancellationToken cancelToken)
|
||||
{
|
||||
string endpoint = "";
|
||||
string siafile = m_targetpath + "/" + remotename;
|
||||
string tmpfilename = localname + ".tmp";
|
||||
|
||||
try
|
||||
{
|
||||
endpoint = string.Format("/renter/download/{0}/{1}?destination={2}",
|
||||
m_targetpath,
|
||||
Utility.Uri.UrlEncode(remotename).Replace("+", "%20"),
|
||||
Utility.Uri.UrlEncode(tmpfilename).Replace("+", "%20")
|
||||
);
|
||||
var req = CreateRequest(endpoint);
|
||||
req.Method = WebRequestMethods.Http.Get;
|
||||
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
|
||||
using (var resp = (HttpWebResponse)areq.GetResponse())
|
||||
{
|
||||
int code = (int)resp.StatusCode;
|
||||
if (code < 200 || code >= 300)
|
||||
throw new WebException(resp.StatusDescription, null, WebExceptionStatus.ProtocolError, resp);
|
||||
|
||||
while (!await IsDownloadComplete(siafile, localname, cancelToken).ConfigureAwait(false))
|
||||
await Task.Delay(5000, cancelToken).ConfigureAwait(false);
|
||||
|
||||
File.Copy(tmpfilename, localname, true);
|
||||
try
|
||||
{
|
||||
File.Delete(tmpfilename);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
if (wex.Response is HttpWebResponse response && response.StatusCode == HttpStatusCode.NotFound)
|
||||
throw new FileMissingException(wex);
|
||||
else
|
||||
throw new Exception(await GetResponseBodyOnError(endpoint, wex, cancelToken).ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string remotename, CancellationToken cancellationToken)
|
||||
{
|
||||
string endpoint = "";
|
||||
|
||||
try
|
||||
{
|
||||
endpoint = string.Format("/renter/delete/{0}/{1}",
|
||||
m_targetpath,
|
||||
Utility.Uri.UrlEncode(remotename).Replace("+", "%20")
|
||||
);
|
||||
var req = CreateRequest(endpoint);
|
||||
req.Method = WebRequestMethods.Http.Post;
|
||||
|
||||
await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancellationToken, _ =>
|
||||
{
|
||||
var areq = new AsyncHttpRequest(req);
|
||||
using (var resp = (HttpWebResponse)areq.GetResponse())
|
||||
{
|
||||
int code = (int)resp.StatusCode;
|
||||
if (code < 200 || code >= 300)
|
||||
throw new WebException(resp.StatusDescription, null, WebExceptionStatus.ProtocolError, resp);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
if (wex.Response is HttpWebResponse response && response.StatusCode == HttpStatusCode.NotFound)
|
||||
throw new FileMissingException(wex);
|
||||
else
|
||||
throw new Exception(await GetResponseBodyOnError(endpoint, wex, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public IList<ICommandLineArgument> SupportedCommands => new List<ICommandLineArgument>([
|
||||
new CommandLineArgument(SIA_TARGETPATH, CommandLineArgument.ArgumentType.String, Strings.Sia.SiaPathDescriptionShort, Strings.Sia.SiaPathDescriptionLong, "/backup"),
|
||||
new CommandLineArgument(SIA_PASSWORD, CommandLineArgument.ArgumentType.Password, Strings.Sia.SiaPasswordShort, Strings.Sia.SiaPasswordLong, null),
|
||||
new CommandLineArgument(SIA_REDUNDANCY, CommandLineArgument.ArgumentType.Decimal, Strings.Sia.SiaRedundancyDescriptionShort, Strings.Sia.SiaRedundancyDescriptionLong, "1.5"),
|
||||
.. TimeoutOptionsHelper.GetOptions()
|
||||
.Where(x => x.Name != TimeoutOptionsHelper.ReadWriteTimeoutOption)
|
||||
]);
|
||||
|
||||
public string Description => Strings.Sia.Description;
|
||||
|
||||
public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] { new System.Uri(m_apihost).Host });
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Members
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.Localization.Short;
|
||||
namespace Duplicati.Library.Backend.Strings
|
||||
{
|
||||
internal static class Sia
|
||||
{
|
||||
public static string Description => LC.L(@"This backend can read and write data to Sia.");
|
||||
public static string DisplayName => LC.L(@"Sia Decentralized Cloud");
|
||||
public static string SiaPathDescriptionLong => LC.L(@"Set the target path. Example: /backup");
|
||||
public static string SiaPathDescriptionShort => LC.L(@"Backup path");
|
||||
public static string SiaPasswordLong => LC.L(@"Supply a password for Sia server.");
|
||||
public static string SiaPasswordShort => LC.L(@"Sia password");
|
||||
public static string SiaRedundancyDescriptionLong => LC.L(@"The minimum value for redundancy is 1.0.");
|
||||
public static string SiaRedundancyDescriptionShort => LC.L(@"Set the minimum redundancy");
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,6 @@ public static class BackendModules
|
||||
new Backend.OneDriveForBusinessBackend(),
|
||||
new Backend.SharePointBackend(),
|
||||
new Backend.SharePointV2(),
|
||||
new Backend.Sia.Sia(),
|
||||
IsStorjSupported? new Backend.Storj.Storj() : null,
|
||||
new Backend.TahoeBackend(),
|
||||
new Backend.TencentCOS.COS(),
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<ProjectReference Include="..\Backend\Rclone\Duplicati.Library.Backend.Rclone.csproj" />
|
||||
<ProjectReference Include="..\Backend\S3\Duplicati.Library.Backend.S3.csproj" />
|
||||
<ProjectReference Include="..\Backend\SharePoint\Duplicati.Library.Backend.SharePoint.csproj" />
|
||||
<ProjectReference Include="..\Backend\Sia\Duplicati.Library.Backend.Sia.csproj" />
|
||||
<ProjectReference Include="..\Backend\SSHv2\Duplicati.Library.Backend.SSHv2.csproj" />
|
||||
<ProjectReference Include="..\Backend\Storj\Duplicati.Library.Backend.Storj.csproj" />
|
||||
<ProjectReference Include="..\Backend\TahoeLAFS\Duplicati.Library.Backend.TahoeLAFS.csproj" />
|
||||
|
||||
@@ -359,7 +359,7 @@ namespace Duplicati.Library.Main.Operation
|
||||
|
||||
// Start the progress updater
|
||||
using (new Logging.Timer(LOGTAG, "RestoreNetworkWait", "RestoreNetworkWait"))
|
||||
using (var kill_updater = new CancellationTokenSource())
|
||||
using (var kill_updater = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var updater = Task.Run(async () =>
|
||||
{
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
@@ -33,7 +33,6 @@ namespace Duplicati.Server.Database
|
||||
private readonly string[] UrlPasswords = {
|
||||
"authid",
|
||||
"auth-password",
|
||||
"sia-password",
|
||||
"storj-secret",
|
||||
"storj-shared-access",
|
||||
};
|
||||
@@ -135,6 +134,7 @@ namespace Duplicati.Server.Database
|
||||
// breaks assumptions made by the decode_uri function in AppUtils.js. Since we are simply
|
||||
// removing password parameters, we will leave the parameters as they are in the target URL.
|
||||
filteredParameters = Library.Utility.Uri.ParseQueryString(url.Query, false);
|
||||
// TODO: This list is not exhaustive, look at the BackendLoader and find all password fields + aliases
|
||||
foreach (string field in this.UrlPasswords)
|
||||
{
|
||||
filteredParameters.Remove(field);
|
||||
|
||||
@@ -28,7 +28,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
|
||||
EditUriBackendConfig.templates['idrive'] = 'templates/backends/idrive.html';
|
||||
EditUriBackendConfig.templates['box'] = 'templates/backends/oauth.html';
|
||||
EditUriBackendConfig.templates['dropbox'] = 'templates/backends/oauth.html';
|
||||
EditUriBackendConfig.templates['sia'] = 'templates/backends/sia.html';
|
||||
EditUriBackendConfig.templates['storj'] = 'templates/backends/storj.html';
|
||||
EditUriBackendConfig.templates['rclone'] = 'templates/backends/rclone.html';
|
||||
EditUriBackendConfig.templates['cos'] = 'templates/backends/cos.html';
|
||||
@@ -543,20 +542,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
|
||||
delete options[nukeopts[x]];
|
||||
}
|
||||
|
||||
|
||||
EditUriBackendConfig.parsers['sia'] = function (scope, module, server, port, path, options) {
|
||||
if (options['--sia-targetpath'])
|
||||
scope.sia_targetpath = options['--sia-targetpath'];
|
||||
if (options['--sia-redundancy'])
|
||||
scope.sia_redundancy = options['--sia-redundancy'];
|
||||
if (options['--sia-password'])
|
||||
scope.sia_password = options['--sia-password'];
|
||||
|
||||
var nukeopts = ['--sia-targetpath', '--sia-redundancy', '--sia-password'];
|
||||
for (var x in nukeopts)
|
||||
delete options[nukeopts[x]];
|
||||
}
|
||||
|
||||
EditUriBackendConfig.parsers['storj'] = function (scope, module, server, port, path, options) {
|
||||
if (options['--storj-auth-method'])
|
||||
scope.storj_auth_method = options['--storj-auth-method'];
|
||||
@@ -893,25 +878,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
|
||||
return url;
|
||||
};
|
||||
|
||||
EditUriBackendConfig.builders['sia'] = function (scope) {
|
||||
var opts = {
|
||||
'sia-password': scope.sia_password,
|
||||
'sia-targetpath': scope.sia_targetpath,
|
||||
'sia-redundancy': scope.sia_redundancy
|
||||
};
|
||||
|
||||
EditUriBackendConfig.merge_in_advanced_options(scope, opts, false);
|
||||
|
||||
var url = AppUtils.format('{0}://{1}/{2}{3}',
|
||||
scope.Backend.Key,
|
||||
scope.Server || '',
|
||||
scope.sia_targetpath || '',
|
||||
AppUtils.encodeDictAsUrl(opts)
|
||||
);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
EditUriBackendConfig.builders['storj'] = function (scope) {
|
||||
var opts = {
|
||||
'storj-auth-method': scope.storj_auth_method,
|
||||
@@ -1334,22 +1300,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
|
||||
if (res)
|
||||
continuation();
|
||||
};
|
||||
|
||||
EditUriBackendConfig.validaters['sia'] = function (scope, continuation) {
|
||||
var res =
|
||||
EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Server'));
|
||||
|
||||
var re = new RegExp('^(([a-zA-Z0-9-])|(\/(?!\/)))*$');
|
||||
if (res && !re.test(scope['sia_targetpath'])) {
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('Invalid characters in path'));
|
||||
}
|
||||
|
||||
if (res && (scope['sia_redundancy'] || '').trim().length == 0 || parseFloat(scope['sia_redundancy']) < 1.0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('Minimum redundancy is 1.0')); // Do not forget to update SiaRedundancyDescriptionLong as well if the value is changed
|
||||
|
||||
if (res)
|
||||
continuation();
|
||||
};
|
||||
|
||||
EditUriBackendConfig.validaters['storj'] = function (scope, continuation) {
|
||||
var res = true;
|
||||
|
||||
@@ -71,7 +71,6 @@ backupApp.service('SystemInfo', function($rootScope, $timeout, $cookies, AppServ
|
||||
'od4b': null,
|
||||
'mssp': null,
|
||||
'dropbox': null,
|
||||
'sia': null,
|
||||
'storj': null,
|
||||
'jottacloud': null,
|
||||
'rclone': null,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<div class="input text">
|
||||
<label for="sia_server" translate>Server</label>
|
||||
<input type="text" id="sia_server" ng-model="$parent.Server" placeholder="{{'127.0.0.1:9980'}}" />
|
||||
</div>
|
||||
<div class="input text">
|
||||
<label for="sia_path" translate>Folder path</label>
|
||||
<input type="text" name="sia_targetpath" id="sia_targetpath" ng-model="$parent.sia_targetpath" placeholder="{{'Target path. Example: /backup' | translate}}" />
|
||||
</div>
|
||||
<div class="input password">
|
||||
<label for="sia_password" translate>Server password</label>
|
||||
<input autocomplete="new-password" type="password" name="sia_password" id="sia_password" ng-model="$parent.sia_password" placeholder="{{'Sia server password' | translate}}" />
|
||||
</div>
|
||||
<div class="input text">
|
||||
<label for="sia_redundancy" translate>Minimum redundancy</label>
|
||||
<input type="text" name="sia_redundancy" id="sia_redundancy" ng-model="$parent.sia_redundancy" placeholder="{{'1.5'}}" />
|
||||
</div>
|
||||
<div class="note">
|
||||
<p translate>Choose 1.0 for fast backup, 1.5 for decent reliability, 2.0 for safer upload but slow backup.</p>
|
||||
<p translate><b>Note:</b> Sia will still boost redundancy later as long as you're connected to your hosts.</p>
|
||||
</div>
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
Duplicati は、フリー(自由)でオープンソースのバックアップ用クライアントです。圧縮し、暗号化した増分バックアップを、クラウドストレージサービスや遠隔のファイルサーバー上に安全に保存できます。Duplicati は、主に以下のサービスやソフトウェアで使うことができます。
|
||||
|
||||
_Amazon S3、[IDrive e2](https://www.idrive.com/e2/duplicati "Using Duplicati with IDrive e2")、[Backblaze (B2)](https://www.backblaze.com/blog/duplicati-backups-cloud-storage/ "Duplicati with Backblaze B2 Cloud Storage")、Box、Dropbox、FTP、Google クラウド、Google ドライブ、MEGA、Microsoft Azure、Microsoft OneDrive、Rackspace Cloud Files、OpenStack Storage (Swift)、Sia、Storj DCS、SSH (SFTP)、WebDAV、Tencent クラウドオブジェクトストレージ(COS)、Aliyun OSS、[その他にも対応しています!](https://docs.duplicati.com/backup-destinations/destination-overview)_
|
||||
_Amazon S3、[IDrive e2](https://www.idrive.com/e2/duplicati "Using Duplicati with IDrive e2")、[Backblaze (B2)](https://www.backblaze.com/blog/duplicati-backups-cloud-storage/ "Duplicati with Backblaze B2 Cloud Storage")、Box、Dropbox、FTP、Google クラウド、Google ドライブ、MEGA、Microsoft Azure、Microsoft OneDrive、Rackspace Cloud Files、OpenStack Storage (Swift)、Storj DCS、SSH (SFTP)、WebDAV、Tencent クラウドオブジェクトストレージ(COS)、Aliyun OSS、[その他にも対応しています!](https://docs.duplicati.com/backup-destinations/destination-overview)_
|
||||
|
||||
Duplicati は MIT ライセンスで公開されており、Windows、OSX、Linux で利用できます(.NET 4.7.1 以上、または Mono 5.10.0 以上が必要です)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user