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:
authorKenneth Skovhede <kenneth@hexad.dk>2017-03-12 14:55:17 +0300
committerGitHub <noreply@github.com>2017-03-12 14:55:17 +0300
commit3071ac7c754f83c7f42eaa74d8123e9bb4b96185 (patch)
treef52b41691b822bbb3605d73e6e4f649e1d7305b2
parent9d81d3da7e3b6f84734ec23eed32d6573b09b2a6 (diff)
parentf4c8c27663e232038a66a3e67ba497c7bfcac89b (diff)
Merge pull request #2355 from albertony/jottacloud_backend
Jottacloud backend
-rw-r--r--Duplicati.sln6
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj76
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Duplicati.snkbin0 -> 596 bytes
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Jottacloud.cs443
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Properties/AssemblyInfo.cs54
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Strings.cs21
-rw-r--r--Duplicati/Library/Backend/Mega/Strings.cs2
-rw-r--r--Duplicati/Server/Duplicati.Server.csproj4
-rw-r--r--Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js32
-rw-r--r--Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js3
-rw-r--r--Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html13
11 files changed, 652 insertions, 2 deletions
diff --git a/Duplicati.sln b/Duplicati.sln
index 6ebd82eaf..40bb8081a 100644
--- a/Duplicati.sln
+++ b/Duplicati.sln
@@ -93,6 +93,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Tools", "Duplicati\Tools\Duplicati.Tools.csproj", "{0797AA22-C5DD-4950-BB60-34765AB8C6DD}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Jottacloud", "Duplicati\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj", "{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -279,6 +281,10 @@ Global
{0797AA22-C5DD-4950-BB60-34765AB8C6DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0797AA22-C5DD-4950-BB60-34765AB8C6DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0797AA22-C5DD-4950-BB60-34765AB8C6DD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {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
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
Policies = $0
diff --git a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj
new file mode 100644
index 000000000..e308a8103
--- /dev/null
+++ b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Duplicati.Library.Backend</RootNamespace>
+ <AssemblyName>Duplicati.Library.Backend.Jottacloud</AssemblyName>
+ <FileUpgradeFlags>
+ </FileUpgradeFlags>
+ <OldToolsVersion>3.5</OldToolsVersion>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <TargetFrameworkProfile />
+ <UpgradeBackupLocation>
+ </UpgradeBackupLocation>
+ <AssemblyOriginatorKeyFile>Duplicati.snk</AssemblyOriginatorKeyFile>
+ <UseMSBuildEngine>false</UseMSBuildEngine>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Jottacloud.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Strings.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj">
+ <Project>{52826615-7964-47FE-B4B3-1B2DBDF605B9}</Project>
+ <Name>Duplicati.Library.Modules.Builtin</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj">
+ <Project>{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}</Project>
+ <Name>Duplicati.Library.Utility</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Interface\Duplicati.Library.Interface.csproj">
+ <Project>{C5899F45-B0FF-483C-9D38-24A9FCAAB237}</Project>
+ <Name>Duplicati.Library.Interface</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj">
+ <Project>{B68F2214-951F-4F78-8488-66E1ED3F50BF}</Project>
+ <Name>Duplicati.Library.Localization</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="Duplicati.snk" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/Duplicati/Library/Backend/Jottacloud/Duplicati.snk b/Duplicati/Library/Backend/Jottacloud/Duplicati.snk
new file mode 100644
index 000000000..e0c1e2dd8
--- /dev/null
+++ b/Duplicati/Library/Backend/Jottacloud/Duplicati.snk
Binary files differ
diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs
new file mode 100644
index 000000000..5cc6e3133
--- /dev/null
+++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs
@@ -0,0 +1,443 @@
+#region Disclaimer / License
+// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+#endregion
+using System;
+using System.Collections.Generic;
+using Duplicati.Library.Interface;
+
+namespace Duplicati.Library.Backend
+{
+ public class Jottacloud : IBackend, IStreamingBackend
+ {
+ private const string JFS_ROOT = "https://jfs.jottacloud.com/jfs";
+ private const string JFS_ROOT_UPLOAD = "https://up.jottacloud.com/jfs"; // Separate host for uploading files
+ private const string API_VERSION = "2.4"; // Hard coded per 09. March 2017.
+ private const string JFS_BUILTIN_DEVICE = "Jotta"; // The built-in device used for the built-in Sync and Archive mount points.
+ private static readonly string JFS_DEFAULT_BUILTIN_MOUNT_POINT = "Archive"; // When using the built-in device we pick this mount point as our default.
+ private static readonly string JFS_DEFAULT_CUSTOM_MOUNT_POINT = "Duplicati"; // When custom device is specified then we pick this mount point as our default.
+ private static readonly string[] JFS_BUILTIN_MOUNT_POINTS = { "Archive", "Sync" }; // Name of built-in mount points that we can use.
+ private static readonly string[] JFS_BUILTIN_ILLEGAL_MOUNT_POINTS = { "Trash", "Links", "Latest", "Shared" }; // Name of built-in mount points that we can not use. These are treated as mount points in the API, but they are for used for special functionality and we cannot upload files to them!
+ private const string JFS_DEVICE_OPTION = "jottacloud-device";
+ private const string JFS_MOUNT_POINT_OPTION = "jottacloud-mountpoint";
+ private const string JFS_DATE_FORMAT = "yyyy'-'MM'-'dd-'T'HH':'mm':'ssK";
+ private const bool ALLOW_USER_DEFINED_MOUNT_POINTS = false;
+ private readonly string m_device;
+ private readonly bool m_device_builtin;
+ private readonly string m_mountPoint;
+ private readonly string m_path;
+ private readonly string m_url_device;
+ private readonly string m_url;
+ private readonly string m_url_upload;
+ private System.Net.NetworkCredential m_userInfo;
+ private readonly byte[] m_copybuffer = new byte[Duplicati.Library.Utility.Utility.DEFAULT_BUFFER_SIZE];
+
+ public Jottacloud()
+ {
+ }
+
+ public Jottacloud(string url, Dictionary<string, string> options)
+ {
+ // Duplicati back-end url for Jottacloud is in format "jottacloud://folder/subfolder", we transform them to
+ // the Jottacloud REST API (JFS) url format "https://jfs.jottacloud.com/jfs/[username]/[device]/[mountpoint]/[folder]/[subfolder]".
+
+ // Find out what JFS device to use.
+ if (options.ContainsKey(JFS_DEVICE_OPTION))
+ {
+ // Custom device specified.
+ m_device = options[JFS_DEVICE_OPTION];
+ if (string.Equals(m_device, JFS_BUILTIN_DEVICE, StringComparison.OrdinalIgnoreCase))
+ {
+ m_device_builtin = true; // Device is configured, but value set to the built-in device!
+ m_device = JFS_BUILTIN_DEVICE; // Ensure correct casing (doesn't seem to matter, but in theory it could).
+ }
+ else
+ {
+ m_device_builtin = false;
+ }
+ }
+ else
+ {
+ // Use default: The built-in device.
+ m_device = JFS_BUILTIN_DEVICE;
+ m_device_builtin = true;
+ }
+
+ // Find out what JFS mount point to use on the device.
+ if (options.ContainsKey(JFS_MOUNT_POINT_OPTION))
+ {
+ // Custom mount point specified.
+ m_mountPoint = options[JFS_MOUNT_POINT_OPTION];
+
+ // If we are using the built-in device make sure we have picked a mount point that we can use.
+ if (m_device_builtin)
+ {
+ // Check that it is not set to one of the special built-in mount points that we definitely cannot make use of.
+ if (Array.FindIndex(JFS_BUILTIN_ILLEGAL_MOUNT_POINTS, x => x.Equals(m_mountPoint, StringComparison.OrdinalIgnoreCase)) != -1)
+ throw new UserInformationException(Strings.Jottacloud.IllegalMountPoint);
+ // Check if it is one of the legal built-in mount points.
+ // What to do if it is not is open for discussion: The JFS API supports creation of custom mount points not only
+ // for custom (backup) devices, but also for the built-in device. But this will not be visible via the official
+ // web interface, so you are kind of working in the dark and need to use the REST API to delete it etc. Therefore
+ // we do not allow this for now, although in future maybe we could consider it, as a "hidden" location?
+ var i = Array.FindIndex(JFS_BUILTIN_MOUNT_POINTS, x => x.Equals(m_mountPoint, StringComparison.OrdinalIgnoreCase));
+ if (i != -1)
+ m_mountPoint = JFS_BUILTIN_MOUNT_POINTS[i]; // Ensure correct casing (doesn't seem to matter, but in theory it could).
+ else
+ throw new UserInformationException(Strings.Jottacloud.IllegalMountPoint); // User defined mount points on built-in device currently not allowed.
+ }
+ }
+ else
+ {
+ if (m_device_builtin)
+ m_mountPoint = JFS_DEFAULT_BUILTIN_MOUNT_POINT; // Set a suitable built-in mount point for the built-in device.
+ else
+ m_mountPoint = JFS_DEFAULT_CUSTOM_MOUNT_POINT; // Set a suitable default mount point for custom (backup) devices.
+ }
+
+ // Build URL
+ var u = new Utility.Uri(url);
+ m_path = u.HostAndPath; // Host and path of "jottacloud://folder/subfolder" is "folder/subfolder", so the actual folder path within the mount point.
+ if (string.IsNullOrEmpty(m_path)) // Require a folder. Actually it is possible to store files directly on the root level of the mount point, but that does not seem to be a good option.
+ throw new UserInformationException(Strings.Jottacloud.NoPathError);
+ if (!m_path.EndsWith("/"))
+ m_path += "/";
+ if (!string.IsNullOrEmpty(u.Username))
+ {
+ m_userInfo = new System.Net.NetworkCredential();
+ m_userInfo.UserName = u.Username;
+ if (!string.IsNullOrEmpty(u.Password))
+ m_userInfo.Password = u.Password;
+ else if (options.ContainsKey("auth-password"))
+ m_userInfo.Password = options["auth-password"];
+ }
+ else
+ {
+ if (options.ContainsKey("auth-username"))
+ {
+ m_userInfo = new System.Net.NetworkCredential();
+ m_userInfo.UserName = options["auth-username"];
+ if (options.ContainsKey("auth-password"))
+ m_userInfo.Password = options["auth-password"];
+ }
+ }
+ if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.UserName))
+ throw new UserInformationException(Strings.Jottacloud.NoUsernameError);
+ if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.Password))
+ throw new UserInformationException(Strings.Jottacloud.NoPasswordError);
+ if (m_userInfo != null) // 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
+ m_userInfo.Domain = "";
+ m_url_device = JFS_ROOT + "/" + m_userInfo.UserName + "/" + m_device;
+ m_url = m_url_device + "/" + m_mountPoint + "/" + m_path;
+ m_url_upload = JFS_ROOT_UPLOAD + "/" + m_userInfo.UserName + "/" + m_device + "/" + m_mountPoint + "/" + m_path; // Different hostname, else identical to m_url.
+ }
+
+ #region IBackend Members
+
+ public string DisplayName
+ {
+ get { return Strings.Jottacloud.DisplayName; }
+ }
+
+ public string ProtocolKey
+ {
+ get { return "jottacloud"; }
+ }
+
+ public List<IFileEntry> List()
+ {
+ var doc = new System.Xml.XmlDocument();
+ try
+ {
+ // Send request and load XML response.
+ var req = CreateRequest(System.Net.WebRequestMethods.Http.Get, "", "", false);
+ var areq = new Utility.AsyncHttpRequest(req);
+ using (var resp = (System.Net.HttpWebResponse)areq.GetResponse())
+ using (var rs = areq.GetResponseStream())
+ doc.Load(rs);
+ }
+ catch (System.Net.WebException wex)
+ {
+ if (wex.Response is System.Net.HttpWebResponse && ((System.Net.HttpWebResponse)wex.Response).StatusCode == System.Net.HttpStatusCode.NotFound)
+ throw new FolderMissingException(wex);
+ throw;
+ }
+ // Handle XML response. Since we in the constructor demand a folder below the mount point we know the root
+ // element must be a "folder", else it could also have been a "mountPoint" (which has a very similar structure).
+ // We must check for "deleted" attribute, because files/folders which has it is deleted (attribute contains the timestamp of deletion)
+ // so we treat them as non-existant here.
+ List<IFileEntry> files = new List<IFileEntry>();
+ var xRoot = doc.DocumentElement;
+ if (xRoot.Attributes["deleted"] != null)
+ {
+ throw new FolderMissingException();
+ }
+ foreach (System.Xml.XmlNode xFolder in xRoot.SelectNodes("folders/folder[not(@deleted)]"))
+ {
+ // Subfolders are only listed with name. We can get a timestamp by sending a request for each folder, but that is probably not necessary?
+ FileEntry fe = new FileEntry(xFolder.Attributes["name"].Value);
+ fe.IsFolder = true;
+ files.Add(fe);
+ }
+ foreach (System.Xml.XmlNode xFile in xRoot.SelectNodes("files/file[not(@deleted)]"))
+ {
+ string name = xFile.Attributes["name"].Value;
+ // Normal files have an "currentRevision", which represent the most recent successfully upload
+ // (could also checked that currentRevision/state is "COMPLETED", but should not be necessary).
+ // There might also be a newer "latestRevision" coming from an incomplete or corrupt upload,
+ // but we ignore that here and use the information about the last valid version.
+ System.Xml.XmlNode xRevision = xFile.SelectSingleNode("currentRevision");
+ if (xRevision != null)
+ {
+ System.Xml.XmlNode xNode = xRevision.SelectSingleNode("size");
+ long size;
+ if (xNode == null || !long.TryParse(xNode.InnerText, out size))
+ size = -1;
+ DateTime lastModified;
+ xNode = xRevision.SelectSingleNode("modified"); // There is created, modified and updated time stamps, but not last accessed.
+ if (xNode == null || !DateTime.TryParseExact(xNode.InnerText, JFS_DATE_FORMAT, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AdjustToUniversal, out lastModified))
+ lastModified = new DateTime();
+ FileEntry fe = new FileEntry(name, size, lastModified, lastModified);
+ files.Add(fe);
+ }
+ }
+ return files;
+ }
+
+ 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)
+ {
+ System.Net.HttpWebRequest req = CreateRequest(System.Net.WebRequestMethods.Http.Post, remotename, "rm=true", false); // rm=true means permanent delete, dl=true would be move to trash.
+ Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
+ using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)areq.GetResponse())
+ { }
+ }
+
+ public IList<ICommandLineArgument> SupportedCommands
+ {
+ get
+ {
+ return new List<ICommandLineArgument>(new ICommandLineArgument[] {
+ new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.Jottacloud.DescriptionAuthPasswordShort, Strings.Jottacloud.DescriptionAuthPasswordLong),
+ new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionAuthUsernameShort, Strings.Jottacloud.DescriptionAuthUsernameLong),
+ new CommandLineArgument(JFS_DEVICE_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionDeviceShort, Strings.Jottacloud.DescriptionDeviceLong(JFS_MOUNT_POINT_OPTION)),
+ new CommandLineArgument(JFS_MOUNT_POINT_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionMountPointShort, Strings.Jottacloud.DescriptionMountPointLong(JFS_DEVICE_OPTION)),
+ });
+ }
+ }
+
+ public string Description
+ {
+ get { return Strings.Jottacloud.Description; }
+ }
+
+ public void Test()
+ {
+ List();
+ }
+
+ public void CreateFolder()
+ {
+ // When using custom (backup) device we must create the device first (if not already exists).
+ if (!m_device_builtin)
+ {
+ System.Net.HttpWebRequest req = CreateRequest(System.Net.WebRequestMethods.Http.Post, m_url_device, "type=WORKSTATION"); // Hard-coding device type. Must be one of "WORKSTATION", "LAPTOP", "IMAC", "MACBOOK", "IPAD", "ANDROID", "IPHONE" or "WINDOWS_PHONE".
+ Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
+ using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)areq.GetResponse())
+ { }
+ }
+ // Create the folder path, and if using custom mount point it will be created as well in the same operation.
+ {
+ System.Net.HttpWebRequest req = CreateRequest(System.Net.WebRequestMethods.Http.Post, "", "mkDir=true", false);
+ Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
+ using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)areq.GetResponse())
+ { }
+ }
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ public void Dispose()
+ {
+ }
+
+ #endregion
+
+ private System.Net.HttpWebRequest CreateRequest(string method, string url, string queryparams)
+ {
+ System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url + (string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams));
+ req.Method = method;
+ req.Credentials = m_userInfo;
+ req.PreAuthenticate = true; // We need this under Mono for some reason, and it appears some servers require this as well
+ req.KeepAlive = false;
+ req.UserAgent = "Duplicati Jottacloud Client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
+ req.Headers.Add("x-jftp-version", API_VERSION);
+ return req;
+ }
+
+ private System.Net.HttpWebRequest CreateRequest(string method, string remotename, string queryparams, bool upload)
+ {
+ var url = (upload ? m_url_upload : m_url) + Library.Utility.Uri.UrlEncode(remotename).Replace("+", "%20");
+ return CreateRequest(method, url, queryparams);
+ }
+
+ #region IStreamingBackend Members
+
+ public bool SupportsStreaming
+ {
+ get { return true; }
+ }
+
+ public void Get(string remotename, System.IO.Stream stream)
+ {
+ var req = CreateRequest(System.Net.WebRequestMethods.Http.Get, remotename, "mode=bin", false);
+ var areq = new Utility.AsyncHttpRequest(req);
+ using (var resp = (System.Net.HttpWebResponse)areq.GetResponse())
+ using (var s = areq.GetResponseStream())
+ Utility.Utility.CopyStream(s, stream, true, m_copybuffer);
+ }
+
+ public void Put(string remotename, System.IO.Stream stream)
+ {
+ // Some challenges with uploading to Jottacloud:
+ // - Jottacloud supports use of a custom header where we can tell the server the MD5 hash of the file
+ // we are uploading, and then it will verify the content of our request against it. But the HTTP
+ // status code we get back indicates success even if there is a mismatch, so we must dig into the
+ // XML response to see if we were able to correctly upload the new content or not. Another issue
+ // is that if the stream is not seek-able we have a challenge pre-calculating MD5 hash on it before
+ // writing it out on the HTTP request stream. And even if the stream is seek-able it may be throttled.
+ // One way to avoid using the throttled stream for calculating the MD5 is to try to get the
+ // underlying stream from the "m_basestream" field, with fall-back to a temporary file.
+ // - We can instead chose to upload the data without setting the MD5 hash header. The server will
+ // calculate the MD5 on its side and return it in the response back to use. We can then compare it
+ // with the MD5 hash of the stream (using a MD5CalculatingStream), and if there is a mismatch we can
+ // request the server to remove the file again and throw an exception. But there is a requirement that
+ // we specify the file size in a custom header. And if the stream is not seek-able we are not able
+ // to use stream.Length, so we are back at square one.
+ Duplicati.Library.Utility.TempFile tmpFile = null;
+ var baseStream = stream;
+ while (baseStream is Duplicati.Library.Utility.OverrideableStream)
+ baseStream = typeof(Duplicati.Library.Utility.OverrideableStream).GetField("m_basestream", System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(baseStream) as System.IO.Stream;
+ if (baseStream == null)
+ throw new Exception(string.Format("Unable to unwrap stream from: {0}", stream.GetType()));
+ string md5Hash;
+ if (baseStream.CanSeek)
+ {
+ var originalPosition = baseStream.Position;
+ using (var md5 = System.Security.Cryptography.MD5.Create())
+ md5Hash = Library.Utility.Utility.ByteArrayAsHexString(md5.ComputeHash(baseStream));
+ baseStream.Position = originalPosition;
+ }
+ else
+ {
+ // No seeking possible, use a temp file
+ tmpFile = new Duplicati.Library.Utility.TempFile();
+ using (var os = System.IO.File.OpenWrite(tmpFile))
+ using (var md5 = new Utility.MD5CalculatingStream(baseStream))
+ {
+ Library.Utility.Utility.CopyStream(md5, os, true, m_copybuffer);
+ md5Hash = md5.GetFinalHashString();
+ }
+ stream = System.IO.File.OpenRead(tmpFile);
+ }
+ try
+ {
+ // Create request, with query parameter, and a few custom headers.
+ // NB: If we wanted to we could send the same POST request as below but without the file contents
+ // and with "cphash=[md5Hash]" as the only query parameter. Then we will get an HTTP 200 (OK) response
+ // if an identical file already exists, and we can skip uploading the new file. We will get
+ // HTTP 404 (Not Found) if file does not exists or it exists with a different hash, in which
+ // case we must send a new request to upload the new content.
+ var fileSize = stream.Length;
+ var req = CreateRequest(System.Net.WebRequestMethods.Http.Post, remotename, "umode=nomultipart", true);
+ req.Headers.Add("JMd5", md5Hash); // Not required, but it will make the server verify the content and mark the file as corrupt if there is a mismatch.
+ req.Headers.Add("JSize", fileSize.ToString()); // Required, and used to mark file as incomplete if we upload something be the total size of the original file!
+ // File time stamp headers: Since we are working with a stream here we do not know the local file's timestamps,
+ // and then we can just omit the JCreated and JModified and let the server automatically set the current time.
+ //req.Headers.Add("JCreated", timeCreated);
+ //req.Headers.Add("JModified", timeModified);
+ req.ContentType = "application/octet-stream";
+ req.ContentLength = fileSize;
+ // Write post data request
+ var areq = new Utility.AsyncHttpRequest(req);
+ using (var rs = areq.GetRequestStream())
+ Utility.Utility.CopyStream(stream, rs, true, m_copybuffer);
+ // Send request, and check response
+ using (var resp = (System.Net.HttpWebResponse)areq.GetResponse())
+ {
+ if (resp.StatusCode != System.Net.HttpStatusCode.Created)
+ throw new System.Net.WebException(Strings.Jottacloud.FileUploadError, null, System.Net.WebExceptionStatus.ProtocolError, resp);
+
+ // Request seems to be successful, but we must verify the response XML content to be sure that the file
+ // was correctly uploaded: The server will verify the JSize header and mark the file as incomplete if
+ // there was mismatch, and it will verify the JMd5 header and mark the file as corrupt if there was a hash
+ // mismatch. The returned XML contains a file element, and if upload was error free it contains a single
+ // child element "currentRevision", which has a "state" child element with the string "COMPLETED".
+ // If there was a problem we should have a "latestRevision" child element (as the only child if the file
+ // was new or had no previous complete versions, or together with a "currentRevision" if there was a previous
+ // complete version), and this will have state with value "INCOMPLETE" or "CORRUPT".
+ using (var rs = areq.GetResponseStream())
+ {
+ var doc = new System.Xml.XmlDocument();
+ try { doc.Load(rs); }
+ catch (System.Xml.XmlException)
+ {
+ throw new System.Net.WebException(Strings.Jottacloud.FileUploadError, System.Net.WebExceptionStatus.ProtocolError);
+ }
+ bool uploadCompletedSuccessfully = false;
+ var xFile = doc["file"];
+ if (xFile != null)
+ {
+ var xRevState = xFile.SelectSingleNode("latestRevision/state");
+ if (xRevState == null)
+ xRevState = xFile.SelectSingleNode("currentRevision/state");
+ if (xRevState != null)
+ uploadCompletedSuccessfully = xRevState.InnerText == "COMPLETED";
+ }
+ if (!uploadCompletedSuccessfully) // Report error (and we just let the incomplete/corrupt file revision stay on the server..)
+ throw new System.Net.WebException(Strings.Jottacloud.FileUploadError, System.Net.WebExceptionStatus.ProtocolError);
+ }
+ }
+ }
+ finally
+ {
+ try
+ {
+ if (tmpFile != null)
+ tmpFile.Dispose();
+ }
+ catch { }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Duplicati/Library/Backend/Jottacloud/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/Jottacloud/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..3b65be92d
--- /dev/null
+++ b/Duplicati/Library/Backend/Jottacloud/Properties/AssemblyInfo.cs
@@ -0,0 +1,54 @@
+#region Disclaimer / License
+// Copyright (C) 2017, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+#endregion
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Duplicati.Library.Backend.Jottacloud")]
+[assembly: AssemblyDescription("A Jottacloud backend for Duplicati")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Duplicati Team")]
+[assembly: AssemblyProduct("Duplicati.Backend.Jottacloud")]
+[assembly: AssemblyCopyright("Copyright © Duplicati Team 2017")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("4fb0b20e-c5c6-4085-a8fa-e0e0722fb5a2")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("2.0.0.7")]
+[assembly: AssemblyFileVersion("2.0.0.7")]
diff --git a/Duplicati/Library/Backend/Jottacloud/Strings.cs b/Duplicati/Library/Backend/Jottacloud/Strings.cs
new file mode 100644
index 000000000..871ddb815
--- /dev/null
+++ b/Duplicati/Library/Backend/Jottacloud/Strings.cs
@@ -0,0 +1,21 @@
+using Duplicati.Library.Localization.Short;
+namespace Duplicati.Library.Backend.Strings {
+ internal static class Jottacloud
+ {
+ public static string DisplayName { get { return LC.L(@"Jottacloud"); } }
+ public static string Description { get { return LC.L(@"This backend can read and write data to Jottacloud using it's REST protocol. Allowed format is ""jottacloud://folder/subfolder""."); } }
+ public static string NoUsernameError { get { return LC.L(@"No username given"); } }
+ public static string NoPasswordError { get { return LC.L(@"No password given"); } }
+ public static string NoPathError { get { return LC.L(@"No path given, cannot upload files to the root folder"); } }
+ 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 DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username 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 DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
+ 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 DescriptionDeviceShort { get { return LC.L(@"Supplies the backup device to use"); } }
+ 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 DescriptionMountPointShort { get { return LC.L(@"Supplies the mount point to use on the server"); } }
+ 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); }
+ }
+}
diff --git a/Duplicati/Library/Backend/Mega/Strings.cs b/Duplicati/Library/Backend/Mega/Strings.cs
index 00d56dd09..d44925690 100644
--- a/Duplicati/Library/Backend/Mega/Strings.cs
+++ b/Duplicati/Library/Backend/Mega/Strings.cs
@@ -9,6 +9,6 @@ namespace Duplicati.Library.Backend.Strings {
public static string NoPasswordError { get { return LC.L(@"No password given"); } }
public static string NoUsernameError { get { return LC.L(@"No username given"); } }
public static string NoPathError { get { return LC.L(@"No path given, cannot upload files to the root folder"); } }
- public static string Description { get { return LC.L(@"This backend can read and write data to Mega.co.nz. Allowed formats are: ""mega://folder/subfolder"""); } }
+ public static string Description { get { return LC.L(@"This backend can read and write data to Mega.co.nz. Allowed formats are: ""mega://folder/subfolder"""); } }
}
}
diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj
index 0c6eab3fe..a3c748c43 100644
--- a/Duplicati/Server/Duplicati.Server.csproj
+++ b/Duplicati/Server/Duplicati.Server.csproj
@@ -163,6 +163,10 @@
<Project>{F61679A9-E5DE-468A-B5A4-05F92D0143D2}</Project>
<Name>Duplicati.Library.Backend.FTP</Name>
</ProjectReference>
+ <ProjectReference Include="..\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj">
+ <Project>{2cd5dbc3-3da6-432d-ba97-f0b8d24501c2}</Project>
+ <Name>Duplicati.Library.Backend.Jottacloud</Name>
+ </ProjectReference>
<ProjectReference Include="..\Library\Backend\OAuthHelper\Duplicati.Library.OAuthHelper.csproj">
<Project>{d4c37c33-5e73-4b56-b2c3-dc4a6baa36bb}</Project>
<Name>Duplicati.Library.OAuthHelper</Name>
diff --git a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
index 2f3642d56..6215534c3 100644
--- a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
+++ b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
@@ -23,6 +23,7 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
EditUriBackendConfig.templates['gcs'] = 'templates/backends/gcs.html';
EditUriBackendConfig.templates['b2'] = 'templates/backends/b2.html';
EditUriBackendConfig.templates['mega'] = 'templates/backends/mega.html';
+ EditUriBackendConfig.templates['jottacloud'] = 'templates/backends/jottacloud.html';
EditUriBackendConfig.templates['box'] = 'templates/backends/oauth.html';
EditUriBackendConfig.templates['dropbox'] = 'templates/backends/oauth.html';
@@ -355,6 +356,10 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
EditUriBackendConfig.mergeServerAndPath(scope);
};
+ EditUriBackendConfig.parsers['jottacloud'] = function(scope, module, server, port, path, options) {
+ EditUriBackendConfig.mergeServerAndPath(scope);
+ };
+
// Builders take the scope and produce the uri output
EditUriBackendConfig.builders['s3'] = function(scope) {
var opts = {
@@ -519,6 +524,23 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
return url;
};
+ EditUriBackendConfig.builders['jottacloud'] = function(scope) {
+ var opts = { };
+
+ EditUriBackendConfig.merge_in_advanced_options(scope, opts);
+
+ // Slightly better error message
+ scope.Folder = scope.Path;
+
+ var url = AppUtils.format('{0}://{1}{2}',
+ scope.Backend.Key,
+ scope.Path,
+ AppUtils.encodeDictAsUrl(opts)
+ );
+
+ return url;
+ };
+
EditUriBackendConfig.validaters['file'] = function(scope, continuation) {
if (EditUriBackendConfig.require_path(scope))
continuation();
@@ -697,5 +719,15 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
continuation();
};
+ EditUriBackendConfig.validaters['jottacloud'] = function(scope, continuation) {
+ scope.Path = scope.Path || '';
+ var res =
+ EditUriBackendConfig.require_field(scope, 'Username', gettextCatalog.getString('Username')) &&
+ EditUriBackendConfig.require_field(scope, 'Password', gettextCatalog.getString('Password'));
+
+ if (res)
+ continuation();
+ };
+
});
diff --git a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js
index c2773146d..dc7467689 100644
--- a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js
+++ b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js
@@ -67,7 +67,8 @@ backupApp.service('SystemInfo', function($rootScope, $timeout, $cookies, AppServ
'box': null,
'od4b': null,
'mssp': null,
- 'dropbox': null
+ 'dropbox': null,
+ 'jottacloud': null
}
};
diff --git a/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html b/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html
new file mode 100644
index 000000000..40d523ec0
--- /dev/null
+++ b/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html
@@ -0,0 +1,13 @@
+<div class="input text">
+ <label for="jottacloud_path" translate>Folder path</label>
+ <input type="text" name="jottacloud_path" id="jottacloud_path" ng-model="$parent.Path" placeholder="{{'Enter folder path name' | translate}}" />
+</div>
+
+<div class="input text">
+ <label for="jottacloud_username" translate>Username</label>
+ <input type="text" name="jottacloud_username" id="jottacloud_username" ng-model="$parent.Username" placeholder="{{'Username' | translate}}" />
+</div>
+<div class="input password">
+ <label for="jottacloud_password" translate>Password</label>
+ <input type="password" name="jottacloud_password" id="jottacloud_password" ng-model="$parent.Password" placeholder="{{'Password' | translate}}" />
+</div>