diff --git a/Duplicati/Library/Backend/Filen/FilenClient.cs b/Duplicati/Library/Backend/Filen/FilenClient.cs index 61d897d5f..c7c6a54cd 100644 --- a/Duplicati/Library/Backend/Filen/FilenClient.cs +++ b/Duplicati/Library/Backend/Filen/FilenClient.cs @@ -20,6 +20,7 @@ // DEALINGS IN THE SOFTWARE. using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -269,13 +270,13 @@ public class FilenClient : IDisposable { response.EnsureSuccessStatusCode(); - var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - var result = JsonSerializer.Deserialize>(json); - //var result = await response.Content.ReadFromJsonAsync>(cancellationToken).ConfigureAwait(false); + // var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + // var result = JsonSerializer.Deserialize>(json); + var result = await response.Content.ReadFromJsonAsync>(cancellationToken).ConfigureAwait(false); if (result is null) throw new Exception("Failed to read response"); if (!result.Status || !string.IsNullOrWhiteSpace(result.Error)) - throw new Exception($"Server returned error: {result.Error ?? result.Message ?? "Unknown"}"); + throw new UserInformationException($"{result.Error ?? result.Message ?? "Unknown"}", "FilenAPIError"); return result.Data; } @@ -456,6 +457,8 @@ public class FilenClient : IDisposable bytesRead += read; } + Logging.Log.WriteVerboseMessage(LOGTAG, "UploadingChunk", "Uploading chunk {0}, size {1}", chunk, bytesRead); + // Stream is exhausted if (bytesRead == 0) break; @@ -482,6 +485,8 @@ public class FilenClient : IDisposable chunk++; } + Logging.Log.WriteVerboseMessage(LOGTAG, "UploadingChunk", "Finished uploading {0} chunks", chunk); + return chunk; } @@ -554,6 +559,10 @@ public class FilenClient : IDisposable var totalSize = file.Size; while (downloaded < totalSize) { + if (chunk >= file.Chunks) + throw new Exception($"Attempted to download more chunks than available, expected {file.Chunks}, got {chunk}. File: {file.Name}, total size: {totalSize}, downloaded: {downloaded}"); + + Logging.Log.WriteVerboseMessage(LOGTAG, "DownloadingChunk", "Downloading chunk {0}, size {1}", chunk, totalSize - downloaded); var url = $"{EgestUrls[Random.Shared.Next(0, EgestUrls.Count)]}/{file.Region}/{file.Bucket}/{file.Uuid}/{chunk}"; using var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _authResult.ApiKey); diff --git a/Duplicati/Library/Backend/Filen/FilenCrypto.cs b/Duplicati/Library/Backend/Filen/FilenCrypto.cs index 8f10cde7e..add15e35f 100644 --- a/Duplicati/Library/Backend/Filen/FilenCrypto.cs +++ b/Duplicati/Library/Backend/Filen/FilenCrypto.cs @@ -233,13 +233,13 @@ public static class FilenCrypto } else if (version == "003") { - Logging.Log.WriteWarningMessage(LOGTAG, "EncV3NotTested", null, "Encryption version 3 is not tested."); + Logging.Log.WriteVerboseMessage(LOGTAG, "EncV3NotTested", "Encryption version 3 encoutered for metadata, but this is not tested."); if (key.Key.Length != 64) throw new ArgumentException($"Invalid key length {key.Key.Length}. Expected 64 (hex).", nameof(key)); var keyBuffer = Convert.FromHexString(key.Key); - var ivBuffer = Convert.FromHexString(metadata.Substring(3, 24)); // Sliced from 3, length 24 - var encrypted = Convert.FromBase64String(metadata.Substring(27)); + var ivBuffer = Convert.FromHexString(metadata.Substring(3, 12)); // Sliced from 3, length 24 + var encrypted = Convert.FromBase64String(metadata.Substring(15)); return Encoding.UTF8.GetString(AesGcmDecrypt(keyBuffer, ivBuffer, encrypted)); } diff --git a/Duplicati/Library/Backend/Filen/Strings.cs b/Duplicati/Library/Backend/Filen/Strings.cs index c12ad4531..5d787db3e 100644 --- a/Duplicati/Library/Backend/Filen/Strings.cs +++ b/Duplicati/Library/Backend/Filen/Strings.cs @@ -23,8 +23,8 @@ namespace Duplicati.Library.Backend.Strings { internal static class FilenBackend { - public static string Description => LC.L(@"This backend can read and write data to Filen.io using its REST protocol. Allowed format is ""filen://folder/subfolder""."); - public static string DisplayName => LC.L(@"Local folder or drive"); + public static string Description => LC.L(@"This backend can read and write data to Filen.io using its REST protocol. Supported format is ""filen://folder/subfolder""."); + public static string DisplayName => LC.L(@"Filen.io"); public static string TwoFactorShort => LC.L(@"Optional 2-factor code"); public static string TwoFactorLong => LC.L(@"The 2-factor code to use for authentication, leave empty if the account is not MFA protected. Not that a new code must be provided by the user for each authentication attempt."); public static string MoveToTrashShort => LC.L(@"Move to trash"); diff --git a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js index c72b18138..ba61e296a 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js @@ -35,8 +35,9 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.templates['aliyunoss'] = 'templates/backends/aliyunoss.html'; EditUriBackendConfig.templates['e2'] = 'templates/backends/e2.html'; EditUriBackendConfig.templates['pcloud'] = 'templates/backends/pcloud.html'; - EditUriBackendConfig.templates['smb'] = 'templates/backends/smb.html'; + EditUriBackendConfig.templates['smb'] = 'templates/backends/smb.html'; EditUriBackendConfig.templates['cifs'] = 'templates/backends/smb.html'; + EditUriBackendConfig.templates['filen'] = 'templates/backends/filen.html'; EditUriBackendConfig.testers['s3'] = function(scope, callback) { @@ -637,6 +638,15 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.parsers['cifs'] = EditUriBackendConfig.parsers['smb']; + EditUriBackendConfig.parsers['filen'] = function (scope, module, server, path, port, options) { + console.log("filen parser", scope, module, server, path, port, options); + var p1 = server ?? ''; + var p2 = path ?? ''; + var combined = p1; + if (p1.length > 0 && p2.length > 0) + combined += '/' + p2; + scope.Path = combined; + }; // Builders take the scope and produce the uri output EditUriBackendConfig.builders['s3'] = function (scope) { @@ -1007,6 +1017,22 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.builders['cifs'] = EditUriBackendConfig.builders['smb']; + EditUriBackendConfig.builders['filen'] = function (scope) { + var opts = {}; + EditUriBackendConfig.merge_in_advanced_options(scope, opts, true); + // Remove trailing and leading slashes + if (scope.Path) + scope.Path = scope.Path.replace(/^[/\\]+/, '').replace(/[/\\]+$/, ''); + else + scope.Path = ''; + + return AppUtils.format('{0}://{1}{2}', + scope.Backend.Key, + scope.Path, + AppUtils.encodeDictAsUrl(opts) + ); + } + EditUriBackendConfig.validaters['smb'] = function (scope, continuation) { if (EditUriBackendConfig.require_server(scope) && EditUriBackendConfig.require_username(scope) @@ -1016,6 +1042,11 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.validaters['smb'] = EditUriBackendConfig.validaters['cifs']; + EditUriBackendConfig.validaters['filen'] = function (scope, continuation) { + if (EditUriBackendConfig.require_username_and_password(scope)) + EditUriBackendConfig.recommend_path(scope, continuation); + }; + EditUriBackendConfig.validaters['file'] = function (scope, continuation) { if (EditUriBackendConfig.require_path(scope)) continuation(); diff --git a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js index 1a0c76ee4..e96cc6626 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js @@ -78,7 +78,8 @@ backupApp.service('SystemInfo', function($rootScope, $timeout, $cookies, AppServ 'rclone': null, 'cos': null, 'aliyunoss': null, - 'pcloud': null + 'pcloud': null, + 'filen': null, } }; diff --git a/Duplicati/Server/webroot/ngax/templates/backends/filen.html b/Duplicati/Server/webroot/ngax/templates/backends/filen.html new file mode 100644 index 000000000..5ec521a01 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/templates/backends/filen.html @@ -0,0 +1,13 @@ +
+ + +
+ +
+ + +
+
+ + +