From c46e07b1e5efe76e99766d34ff94545664bdf14f Mon Sep 17 00:00:00 2001 From: Joris Date: Tue, 19 Mar 2019 19:32:00 +0100 Subject: Implemented Jottacloud multithreaded downloading support --- Duplicati/Library/Backend/Jottacloud/Jottacloud.cs | 148 ++++++++++++++++++++- Duplicati/Library/Backend/Jottacloud/Strings.cs | 4 + 2 files changed, 148 insertions(+), 4 deletions(-) diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index 472043acf..b5281e313 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -20,6 +20,7 @@ using System; using System.Collections.Generic; using Duplicati.Library.Interface; +using System.Threading.Tasks; using Duplicati.Library.Common.IO; namespace Duplicati.Library.Backend @@ -36,6 +37,8 @@ namespace Duplicati.Library.Backend 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_THREADS = "jottacloud-threads"; + private const string JFS_CHUNKSIZE = "jottacloud-chunksize"; private const string JFS_DATE_FORMAT = "yyyy'-'MM'-'dd-'T'HH':'mm':'ssK"; private readonly string m_device; private readonly bool m_device_builtin; @@ -47,6 +50,21 @@ namespace Duplicati.Library.Backend private readonly System.Net.NetworkCredential m_userInfo; private readonly byte[] m_copybuffer = new byte[Duplicati.Library.Utility.Utility.DEFAULT_BUFFER_SIZE]; + private static readonly string JFS_DEFAULT_CHUNKSIZE = "5mb"; + private static readonly string JFS_DEFAULT_THREADS = "4"; + private readonly int m_threads; + private readonly int m_chunksize; + + /// + /// The default maximum number of concurrent connections allowed by a ServicePoint object is 2. + /// It should be increased to allow multiple download threads. + /// https://stackoverflow.com/a/44637423/1105812 + /// + static Jottacloud() + { + System.Net.ServicePointManager.DefaultConnectionLimit = 1000; + } + public Jottacloud() { } @@ -144,6 +162,22 @@ namespace Duplicati.Library.Backend 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. + + m_threads = options.ContainsKey(JFS_THREADS) ? int.Parse(options[JFS_THREADS]) : 4; + + if (!options.TryGetValue(JFS_CHUNKSIZE, out var tmp)) + { + tmp = JFS_DEFAULT_CHUNKSIZE; + } + + var chunksize = Utility.Sizeparser.ParseSize(tmp, "mb"); + + if (chunksize > int.MaxValue || chunksize < 1024) + { + throw new ArgumentOutOfRangeException(nameof(chunksize), string.Format("The chunk size cannot be less than {0}, nor larger than {1}", 1024, int.MaxValue)); + } + + m_chunksize = (int)chunksize; } #region IBackend Members @@ -220,6 +254,64 @@ namespace Duplicati.Library.Backend } } + /// + /// Retrieves info for a single file (used to determine file size for chunking) + /// + /// + /// + public IFileEntry Info(string remotename) + { + var doc = new System.Xml.XmlDocument(); + try + { + // Send request and load XML response. + var req = CreateRequest(System.Net.WebRequestMethods.Http.Get, remotename, "", 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 FileMissingException(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. + var xFile = doc.DocumentElement; + if (xFile.Attributes["deleted"] != null) + { + throw new FileMissingException(); + } + 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("state"); + if (xNode.InnerText == "COMPLETED") // Think "currentRevision" always is a complete version, but just to be on the safe side.. + { + 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); + return fe; + } + } + return null; + } + public void Put(string remotename, string filename) { using (System.IO.FileStream fs = System.IO.File.OpenRead(filename)) @@ -249,6 +341,8 @@ namespace Duplicati.Library.Backend 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)), + new CommandLineArgument(JFS_THREADS, CommandLineArgument.ArgumentType.Integer, Strings.Jottacloud.ThreadsShort, Strings.Jottacloud.ThreadsLong, JFS_DEFAULT_THREADS), + new CommandLineArgument(JFS_CHUNKSIZE, CommandLineArgument.ArgumentType.Size, Strings.Jottacloud.ChunksizeShort, Strings.Jottacloud.ChunksizeLong, JFS_DEFAULT_CHUNKSIZE), }); } } @@ -315,15 +409,20 @@ namespace Duplicati.Library.Backend public bool SupportsStreaming { get { return true; } - } - + } + public string[] DNSName { get { return new string[] { new Uri(JFS_ROOT).Host, new Uri(JFS_ROOT_UPLOAD).Host }; } - } - + } + public void Get(string remotename, System.IO.Stream stream) { + if (m_threads > 1) + { + ParallelGet(remotename, stream); + return; + } // Downloading from Jottacloud: Will only succeed if the file has a completed revision, // and if there are multiple versions of the file we will only get the latest completed version, // ignoring any incomplete or corrupt versions. @@ -334,6 +433,47 @@ namespace Duplicati.Library.Backend Utility.Utility.CopyStream(s, stream, true, m_copybuffer); } + /// + /// Fetches the file in chunks (parallelized) + /// + public void ParallelGet(string remotename, System.IO.Stream stream) + { + var size = Info(remotename).Size; + + var chunks = new Queue>(); // Tuple => Position (from), Position (to) + + long position = 0; + + while (position < size) + { + chunks.Enqueue(new Tuple(position, position += Math.Min(m_chunksize, size - position))); + } + + var tasks = new Queue>(); + + while (tasks.Count > 0 || chunks.Count > 0) + { + while (chunks.Count > 0 && tasks.Count < m_threads) + { + var item = chunks.Dequeue(); + tasks.Enqueue(Task.Run(() => + { + var req = CreateRequest(System.Net.WebRequestMethods.Http.Get, remotename, "mode=bin", false); + req.AddRange(item.Item1, item.Item2 - 1); + var areq = new Utility.AsyncHttpRequest(req); + using (var resp = (System.Net.HttpWebResponse)areq.GetResponse()) + using (var s = areq.GetResponseStream()) + using (var reader = new System.IO.BinaryReader(s)) + { + return reader.ReadBytes((int)(item.Item2 - item.Item1)); + } + })); + } + var buffer = tasks.Dequeue().Result; + stream.Write(buffer, 0, buffer.Length); + } + } + public void Put(string remotename, System.IO.Stream stream) { // Some challenges with uploading to Jottacloud: diff --git a/Duplicati/Library/Backend/Jottacloud/Strings.cs b/Duplicati/Library/Backend/Jottacloud/Strings.cs index 871ddb815..08ba011b8 100644 --- a/Duplicati/Library/Backend/Jottacloud/Strings.cs +++ b/Duplicati/Library/Backend/Jottacloud/Strings.cs @@ -17,5 +17,9 @@ namespace Duplicati.Library.Backend.Strings { 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); } + public static string ThreadsShort { get { return LC.L(@"Number of threads for restore operations."); } } + public static string ThreadsLong { get { return LC.L(@"Number of threads for restore operations. In some cases the download rate is limited to 18.5 Mbps per stream. Use multiple threads to increase throughput."); } } + public static string ChunksizeShort { get { return LC.L(@"The chunk size for simultaneous downloading."); } } + public static string ChunksizeLong { get { return LC.L(@"The chunk size for simultaneous downloading. These chunks will be held in memory, so keep it as low as possible."); } } } } -- cgit v1.2.3 From 38968d03eb7756bf94b244ead49aea49bb465b76 Mon Sep 17 00:00:00 2001 From: Joris Date: Tue, 19 Mar 2019 21:44:22 +0100 Subject: Removing Codacy recommendation --- Duplicati/Library/Backend/Jottacloud/Jottacloud.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index 442870c4e..66913dcf5 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -259,7 +259,7 @@ namespace Duplicati.Library.Backend /// /// /// - public static IFileEntry Info(string remotename) + public IFileEntry Info(string remotename) { var doc = new System.Xml.XmlDocument(); try -- cgit v1.2.3 From eeab3ced58ac8b9ed0fdbded7fb13b1e7a177a31 Mon Sep 17 00:00:00 2001 From: Joris Date: Wed, 20 Mar 2019 09:12:19 +0100 Subject: Removed unnecessary GetResponse calls --- Duplicati/Library/Backend/Jottacloud/Jottacloud.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index 66913dcf5..b75a728ba 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -200,7 +200,6 @@ namespace Duplicati.Library.Backend // 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); } @@ -267,7 +266,6 @@ namespace Duplicati.Library.Backend // Send request and load XML response. var req = CreateRequest(System.Net.WebRequestMethods.Http.Get, remotename, "", false); var areq = new Utility.AsyncHttpRequest(req); - using (var resp = (System.Net.HttpWebResponse)areq.GetResponse()) using (var rs = areq.GetResponseStream()) doc.Load(rs); } @@ -428,7 +426,6 @@ namespace Duplicati.Library.Backend // ignoring any incomplete or corrupt versions. 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); } @@ -463,11 +460,11 @@ namespace Duplicati.Library.Backend var req = CreateRequest(System.Net.WebRequestMethods.Http.Get, remotename, "mode=bin", false); req.AddRange(item.Item1, item.Item2 - 1); var areq = new Utility.AsyncHttpRequest(req); - using (var resp = (System.Net.HttpWebResponse)areq.GetResponse()) using (var s = areq.GetResponseStream()) using (var reader = new System.IO.BinaryReader(s)) { - return reader.ReadBytes((int)(item.Item2 - item.Item1)); + var length = item.Item2 - item.Item1; + return reader.ReadBytes((int)length); } })); } -- cgit v1.2.3 From c93e0eed4da8b1c89519e60594d0d9eb3063d2e8 Mon Sep 17 00:00:00 2001 From: Joris Date: Thu, 21 Mar 2019 10:14:57 +0100 Subject: Processed code review findings --- Duplicati/Library/Backend/Jottacloud/Jottacloud.cs | 88 ++++++++++------------ 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index b75a728ba..a13137a4e 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -19,6 +19,7 @@ #endregion using Duplicati.Library.Common.IO; using Duplicati.Library.Interface; +using Duplicati.Library.Localization.Short; using System; using System.Collections.Generic; using System.Threading; @@ -53,7 +54,7 @@ namespace Duplicati.Library.Backend private static readonly string JFS_DEFAULT_CHUNKSIZE = "5mb"; private static readonly string JFS_DEFAULT_THREADS = "4"; private readonly int m_threads; - private readonly int m_chunksize; + private readonly long m_chunksize; /// /// The default maximum number of concurrent connections allowed by a ServicePoint object is 2. @@ -163,7 +164,7 @@ namespace Duplicati.Library.Backend 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. - m_threads = options.ContainsKey(JFS_THREADS) ? int.Parse(options[JFS_THREADS]) : 4; + m_threads = int.Parse(options.ContainsKey(JFS_THREADS) ? options[JFS_THREADS] : JFS_DEFAULT_THREADS); if (!options.TryGetValue(JFS_CHUNKSIZE, out var tmp)) { @@ -174,10 +175,10 @@ namespace Duplicati.Library.Backend if (chunksize > int.MaxValue || chunksize < 1024) { - throw new ArgumentOutOfRangeException(nameof(chunksize), string.Format("The chunk size cannot be less than {0}, nor larger than {1}", 1024, int.MaxValue)); + throw new ArgumentOutOfRangeException(nameof(chunksize), string.Format("The chunk size cannot be less than {0}, nor larger than {1}", Utility.Utility.FormatSizeString(1024), Utility.Utility.FormatSizeString(int.MaxValue))); } - m_chunksize = (int)chunksize; + m_chunksize = chunksize; } #region IBackend Members @@ -227,30 +228,39 @@ namespace Duplicati.Library.Backend } 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) + var fe = ToFileEntry(xFile); + if (fe != null) { - System.Xml.XmlNode xNode = xRevision.SelectSingleNode("state"); - if (xNode.InnerText == "COMPLETED") // Think "currentRevision" always is a complete version, but just to be on the safe side.. - { - 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); - yield return fe; - } + yield return fe; + } + } + } + + public IFileEntry ToFileEntry(System.Xml.XmlNode xFile) + { + 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("state"); + if (xNode.InnerText == "COMPLETED") // Think "currentRevision" always is a complete version, but just to be on the safe side.. + { + 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); + return fe; } } + return null; } /// @@ -282,32 +292,10 @@ namespace Duplicati.Library.Backend var xFile = doc.DocumentElement; if (xFile.Attributes["deleted"] != null) { - throw new FileMissingException(); - } - 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("state"); - if (xNode.InnerText == "COMPLETED") // Think "currentRevision" always is a complete version, but just to be on the safe side.. - { - 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); - return fe; - } + throw new FileMissingException(string.Format("{0}: {1}", LC.L("The requested file does not exist"), remotename)); } - return null; + + return ToFileEntry(xFile); } public Task PutAsync(string remotename, string filename, CancellationToken cancelToken) -- cgit v1.2.3 From 6440bf7c903161f18c2fb56ccf26f22082cc2a55 Mon Sep 17 00:00:00 2001 From: Joris Date: Fri, 22 Mar 2019 09:35:09 +0100 Subject: Fixed: The file X was downloaded and had size 0 but the size was expected to be N --- Duplicati/Library/Backend/Jottacloud/Jottacloud.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index a13137a4e..065893b8f 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -246,15 +246,16 @@ namespace Duplicati.Library.Backend System.Xml.XmlNode xRevision = xFile.SelectSingleNode("currentRevision"); if (xRevision != null) { - System.Xml.XmlNode xNode = xRevision.SelectSingleNode("state"); - if (xNode.InnerText == "COMPLETED") // Think "currentRevision" always is a complete version, but just to be on the safe side.. + System.Xml.XmlNode xState = xRevision.SelectSingleNode("state"); + if (xState != null && xState.InnerText == "COMPLETED") // Think "currentRevision" always is a complete version, but just to be on the safe side.. { + System.Xml.XmlNode xSize = xRevision.SelectSingleNode("size"); long size; - if (xNode == null || !long.TryParse(xNode.InnerText, out size)) + if (xSize == null || !long.TryParse(xSize.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)) + System.Xml.XmlNode xModified = xRevision.SelectSingleNode("modified"); // There is created, modified and updated time stamps, but not last accessed. + if (xModified == null || !DateTime.TryParseExact(xModified.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); return fe; -- cgit v1.2.3 From 5bffd23879700df048ea116dd8c1da1ad1b6fdb7 Mon Sep 17 00:00:00 2001 From: Joris Date: Mon, 25 Mar 2019 10:46:58 +0100 Subject: Commented on chunk size limiting --- Duplicati/Library/Backend/Jottacloud/Jottacloud.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index 065893b8f..f6047970e 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -173,6 +173,8 @@ namespace Duplicati.Library.Backend var chunksize = Utility.Sizeparser.ParseSize(tmp, "mb"); + // Chunk size is bound by BinaryReader.ReadBytes(length) where length is an int. + if (chunksize > int.MaxValue || chunksize < 1024) { throw new ArgumentOutOfRangeException(nameof(chunksize), string.Format("The chunk size cannot be less than {0}, nor larger than {1}", Utility.Utility.FormatSizeString(1024), Utility.Utility.FormatSizeString(int.MaxValue))); -- cgit v1.2.3 From d1161f9872059a8033c90fe2f60df8dd5dd55639 Mon Sep 17 00:00:00 2001 From: Joris Date: Mon, 25 Mar 2019 10:47:18 +0100 Subject: Codacy recommendation --- Duplicati/Library/Backend/Jottacloud/Jottacloud.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index f6047970e..c3dab0358 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -238,7 +238,7 @@ namespace Duplicati.Library.Backend } } - public IFileEntry ToFileEntry(System.Xml.XmlNode xFile) + public static IFileEntry ToFileEntry(System.Xml.XmlNode xFile) { string name = xFile.Attributes["name"].Value; // Normal files have an "currentRevision", which represent the most recent successfully upload -- cgit v1.2.3