Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/duplicati/duplicati.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'Duplicati')
-rw-r--r--Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj4
-rw-r--r--Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj4
-rw-r--r--Duplicati/CommandLine/Duplicati.CommandLine.csproj4
-rw-r--r--Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj4
-rw-r--r--Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj4
-rw-r--r--Duplicati/Library/Backend/Backblaze/B2.cs520
-rw-r--r--Duplicati/Library/Backend/Backblaze/B2AuthHelper.cs168
-rw-r--r--Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj65
-rw-r--r--Duplicati/Library/Backend/Backblaze/Duplicati.snkbin0 -> 596 bytes
-rw-r--r--Duplicati/Library/Backend/Backblaze/Properties/AssemblyInfo.cs43
-rw-r--r--Duplicati/Library/Backend/Backblaze/Strings.cs19
-rw-r--r--Duplicati/Server/Duplicati.Server.csproj4
-rw-r--r--Duplicati/Server/webroot/greeno/scripts/edituri.js2
-rw-r--r--Duplicati/Server/webroot/greeno/scripts/plugins.js49
-rw-r--r--Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js19
-rw-r--r--Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js4
-rw-r--r--Duplicati/Server/webroot/ngax/templates/backends/b2.html18
17 files changed, 928 insertions, 3 deletions
diff --git a/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj b/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj
index bdb12e541..8f91eb454 100644
--- a/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj
+++ b/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj
@@ -149,6 +149,10 @@
<Project>{D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}</Project>
<Name>Duplicati.Library.Logging</Name>
</ProjectReference>
+ <ProjectReference Include="..\..\Library\Backend\Backblaze\Duplicati.Library.Backend.Backblaze.csproj">
+ <Project>{61C43D61-4368-4942-84A3-1EB623F4EF2A}</Project>
+ <Name>Duplicati.Library.Backend.Backblaze</Name>
+ </ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
diff --git a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj
index 1dce391ef..031a8d129 100644
--- a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj
+++ b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj
@@ -123,6 +123,10 @@
<Project>{08D7E42D-285C-4010-9881-986125FE2F3E}</Project>
<Name>Duplicati.Library.Backend.AmazonCloudDrive</Name>
</ProjectReference>
+ <ProjectReference Include="..\..\Library\Backend\Backblaze\Duplicati.Library.Backend.Backblaze.csproj">
+ <Project>{61C43D61-4368-4942-84A3-1EB623F4EF2A}</Project>
+ <Name>Duplicati.Library.Backend.Backblaze</Name>
+ </ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
diff --git a/Duplicati/CommandLine/Duplicati.CommandLine.csproj b/Duplicati/CommandLine/Duplicati.CommandLine.csproj
index 71ecbf151..2aecc1c08 100644
--- a/Duplicati/CommandLine/Duplicati.CommandLine.csproj
+++ b/Duplicati/CommandLine/Duplicati.CommandLine.csproj
@@ -169,6 +169,10 @@
<Project>{08D7E42D-285C-4010-9881-986125FE2F3E}</Project>
<Name>Duplicati.Library.Backend.AmazonCloudDrive</Name>
</ProjectReference>
+ <ProjectReference Include="..\Library\Backend\Backblaze\Duplicati.Library.Backend.Backblaze.csproj">
+ <Project>{61C43D61-4368-4942-84A3-1EB623F4EF2A}</Project>
+ <Name>Duplicati.Library.Backend.Backblaze</Name>
+ </ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="Duplicati.snk" />
diff --git a/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj
index 3b932c180..d4355ea9c 100644
--- a/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj
+++ b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj
@@ -131,6 +131,10 @@
<Project>{08D7E42D-285C-4010-9881-986125FE2F3E}</Project>
<Name>Duplicati.Library.Backend.AmazonCloudDrive</Name>
</ProjectReference>
+ <ProjectReference Include="..\..\Library\Backend\Backblaze\Duplicati.Library.Backend.Backblaze.csproj">
+ <Project>{61C43D61-4368-4942-84A3-1EB623F4EF2A}</Project>
+ <Name>Duplicati.Library.Backend.Backblaze</Name>
+ </ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="help.txt" />
diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj
index dca8d4f68..38b00abbf 100644
--- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj
+++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj
@@ -284,6 +284,10 @@
<Project>{08D7E42D-285C-4010-9881-986125FE2F3E}</Project>
<Name>Duplicati.Library.Backend.AmazonCloudDrive</Name>
</ProjectReference>
+ <ProjectReference Include="..\..\Library\Backend\Backblaze\Duplicati.Library.Backend.Backblaze.csproj">
+ <Project>{61C43D61-4368-4942-84A3-1EB623F4EF2A}</Project>
+ <Name>Duplicati.Library.Backend.Backblaze</Name>
+ </ProjectReference>
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.0">
diff --git a/Duplicati/Library/Backend/Backblaze/B2.cs b/Duplicati/Library/Backend/Backblaze/B2.cs
new file mode 100644
index 000000000..c05dc18df
--- /dev/null
+++ b/Duplicati/Library/Backend/Backblaze/B2.cs
@@ -0,0 +1,520 @@
+// Copyright (C) 2015, The Duplicati Team
+// http://www.duplicati.com, info@duplicati.com
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using Duplicati.Library.Utility;
+using Duplicati.Library.Interface;
+using Newtonsoft.Json;
+using System.Net;
+
+namespace Duplicati.Library.Backend.Backblaze
+{
+ public class B2 : IBackend, IStreamingBackend
+ {
+ private const string B2_ID_OPTION = "b2-accountid";
+ private const string B2_KEY_OPTION = "b2-applicationkey";
+
+ private const string B2_CREATE_BUCKET_TYPE_OPTION = "b2-create-bucket-type";
+ private const string DEFAULT_BUCKET_TYPE = "allPrivate";
+
+ private const int PAGE_SIZE = 200;
+
+ private string m_bucketname;
+ private string m_prefix;
+ private string m_project;
+ private string m_bucketType;
+ private B2AuthHelper m_helper;
+ private UploadUrlResponse m_uploadUrl;
+
+ private Dictionary<string, List<FileEntity>> m_filecache;
+
+ private BucketEntity m_bucket;
+
+ public B2()
+ {
+ }
+
+ public B2(string url, Dictionary<string, string> options)
+ {
+ var uri = new Utility.Uri(url);
+
+ m_bucketname = uri.Host;
+ m_prefix = "/" + uri.Path;
+ if (!m_prefix.EndsWith("/"))
+ m_prefix += "/";
+
+ // For B2 we do not use a leading slash
+ while(m_prefix.StartsWith("/"))
+ m_prefix = m_prefix.Substring(1);
+
+ m_bucketType = DEFAULT_BUCKET_TYPE;
+ if (options.ContainsKey(B2_CREATE_BUCKET_TYPE_OPTION))
+ m_bucketType = options[B2_CREATE_BUCKET_TYPE_OPTION];
+
+ string accountId = null;
+ string accountKey = null;
+
+ if (options.ContainsKey("auth-username"))
+ accountId = options["auth-username"];
+ if (options.ContainsKey("auth-password"))
+ accountKey = options["auth-password"];
+
+ if (options.ContainsKey(B2_ID_OPTION))
+ accountId = options[B2_ID_OPTION];
+ if (options.ContainsKey(B2_KEY_OPTION))
+ accountKey = options[B2_KEY_OPTION];
+ if (!string.IsNullOrEmpty(uri.Username))
+ accountId = uri.Username;
+ if (!string.IsNullOrEmpty(uri.Password))
+ accountKey = uri.Password;
+
+ if (string.IsNullOrEmpty(accountId))
+ throw new Exception(Strings.B2.NoB2UserIDError);
+ if (string.IsNullOrEmpty(accountKey))
+ throw new Exception(Strings.B2.NoB2KeyError);
+
+ m_helper = new B2AuthHelper(accountId, accountKey);
+ }
+
+ private BucketEntity Bucket
+ {
+ get
+ {
+ if (m_bucket == null)
+ {
+ var buckets = m_helper.PostAndGetJSONData<ListBucketsResponse>(
+ string.Format("{0}/b2api/v1/b2_list_buckets", m_helper.APIUrl),
+ new ListBucketsRequest() {
+ AccountID = m_helper.AccountID
+ }
+ );
+
+ if (buckets != null && buckets.Buckets != null)
+ m_bucket = buckets.Buckets.Where(x => string.Equals(x.BucketName, m_bucketname, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
+
+ if (m_bucket == null)
+ throw new FolderMissingException();
+ }
+
+ return m_bucket;
+ }
+ }
+
+ private UploadUrlResponse UploadUrlData
+ {
+ get
+ {
+ if (m_uploadUrl == null)
+ m_uploadUrl = m_helper.PostAndGetJSONData<UploadUrlResponse>(
+ string.Format("{0}/b2api/v1/b2_get_upload_url", m_helper.APIUrl),
+ new UploadUrlRequest() { BucketID = Bucket.BucketID }
+ );
+
+ return m_uploadUrl;
+ }
+ }
+
+ private string GetFileID(string filename)
+ {
+ if (m_filecache != null && m_filecache.ContainsKey(filename))
+ return m_filecache[filename].OrderByDescending(x => x.UploadTimestamp).First().FileID;
+
+ List();
+ if (m_filecache.ContainsKey(filename))
+ return m_filecache[filename].OrderByDescending(x => x.UploadTimestamp).First().FileID;
+
+ throw new FileMissingException();
+ }
+
+ public IList<ICommandLineArgument> SupportedCommands
+ {
+ get
+ {
+ return new List<ICommandLineArgument>(new ICommandLineArgument[] {
+ new CommandLineArgument(B2_ID_OPTION, CommandLineArgument.ArgumentType.String, Strings.B2.B2accountidDescriptionShort, Strings.B2.B2accountidDescriptionLong, null, new string[] {"auth-password"}, null),
+ new CommandLineArgument(B2_KEY_OPTION, CommandLineArgument.ArgumentType.Password, Strings.B2.B2applicationkeyDescriptionShort, Strings.B2.B2applicationkeyDescriptionLong, null, new string[] {"auth-username"}, null),
+ new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.B2.AuthPasswordDescriptionShort, Strings.B2.AuthPasswordDescriptionLong),
+ new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.B2.AuthUsernameDescriptionShort, Strings.B2.AuthUsernameDescriptionLong),
+ new CommandLineArgument(B2_CREATE_BUCKET_TYPE_OPTION, CommandLineArgument.ArgumentType.String, Strings.B2.B2createbuckettypeDescriptionShort, Strings.B2.B2createbuckettypeDescriptionLong, DEFAULT_BUCKET_TYPE),
+ });
+
+ }
+ }
+
+ public void Put(string remotename, System.IO.Stream stream)
+ {
+ TempFile tmp = null;
+
+ // A bit dirty, but we need the underlying stream to compute the hash without any interference
+ var measure = stream;
+ while (measure is OverrideableStream)
+ measure = typeof(OverrideableStream).GetField("m_basestream", System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(measure) as System.IO.Stream;
+
+ if (measure == null)
+ throw new Exception(string.Format("Unable to unwrap stream from: {0}", stream.GetType()));
+
+ string sha1;
+ if (measure.CanSeek)
+ {
+ // Record the stream position
+ var p = measure.Position;
+
+ // Compute the hash
+ using(var hashalg = System.Security.Cryptography.HashAlgorithm.Create("sha1"))
+ sha1 = Library.Utility.Utility.ByteArrayAsHexString(hashalg.ComputeHash(measure));
+
+ // Reset the stream position
+ measure.Position = p;
+ }
+ else
+ {
+ // No seeking possible, use a temp file
+ tmp = new TempFile();
+ using(var sr = System.IO.File.OpenWrite(tmp))
+ using(var hc = new HashCalculatingStream(measure, "sha1"))
+ {
+ Library.Utility.Utility.CopyStream(hc, sr);
+ sha1 = hc.GetFinalHashString();
+ }
+
+ stream = System.IO.File.OpenRead(tmp);
+ }
+
+ if (m_filecache == null)
+ List();
+
+ try
+ {
+ var fileinfo = m_helper.GetJSONData<UploadFileResponse>(
+ UploadUrlData.UploadUrl,
+ req =>
+ {
+ req.Method = "POST";
+ req.Headers["Authorization"] = UploadUrlData.AuthorizationToken;
+ req.Headers["X-Bz-Content-Sha1"] = sha1;
+ req.Headers["X-Bz-File-Name"] = m_prefix + remotename;
+ req.ContentType = "application/octet-stream";
+ req.ContentLength = stream.Length;
+ },
+
+ req =>
+ {
+ using(var rs = req.GetRequestStream())
+ Utility.Utility.CopyStream(stream, rs);
+ }
+ );
+
+ // Delete old versions
+ if (m_filecache.ContainsKey(remotename))
+ Delete(remotename);
+
+ m_filecache[remotename] = new List<FileEntity>();
+ m_filecache[remotename].Add(new FileEntity() {
+ FileID = fileinfo.FileID,
+ FileName = fileinfo.FileName,
+ Action = "upload",
+ Size = fileinfo.ContentLength,
+ UploadTimestamp = (long)(DateTime.UtcNow - Utility.Utility.EPOCH).TotalMilliseconds
+ });
+ }
+ catch(Exception ex)
+ {
+ m_filecache = null;
+
+ var code = (int)B2AuthHelper.GetExceptionStatusCode(ex);
+ if (code >= 500 && code <= 599)
+ m_uploadUrl = null;
+
+ throw;
+ }
+ finally
+ {
+ try
+ {
+ if (tmp != null)
+ tmp.Dispose();
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ public void Get(string remotename, System.IO.Stream stream)
+ {
+ AsyncHttpRequest req;
+ if (m_filecache == null || !m_filecache.ContainsKey(remotename))
+ List();
+
+ if (m_filecache != null && m_filecache.ContainsKey(remotename))
+ req = new AsyncHttpRequest(m_helper.CreateRequest(string.Format("{0}/b2api/v1/b2_download_file_by_id?fileId={1}", m_helper.DownloadUrl, Library.Utility.Uri.UrlEncode(GetFileID(remotename)))));
+ else
+ req = new AsyncHttpRequest(m_helper.CreateRequest(string.Format("{0}/{1}{2}", m_helper.DownloadUrl, m_prefix, Library.Utility.Uri.UrlEncode(remotename))));
+
+ try
+ {
+ using(var rs = req.GetResponseStream())
+ Library.Utility.Utility.CopyStream(rs, stream);
+ }
+ catch (Exception ex)
+ {
+ if (B2AuthHelper.GetExceptionStatusCode(ex) == HttpStatusCode.NotFound)
+ throw new FileMissingException();
+
+ B2AuthHelper.AttemptParseAndThrowException(ex);
+
+ throw;
+ }
+ }
+
+ public List<IFileEntry> List()
+ {
+ m_filecache = null;
+ var cache = new Dictionary<string, List<FileEntity>>();
+ string nextFileID = null;
+ do
+ {
+ var resp = m_helper.PostAndGetJSONData<ListFilesResponse>(
+ string.Format("{0}/b2api/v1/b2_list_file_versions", m_helper.APIUrl),
+ new ListFilesRequest() {
+ BucketID = Bucket.BucketID,
+ MaxFileCount = PAGE_SIZE,
+ StartFileID = nextFileID
+ }
+ );
+
+ nextFileID = resp.NextFileID;
+
+ if (resp.Files == null || resp.Files.Length == 0)
+ break;
+
+ foreach(var f in resp.Files)
+ {
+ if (!f.FileName.StartsWith(m_prefix))
+ continue;
+
+ var name = f.FileName.Substring(m_prefix.Length);
+ if (name.Contains("/"))
+ continue;
+
+
+ List<FileEntity> lst;
+ cache.TryGetValue(name, out lst);
+ if (lst == null)
+ cache[name] = lst = new List<FileEntity>();
+ lst.Add(f);
+ }
+
+ } while(nextFileID != null);
+
+ m_filecache = cache;
+
+ return
+ (from x in m_filecache
+ let newest = x.Value.OrderByDescending(y => y.UploadTimestamp).First()
+ let ts = Utility.Utility.EPOCH.AddMilliseconds(newest.UploadTimestamp)
+ select (IFileEntry)new FileEntry(x.Key, newest.Size, ts, ts)
+ ).ToList();
+ }
+
+ public void Put(string remotename, string filename)
+ {
+ using (System.IO.FileStream fs = System.IO.File.OpenRead(filename))
+ Put(remotename, fs);
+ }
+
+ public void Get(string remotename, string filename)
+ {
+ using (System.IO.FileStream fs = System.IO.File.Create(filename))
+ Get(remotename, fs);
+ }
+
+ public void Delete(string remotename)
+ {
+ try
+ {
+ if (m_filecache == null || !m_filecache.ContainsKey(remotename))
+ List();
+
+ if (!m_filecache.ContainsKey(remotename))
+ throw new FileMissingException();
+
+ foreach(var n in m_filecache[remotename].OrderBy(x => x.UploadTimestamp))
+ m_helper.PostAndGetJSONData<DeleteResponse>(
+ string.Format("{0}/b2api/v1/b2_delete_file_version", m_helper.APIUrl),
+ new DeleteRequest() {
+ FileName = m_prefix + remotename,
+ FileID = n.FileID
+ }
+ );
+
+ m_filecache[remotename].Clear();
+ }
+ catch
+ {
+ m_filecache = null;
+ throw;
+ }
+ }
+
+ public void Test()
+ {
+ List();
+ }
+
+ public void CreateFolder()
+ {
+ m_bucket = m_helper.PostAndGetJSONData<BucketEntity>(
+ string.Format("{0}/b2api/v1/b2_create_bucket", m_helper.APIUrl),
+ new BucketEntity() {
+ AccountID = m_helper.AccountID,
+ BucketName = m_bucketname,
+ BucketType = m_bucketType
+ }
+ );
+ }
+
+ public string DisplayName
+ {
+ get { return Strings.B2.DisplayName; }
+ }
+
+ public string ProtocolKey
+ {
+ get { return "b2"; }
+ }
+
+ public string Description
+ {
+ get { return Strings.B2.Description; }
+ }
+
+ public void Dispose()
+ {
+ }
+
+ private class DeleteRequest
+ {
+ [JsonProperty("fileName")]
+ public string FileName { get; set; }
+ [JsonProperty("fileId")]
+ public string FileID { get; set; }
+ }
+
+ private class DeleteResponse : DeleteRequest
+ {
+ }
+
+ private class UploadUrlRequest : BucketIDEntity
+ {
+ }
+
+ private class UploadUrlResponse : BucketIDEntity
+ {
+ [JsonProperty("uploadUrl")]
+ public string UploadUrl { get; set; }
+ [JsonProperty("authorizationToken")]
+ public string AuthorizationToken { get; set; }
+ }
+
+ private class AccountIDEntity
+ {
+ [JsonProperty("accountId")]
+ public string AccountID { get; set; }
+ }
+
+ private class BucketIDEntity
+ {
+ [JsonProperty("bucketId")]
+ public string BucketID { get; set; }
+ }
+
+ private class BucketEntity : AccountIDEntity
+ {
+ [JsonProperty("bucketId", NullValueHandling = NullValueHandling.Ignore)]
+ public string BucketID { get; set; }
+ [JsonProperty("bucketName")]
+ public string BucketName { get; set; }
+ [JsonProperty("bucketType")]
+ public string BucketType { get; set; }
+ }
+
+ private class ListBucketsRequest : AccountIDEntity
+ {
+ }
+
+ private class ListBucketsResponse
+ {
+ [JsonProperty("buckets")]
+ public BucketEntity[] Buckets { get; set; }
+ }
+
+ private class ListFilesRequest : BucketIDEntity
+ {
+ [JsonProperty("startFileName", NullValueHandling = NullValueHandling.Ignore)]
+ public string StartFileName { get; set; }
+ [JsonProperty("startFileId", NullValueHandling = NullValueHandling.Ignore)]
+ public string StartFileID { get; set; }
+ [JsonProperty("maxFileCount")]
+ public long MaxFileCount { get; set; }
+ }
+
+ private class ListFilesResponse
+ {
+ [JsonProperty("nextFileName")]
+ public string NextFileName { get; set; }
+ [JsonProperty("nextFileId")]
+ public string NextFileID { get; set; }
+ [JsonProperty("files")]
+ public FileEntity[] Files { get; set; }
+ }
+
+ private class FileEntity
+ {
+ [JsonProperty("fileId")]
+ public string FileID { get; set; }
+ [JsonProperty("fileName")]
+ public string FileName { get; set; }
+ [JsonProperty("action")]
+ public string Action { get; set; }
+ [JsonProperty("size")]
+ public long Size { get; set; }
+ [JsonProperty("uploadTimestamp")]
+ public long UploadTimestamp { get; set; }
+
+ }
+
+ private class UploadFileResponse : AccountIDEntity
+ {
+ [JsonProperty("bucketId")]
+ public string BucketID { get; set; }
+ [JsonProperty("fileId")]
+ public string FileID { get; set; }
+ [JsonProperty("fileName")]
+ public string FileName { get; set; }
+ [JsonProperty("contentLength")]
+ public long ContentLength { get; set; }
+ [JsonProperty("contentSha1")]
+ public string ContentSha1 { get; set; }
+ [JsonProperty("contentType")]
+ public string ContentType { get; set; }
+ }
+
+ }
+}
+
diff --git a/Duplicati/Library/Backend/Backblaze/B2AuthHelper.cs b/Duplicati/Library/Backend/Backblaze/B2AuthHelper.cs
new file mode 100644
index 000000000..72b1a9c8c
--- /dev/null
+++ b/Duplicati/Library/Backend/Backblaze/B2AuthHelper.cs
@@ -0,0 +1,168 @@
+// Copyright (C) 2015, The Duplicati Team
+// http://www.duplicati.com, info@duplicati.com
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using System;
+using System.Net;
+using System.Text;
+using Newtonsoft.Json;
+using Duplicati.Library.Utility;
+
+namespace Duplicati.Library.Backend.Backblaze
+{
+ public class B2AuthHelper : JSONWebHelper
+ {
+ private readonly string m_credentials;
+ private AuthResponse m_config;
+ private DateTime m_configExpires;
+ private const string AUTH_URL = "https://api.backblaze.com/b2api/v1/b2_authorize_account";
+
+ public B2AuthHelper(string userid, string password)
+ : base()
+ {
+ m_credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes(userid + ":" + password));
+ }
+
+ public override HttpWebRequest CreateRequest(string url, string method = null)
+ {
+ var r = base.CreateRequest(url, method);
+ r.Headers["Authorization"] = AuthorizationToken;
+ return r;
+ }
+
+ public string AuthorizationToken { get { return Config.AuthorizationToken; } }
+ public string APIUrl { get { return Config.APIUrl; } }
+ public string DownloadUrl { get { return Config.DownloadUrl; } }
+ public string AccountID { get { return Config.AccountID; } }
+
+ private string DropTrailingSlashes(string url)
+ {
+ while(url.EndsWith("/"))
+ url = url.Substring(0, url.Length - 1);
+ return url;
+ }
+
+ private AuthResponse Config
+ {
+ get
+ {
+ if (m_config == null || m_configExpires < DateTime.UtcNow)
+ {
+ var retries = 0;
+
+ while(true)
+ {
+ try
+ {
+ var req = base.CreateRequest(AUTH_URL);
+ req.Headers.Add("Authorization", string.Format("Basic {0}", m_credentials));
+ req.ContentType = "application/json; charset=utf-8";
+
+ using(var resp = (HttpWebResponse)new AsyncHttpRequest(req).GetResponse())
+ m_config = ReadJSONResponse<AuthResponse>(resp);
+
+ m_config.APIUrl = DropTrailingSlashes(m_config.APIUrl);
+ m_config.DownloadUrl = DropTrailingSlashes(m_config.DownloadUrl);
+
+ m_configExpires = DateTime.UtcNow + TimeSpan.FromHours(1);
+ return m_config;
+ }
+ catch (Exception ex)
+ {
+ var msg = ex.Message;
+ if (retries >= 5)
+ {
+ AttemptParseAndThrowException(ex);
+ throw;
+ }
+
+ System.Threading.Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retries)));
+ retries++;
+ }
+ }
+ }
+
+ return m_config;
+ }
+ }
+
+ public static void AttemptParseAndThrowException(Exception ex)
+ {
+ Exception newex = null;
+ try
+ {
+ if (ex is WebException && (ex as WebException).Response is HttpWebResponse)
+ {
+ string rawdata = null;
+ var hs = (ex as WebException).Response as HttpWebResponse;
+ using(var rs = hs.GetResponseStream())
+ using(var sr = new System.IO.StreamReader(rs))
+ rawdata = sr.ReadToEnd();
+
+ newex = new Exception("Raw message: " + rawdata);
+
+ var msg = JsonConvert.DeserializeObject<ErrorResponse>(rawdata);
+ newex = new Exception(string.Format("{0} - {1}: {2}", msg.Status, msg.Code, msg.Message));
+ }
+ }
+ catch
+ {
+ }
+
+ if (newex != null)
+ throw newex;
+ }
+
+ protected override void ParseException(Exception ex)
+ {
+ AttemptParseAndThrowException(ex);
+ }
+
+ public static HttpStatusCode GetExceptionStatusCode(Exception ex)
+ {
+ if (ex is WebException && (ex as WebException).Response is HttpWebResponse)
+ return ((ex as WebException).Response as HttpWebResponse).StatusCode;
+ else
+ return (HttpStatusCode)0;
+ }
+
+
+ private class ErrorResponse
+ {
+ [JsonProperty("code")]
+ public string Code { get; set; }
+ [JsonProperty("message")]
+ public string Message { get; set; }
+ [JsonProperty("status")]
+ public long Status { get; set; }
+
+ }
+
+ private class AuthResponse
+ {
+ [JsonProperty("accountId")]
+ public string AccountID { get; set; }
+ [JsonProperty("apiUrl")]
+ public string APIUrl { get; set; }
+ [JsonProperty("authorizationToken")]
+ public string AuthorizationToken { get; set; }
+ [JsonProperty("downloadUrl")]
+ public string DownloadUrl { get; set; }
+ }
+
+ }
+
+}
+
diff --git a/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj b/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj
new file mode 100644
index 000000000..5beca3f45
--- /dev/null
+++ b/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>8.0.30703</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{61C43D61-4368-4942-84A3-1EB623F4EF2A}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>Duplicati.Library.Backend.Backblaze</RootNamespace>
+ <AssemblyName>Duplicati.Library.Backend.Backblaze</AssemblyName>
+ <SignAssembly>true</SignAssembly>
+ <AssemblyOriginatorKeyFile>Duplicati.snk</AssemblyOriginatorKeyFile>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug</OutputPath>
+ <DefineConstants>DEBUG;</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <ConsolePause>false</ConsolePause>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>full</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release</OutputPath>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <ConsolePause>false</ConsolePause>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="Newtonsoft.Json">
+ <HintPath>..\..\..\..\thirdparty\Json.NET\Newtonsoft.Json.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="B2.cs" />
+ <Compile Include="Strings.cs" />
+ <Compile Include="B2AuthHelper.cs" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <ItemGroup>
+ <ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj">
+ <Project>{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}</Project>
+ <Name>Duplicati.Library.Utility</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\OAuthHelper\Duplicati.Library.OAuthHelper.csproj">
+ <Project>{D4C37C33-5E73-4B56-B2C3-DC4A6BAA36BB}</Project>
+ <Name>Duplicati.Library.OAuthHelper</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj">
+ <Project>{B68F2214-951F-4F78-8488-66E1ED3F50BF}</Project>
+ <Name>Duplicati.Library.Localization</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Interface\Duplicati.Library.Interface.csproj">
+ <Project>{C5899F45-B0FF-483C-9D38-24A9FCAAB237}</Project>
+ <Name>Duplicati.Library.Interface</Name>
+ </ProjectReference>
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/Duplicati/Library/Backend/Backblaze/Duplicati.snk b/Duplicati/Library/Backend/Backblaze/Duplicati.snk
new file mode 100644
index 000000000..e0c1e2dd8
--- /dev/null
+++ b/Duplicati/Library/Backend/Backblaze/Duplicati.snk
Binary files differ
diff --git a/Duplicati/Library/Backend/Backblaze/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/Backblaze/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..c04423a5e
--- /dev/null
+++ b/Duplicati/Library/Backend/Backblaze/Properties/AssemblyInfo.cs
@@ -0,0 +1,43 @@
+// Copyright (C) 2015, The Duplicati Team
+// http://www.duplicati.com, info@duplicati.com
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// Information about this assembly is defined by the following attributes.
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle("Duplicati.Library.Backend.Backblaze")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("kenneth")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion("1.0.*")]
+
+// The following attributes are used to specify the signing key for the assembly,
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
+
diff --git a/Duplicati/Library/Backend/Backblaze/Strings.cs b/Duplicati/Library/Backend/Backblaze/Strings.cs
new file mode 100644
index 000000000..c33d850a3
--- /dev/null
+++ b/Duplicati/Library/Backend/Backblaze/Strings.cs
@@ -0,0 +1,19 @@
+using Duplicati.Library.Localization.Short;
+namespace Duplicati.Library.Backend.Strings {
+ internal static class B2 {
+ public static string B2applicationkeyDescriptionShort { get { return LC.L(@"The ""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(@"The ""B2 Cloud Storage Application Key"""); } }
+ public static string B2accountidDescriptionLong { get { return LC.L(@"The ""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 B2accountidDescriptionShort { get { return LC.L(@"The ""B2 Cloud Storage Account ID"""); } }
+ public static string DisplayName { get { return LC.L(@"B2 Cloud Storage"); } }
+ 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 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 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 Description { get { return LC.L(@"This backend can read and write data to the Backblaze B2 Cloud Storage. Allowed formats are: ""b2://bucketname/prefix"""); } }
+ 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 "); } }
+ public static string B2createbuckettypeDescriptionShort { get { return LC.L(@"The bucket type used when creating a bucket"); } }
+ }
+}
diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj
index c40cac13f..8393cebb0 100644
--- a/Duplicati/Server/Duplicati.Server.csproj
+++ b/Duplicati/Server/Duplicati.Server.csproj
@@ -256,6 +256,10 @@
<Project>{08D7E42D-285C-4010-9881-986125FE2F3E}</Project>
<Name>Duplicati.Library.Backend.AmazonCloudDrive</Name>
</ProjectReference>
+ <ProjectReference Include="..\Library\Backend\Backblaze\Duplicati.Library.Backend.Backblaze.csproj">
+ <Project>{61C43D61-4368-4942-84A3-1EB623F4EF2A}</Project>
+ <Name>Duplicati.Library.Backend.Backblaze</Name>
+ </ProjectReference>
</ItemGroup>
<ItemGroup>
<Folder Include="webroot\" />
diff --git a/Duplicati/Server/webroot/greeno/scripts/edituri.js b/Duplicati/Server/webroot/greeno/scripts/edituri.js
index ca2b1efe8..b4f20e5ef 100644
--- a/Duplicati/Server/webroot/greeno/scripts/edituri.js
+++ b/Duplicati/Server/webroot/greeno/scripts/edituri.js
@@ -364,7 +364,7 @@ $(document).ready(function() {
}
}
- for (var i in { 's3': 0, 'azure': 0, 'googledrive': 0, 'onedrive': 0, 'cloudfiles': 0, 'gcs': 0, 'openstack': 0, 'hubic': 0 }) {
+ for (var i in { 's3': 0, 'azure': 0, 'googledrive': 0, 'onedrive': 0, 'cloudfiles': 0, 'gcs': 0, 'openstack': 0, 'hubic': 0, 'amzcd': 0, 'b2': 0 }) {
if (BACKEND_STATE.module_lookup[i]) {
used[i] = true;
group_prop.append($("<option></option>").attr("value", i).text(BACKEND_STATE.module_lookup[i].DisplayName));
diff --git a/Duplicati/Server/webroot/greeno/scripts/plugins.js b/Duplicati/Server/webroot/greeno/scripts/plugins.js
index 666400a32..f064fc9d8 100644
--- a/Duplicati/Server/webroot/greeno/scripts/plugins.js
+++ b/Duplicati/Server/webroot/greeno/scripts/plugins.js
@@ -712,4 +712,53 @@ $(document).ready(function() {
}
}
+ APP_DATA.plugins.backend['b2'] = {
+
+ PLUGIN_B2_LINK: 'https://www.backblaze.com/b2/cloud-storage.html',
+
+ hasssl: false,
+ hideserverandport: true,
+ usernamelabel: 'B2 Account ID',
+ passwordlabel: 'B2 Application Key',
+ usernamewatermark: 'B2 Cloud Storage Account ID',
+ passwordwatermark: 'B2 Cloud Storage Application Key',
+
+ setup: function(dlg, div) {
+ var self = this;
+
+ $('#server-path-label').hide();
+ $('#server-path').hide();
+
+ var bucketfield = EDIT_URI.createFieldset({label: 'B2 Bucket name', name: 'b2-bucket', after: $('#server-username-and-password'), title: 'Use / to access subfolders in the bucket', watermark: 'Enter bucket name'});
+ var signuplink = EDIT_URI.createFieldset({'label': '&nbsp;', href: this.PLUGIN_B2_LINK, type: 'link', before: bucketfield.outer, 'title': 'Click here for the sign up page'});
+
+ signuplink.outer.css('margin-bottom', '10px');
+
+ this.bucket_field = bucketfield.field;
+ },
+
+ cleanup: function(dlg, div) {
+ $('#server-path-label').show();
+ $('#server-path').show();
+ this.bucket_field = null;
+ },
+
+ validate: function(dlg, values) {
+ if (!EDIT_URI.validate_input(values, true))
+ return;
+
+ if (values['server-path'] == '')
+ return EDIT_URI.validation_error(this.bucket_field, 'You must enter a B2 bucket name');
+
+ return true;
+ },
+
+ fill_form_map: {
+ 'server-path': 'b2-bucket'
+ },
+
+ fill_dict_map: {
+ 'b2-bucket': 'server-path'
+ }
+ }
}); \ No newline at end of file
diff --git a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
index e4eb691f6..127dd2219 100644
--- a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
+++ b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
@@ -21,6 +21,7 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
EditUriBackendConfig.templates['openstack'] = 'templates/backends/openstack.html';
EditUriBackendConfig.templates['azure'] = 'templates/backends/azure.html';
EditUriBackendConfig.templates['gcs'] = 'templates/backends/gcs.html';
+ EditUriBackendConfig.templates['b2'] = 'templates/backends/b2.html';
// Loaders are a way for backends to request extra data from the server
EditUriBackendConfig.loaders['s3'] = function(scope) {
@@ -244,6 +245,16 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
this['oauth-base'].apply(this, arguments);
};
+ EditUriBackendConfig.parsers['b2'] = function(scope, module, server, port, path, options) {
+ if (options['--b2-accountid'])
+ scope.Username = options['--b2-accountid'];
+ if (options['--b2-applicationkey'])
+ scope.Password = options['--b2-applicationkey'];
+
+ var nukeopts = ['--b2-accountid', '--b2-applicationkey'];
+ for(var x in nukeopts)
+ delete options[nukeopts[x]];
+ };
// Builders take the scope and produce the uri output
EditUriBackendConfig.builders['s3'] = function(scope) {
@@ -469,6 +480,14 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
return res;
};
+ EditUriBackendConfig.validaters['b2'] = function(scope) {
+ var res =
+ EditUriBackendConfig.require_field(scope, 'Server', 'bucket name') &&
+ EditUriBackendConfig.require_field(scope, 'Username', 'B2 Cloud Storage Account ID') &&
+ EditUriBackendConfig.require_field(scope, 'Password', 'B2 Cloud Storage Application Key');
+
+ return res;
+ };
}); \ No newline at end of file
diff --git a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js
index db61dc062..f441c38e8 100644
--- a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js
+++ b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js
@@ -4,9 +4,9 @@ backupApp.service('SystemInfo', function($rootScope, $timeout, AppService, AppUt
this.state = state;
var backendgroups = {
- std: {'ftp': null, 'ssh': null, 'webdav': null, 'openstack': 'OpenStack Object Storage/ Swift', 's3': 'S3 Compatible'},
+ std: {'ftp': null, 'ssh': null, 'webdav': null, 'openstack': 'OpenStack Object Storage / Swift', 's3': 'S3 Compatible'},
local: {'file': null},
- prop: { 's3': null, 'azure': null, 'googledrive': null, 'onedrive': null, 'cloudfiles': null, 'gcs': null, 'openstack': null, 'hubic': null }
+ prop: { 's3': null, 'azure': null, 'googledrive': null, 'onedrive': null, 'cloudfiles': null, 'gcs': null, 'openstack': null, 'hubic': null, 'amzcd': null, 'b2': null }
}
this.backendgroups = backendgroups;
diff --git a/Duplicati/Server/webroot/ngax/templates/backends/b2.html b/Duplicati/Server/webroot/ngax/templates/backends/b2.html
new file mode 100644
index 000000000..3b53cb041
--- /dev/null
+++ b/Duplicati/Server/webroot/ngax/templates/backends/b2.html
@@ -0,0 +1,18 @@
+<div class="input text">
+ <label for="b2_bucket">Bucket name</label>
+ <input type="text" id="b2_bucket" ng-model="$parent.Server" placeholder="Bucket name" />
+</div>
+
+<div class="input text">
+ <label for="b2_path">Folder path</label>
+ <input type="text" name="b2_path" id="b2_path" ng-model="$parent.Path" placeholder="Path or subfolder in the bucket" />
+</div>
+
+<div class="input text">
+ <label for="b2_username">B2 Account ID</label>
+ <input type="text" name="b2_username" id="b2_username" ng-model="$parent.Username" placeholder="B2 Cloud Storage Account ID" />
+</div>
+<div class="input password">
+ <label for="b2_password">B2 Application Key</label>
+ <input type="text" name="b2_password" id="b2_password" ng-model="$parent.Password" placeholder="B2 Cloud Storage Application Key" />
+</div>