diff --git a/Duplicati/Library/Backend/AliyunOSS/Strings.cs b/Duplicati/Library/Backend/AliyunOSS/Strings.cs index 4cb53f5e1..a94469c89 100644 --- a/Duplicati/Library/Backend/AliyunOSS/Strings.cs +++ b/Duplicati/Library/Backend/AliyunOSS/Strings.cs @@ -11,7 +11,7 @@ namespace Duplicati.Library.Backend.Strings public static string OSSAccessKeySecretDescriptionLong { get { return LC.L(@"Access Key Secret is the key used by the user to encrypt signature strings and by OSS to verify these signature strings."); } } public static string OSSAccessKeySecretDescriptionShort { get { return LC.L(@"Access Key Secret"); } } public static string OSSBucketNameDescriptionLong { get { return LC.L(@"A storage space is a container used to store objects (Object), and all objects must belong to a specific storage space."); } } - public static string OSSBucketNameDescriptionShort { get { return LC.L(@"Bucket Name"); } } + public static string OSSBucketNameDescriptionShort { get { return LC.L(@"Bucket name"); } } public static string OSSRegionDescriptionLong { get { return LC.L(@"Region indicates the physical location of the OSS data center."); } } public static string OSSRegionDescriptionShort { get { return LC.L(@"Region"); } } public static string OSSEndpointDescriptionLong { get { return LC.L(@"Endpoint refers to the domain name through which OSS provides external services."); } } diff --git a/Duplicati/Library/Backend/Storj/Strings.cs b/Duplicati/Library/Backend/Storj/Strings.cs index 56c656add..d053d1ac9 100644 --- a/Duplicati/Library/Backend/Storj/Strings.cs +++ b/Duplicati/Library/Backend/Storj/Strings.cs @@ -26,20 +26,20 @@ namespace Duplicati.Library.Backend.Strings { public static string Description { get { return LC.L(@"This backend can read and write data to the Storj DCS."); } } public static string DisplayName { get { return LC.L(@"Storj DCS (Decentralized Cloud Storage)"); } } - public static string TestConnectionFailed { get { return LC.L(@"The connection-test failed."); } } - public static string StorjAuthMethodDescriptionLong { get { return LC.L(@"The authentication method describes which way to use to connect to the network - either via API key or via an access grant."); } } - public static string StorjAuthMethodDescriptionShort { get { return LC.L(@"The authentication method"); } } - public static string StorjSatelliteDescriptionLong { get { return LC.L(@"The satellite that keeps track of all metadata. Use a Storj DCS server for high-performance SLA-backed connectivity or use a community server. Or even host your own."); } } - public static string StorjSatelliteDescriptionShort { get { return LC.L(@"The satellite"); } } - public static string StorjAPIKeyDescriptionLong { get { return LC.L(@"The API key grants access to a specific project on your chosen satellite. Head over to the dashboard of your satellite to create one if you do not already have an API key."); } } - public static string StorjAPIKeyDescriptionShort { get { return LC.L(@"The API key"); } } - public static string StorjSecretDescriptionLong { get { return LC.L(@"The encryption passphrase is used to encrypt your data before sending it to the Storj network. This passphrase can be the only secret to provide - for Storj you do not necessary need any additional encryption (from Duplicati) in place."); } } - public static string StorjSecretDescriptionShort { get { return LC.L(@"The encryption passphrase"); } } - public static string StorjSharedAccessDescriptionLong { get { return LC.L(@"An access grant contains all information in one encrypted string. You may use it instead of a satellite, API key and secret."); } } - public static string StorjSharedAccessDescriptionShort { get { return LC.L(@"The access grant"); } } - public static string StorjBucketDescriptionLong { get { return LC.L(@"The bucket where the backup will reside in."); } } - public static string StorjBucketDescriptionShort { get { return LC.L(@"The bucket"); } } - public static string StorjFolderDescriptionLong { get { return LC.L(@"The folder within the bucket where the backup will reside in."); } } - public static string StorjFolderDescriptionShort { get { return LC.L(@"The folder"); } } + public static string TestConnectionFailed { get { return LC.L(@"Connection-test failed."); } } + public static string StorjAuthMethodDescriptionLong { get { return LC.L(@"Specify the authentication method which describes which way to use to connect to the network - either via API key or via an access grant."); } } + public static string StorjAuthMethodDescriptionShort { get { return LC.L(@"Authentication method"); } } + public static string StorjSatelliteDescriptionLong { get { return LC.L(@"Specify the satellite that keeps track of all metadata. Use a Storj DCS server for high-performance SLA-backed connectivity or use a community server. Or even host your own."); } } + public static string StorjSatelliteDescriptionShort { get { return LC.L(@"Satellite"); } } + public static string StorjAPIKeyDescriptionLong { get { return LC.L(@"Supply the API key which grants access to a specific project on your chosen satellite. Head over to the dashboard of your satellite to create one if you do not already have an API key."); } } + public static string StorjAPIKeyDescriptionShort { get { return LC.L(@"API key"); } } + public static string StorjSecretDescriptionLong { get { return LC.L(@"Supply the encryption passphrase used to encrypt your data before sending it to the Storj network. This passphrase can be the only secret to provide - for Storj you do not necessary need any additional encryption (from Duplicati) in place."); } } + public static string StorjSecretDescriptionShort { get { return LC.L(@"Encryption passphrase"); } } + public static string StorjSharedAccessDescriptionLong { get { return LC.L(@"Supply the access grant which contains all information in one encrypted string. You may use it instead of a satellite, API key and secret."); } } + public static string StorjSharedAccessDescriptionShort { get { return LC.L(@"Access grant"); } } + public static string StorjBucketDescriptionLong { get { return LC.L(@"Specify the bucket for storing the backup."); } } + public static string StorjBucketDescriptionShort { get { return LC.L(@"Bucket"); } } + public static string StorjFolderDescriptionLong { get { return LC.L(@"Specify the folder in the bucket for storing the backup."); } } + public static string StorjFolderDescriptionShort { get { return LC.L(@"Folder"); } } } } diff --git a/Duplicati/Library/Backend/TencentCOS/Strings.cs b/Duplicati/Library/Backend/TencentCOS/Strings.cs index e8a1b0615..13777b5c4 100644 --- a/Duplicati/Library/Backend/TencentCOS/Strings.cs +++ b/Duplicati/Library/Backend/TencentCOS/Strings.cs @@ -32,8 +32,8 @@ namespace Duplicati.Library.Backend.Strings public static string COSAPISecretIdDescriptionShort { get { return LC.L(@"Secret ID"); } } public static string COSAPISecretKeyDescriptionLong { get { return LC.L(@"Cloud API Secret Key."); } } public static string COSAPISecretKeyDescriptionShort { get { return LC.L(@"Secret Key"); } } - public static string COSBucketDescriptionLong { get { return LC.L(@"Bucket, format: BucketName-APPID"); } } - public static string COSBucketDescriptionShort { get { return LC.L(@"Bucket"); } } + public static string COSBucketDescriptionLong { get { return LC.L(@"Bucket name, format: BucketName-APPID"); } } + public static string COSBucketDescriptionShort { get { return LC.L(@"Bucket name"); } } public static string COSLocationDescriptionLong { get { return LC.L(@"Region is the distribution area of ​​the Tencent cloud hosting machine room. The object storage COS data is stored in the storage buckets of these regions. https://intl.cloud.tencent.com/document/product/436/6224."); } } public static string COSLocationDescriptionShort { get { return LC.L(@"Specifies COS location constraints"); } } public static string COSStorageClassDescriptionLong { get { return LC.L(@"Storage class of the object; check enumerated values at https://intl.cloud.tencent.com/document/product/436/30925."); } } diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index ec87d25c9..780f84bba 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -40,6 +40,12 @@ namespace Duplicati.Server private static readonly List ParameterFileOptionStrings = new List { "parameters-file", "parameterfile" }; +#if DEBUG + private const bool DEBUG_MODE = true; +#else + private const bool DEBUG_MODE = false; +#endif + /// /// The log tag for messages from this class /// @@ -583,13 +589,16 @@ namespace Duplicati.Server if (string.IsNullOrEmpty(serverDataFolder)) { -#if DEBUG - //debug mode uses a lock file located in the app folder - DataFolder = StartupPath; -#else - bool portableMode = commandlineOptions.ContainsKey("portable-mode") ? Library.Utility.Utility.ParseBool(commandlineOptions["portable-mode"], true) : false; + bool portableMode = commandlineOptions.ContainsKey("portable-mode") + ? Library.Utility.Utility.ParseBool(commandlineOptions["portable-mode"], true) + : (DEBUG_MODE ? true : false); // Default to portable mode in debug mode - if (portableMode) + if (DEBUG_MODE && portableMode) + { + //debug mode uses a lock file located in the app folder + DataFolder = StartupPath; + } + else if (portableMode) { //Portable mode uses a data folder in the application home dir DataFolder = System.IO.Path.Combine(StartupPath, "data"); @@ -644,7 +653,6 @@ namespace Duplicati.Server DataFolder = serverDataFolder; } -#endif } else DataFolder = Util.AppendDirSeparator(Environment.ExpandEnvironmentVariables(serverDataFolder).Trim('"')); diff --git a/Duplicati/Server/webroot/ngax/less/base.less b/Duplicati/Server/webroot/ngax/less/base.less index 3f599badd..3eb670d96 100644 --- a/Duplicati/Server/webroot/ngax/less/base.less +++ b/Duplicati/Server/webroot/ngax/less/base.less @@ -729,6 +729,7 @@ body { .stop { background: url('../img/progress-stop.png'); + background-size: 100%; z-index: 10; position: relative; @@ -743,7 +744,8 @@ body { .resume { background: url('../img/progress-resume.png'); - + background-size: 100%; + @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { background-image: url('../img/progress-resume_2x.png'); } @@ -797,6 +799,7 @@ body { > .pause { background: url('../img/pause.png'); + background-size: 100%; @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { background-image: url('../img/pause_2x.png'); @@ -809,6 +812,7 @@ body { > .pause.active { background: url('../img/resume.png'); + background-size: 100%; @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { background-image: url('../img/resume_2x.png'); @@ -821,7 +825,8 @@ body { > .throttle { background: url('../img/throttle.png'); - + background-size: 100%; + @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { background-image: url('../img/throttle_2x.png'); } diff --git a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js index 91d10399c..e687b48b5 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js @@ -997,7 +997,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.validaters['openstack'] = function (scope, continuation) { var res = EditUriBackendConfig.require_field(scope, 'Username', gettextCatalog.getString('Username')) && - EditUriBackendConfig.require_field(scope, 'Path', gettextCatalog.getString('Bucket Name')); + EditUriBackendConfig.require_field(scope, 'Path', gettextCatalog.getString('Bucket name')); if (res && (scope['openstack_server'] || '').trim().length == 0 && (scope['openstack_server_custom'] || '').trim().length == 0) res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must select or fill in the AuthURI')); @@ -1037,7 +1037,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.validaters['s3'] = function (scope, continuation) { var res = - EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Bucket Name')) && + EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Bucket name')) && EditUriBackendConfig.require_field(scope, 'Username', gettextCatalog.getString('AWS Access ID')) && EditUriBackendConfig.require_field(scope, 'Password', gettextCatalog.getString('AWS Access Key')); @@ -1083,7 +1083,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.validaters['b2'] = function (scope, continuation) { var res = - EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Bucket Name')) && + EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Bucket name')) && EditUriBackendConfig.require_field(scope, 'Username', gettextCatalog.getString('B2 Cloud Storage Account ID')) && EditUriBackendConfig.require_field(scope, 'Password', gettextCatalog.getString('B2 Cloud Storage Application Key')); @@ -1097,7 +1097,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, value: bucketname[ix].charCodeAt(), pos: ix, character: bucketname[ix], - fieldname: gettextCatalog.getString('Bucket Name') + fieldname: gettextCatalog.getString('Bucket name') })); res = false; } @@ -1160,13 +1160,13 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, if(res && scope['storj_auth_method'] == 'Access grant'){ res = EditUriBackendConfig.require_field(scope, 'storj_shared_access', gettextCatalog.getString('storj_shared_access')) && - EditUriBackendConfig.require_field(scope, 'storj_bucket', gettextCatalog.getString('Bucket')); + EditUriBackendConfig.require_field(scope, 'storj_bucket', gettextCatalog.getString('Bucket name')); } if(res && scope['storj_auth_method'] == 'API key'){ res = EditUriBackendConfig.require_field(scope, 'storj_api_key', gettextCatalog.getString('API key')) && EditUriBackendConfig.require_field(scope, 'storj_secret', gettextCatalog.getString('Encryption passphrase')) && - EditUriBackendConfig.require_field(scope, 'storj_bucket', gettextCatalog.getString('Bucket')); + EditUriBackendConfig.require_field(scope, 'storj_bucket', gettextCatalog.getString('Bucket name')); } if(res && scope['storj_auth_method'] == 'API key' && !scope['storj_satellite']){ @@ -1223,7 +1223,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, var res = EditUriBackendConfig.require_field(scope, 'Username', gettextCatalog.getString('IDrive e2 Access Key ID')) && EditUriBackendConfig.require_field(scope, 'Password', gettextCatalog.getString('IDrive e2 Access Key Secret')) && - EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Bucket Name')); + EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Bucket name')); if (res) { var re = new RegExp('[^A-Za-z0-9-]'); @@ -1235,7 +1235,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, value: bucketname[ix].charCodeAt(), pos: ix, character: bucketname[ix], - fieldname: gettextCatalog.getString('Bucket Name') + fieldname: gettextCatalog.getString('Bucket name') })); res = false; } diff --git a/Duplicati/Server/webroot/ngax/styles/dark.css b/Duplicati/Server/webroot/ngax/styles/dark.css index 094e57586..561dbc339 100644 --- a/Duplicati/Server/webroot/ngax/styles/dark.css +++ b/Duplicati/Server/webroot/ngax/styles/dark.css @@ -2815,6 +2815,7 @@ body .container .header .state .buttons button { } body .container .header .state .buttons .stop { background: url('../img/progress-stop.png'); + background-size: 100%; z-index: 10; position: relative; } @@ -2830,6 +2831,7 @@ body .container .header .state .buttons .stop { } body .container .header .state .buttons .resume { background: url('../img/progress-resume.png'); + background-size: 100%; } @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { body .container .header .state .buttons .resume { @@ -2877,6 +2879,7 @@ body .container .header .action-icons-small > button { body .container .header .action-icons > .pause, body .container .header .action-icons-small > .pause { background: url('../img/pause.png'); + background-size: 100%; } @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { body .container .header .action-icons > .pause, @@ -2893,6 +2896,7 @@ body .container .header .action-icons-small > .pause { body .container .header .action-icons > .pause.active, body .container .header .action-icons-small > .pause.active { background: url('../img/resume.png'); + background-size: 100%; } @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { body .container .header .action-icons > .pause.active, @@ -2909,6 +2913,7 @@ body .container .header .action-icons-small > .pause.active { body .container .header .action-icons > .throttle, body .container .header .action-icons-small > .throttle { background: url('../img/throttle.png'); + background-size: 100%; } @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { body .container .header .action-icons > .throttle, diff --git a/Duplicati/Server/webroot/ngax/styles/default.css b/Duplicati/Server/webroot/ngax/styles/default.css index 01e60dcc7..d7d6cefc8 100644 --- a/Duplicati/Server/webroot/ngax/styles/default.css +++ b/Duplicati/Server/webroot/ngax/styles/default.css @@ -2815,6 +2815,7 @@ body .container .header .state .buttons button { } body .container .header .state .buttons .stop { background: url('../img/progress-stop.png'); + background-size: 100%; z-index: 10; position: relative; } @@ -2830,6 +2831,7 @@ body .container .header .state .buttons .stop { } body .container .header .state .buttons .resume { background: url('../img/progress-resume.png'); + background-size: 100%; } @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { body .container .header .state .buttons .resume { @@ -2877,6 +2879,7 @@ body .container .header .action-icons-small > button { body .container .header .action-icons > .pause, body .container .header .action-icons-small > .pause { background: url('../img/pause.png'); + background-size: 100%; } @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { body .container .header .action-icons > .pause, @@ -2893,6 +2896,7 @@ body .container .header .action-icons-small > .pause { body .container .header .action-icons > .pause.active, body .container .header .action-icons-small > .pause.active { background: url('../img/resume.png'); + background-size: 100%; } @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { body .container .header .action-icons > .pause.active, @@ -2909,6 +2913,7 @@ body .container .header .action-icons-small > .pause.active { body .container .header .action-icons > .throttle, body .container .header .action-icons-small > .throttle { background: url('../img/throttle.png'); + background-size: 100%; } @media only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 1.25dppx) { body .container .header .action-icons > .throttle, diff --git a/Duplicati/Server/webroot/ngax/templates/backends/aliyunoss.html b/Duplicati/Server/webroot/ngax/templates/backends/aliyunoss.html index 889b29727..4a67549e1 100644 --- a/Duplicati/Server/webroot/ngax/templates/backends/aliyunoss.html +++ b/Duplicati/Server/webroot/ngax/templates/backends/aliyunoss.html @@ -22,8 +22,8 @@
- - + +
diff --git a/Duplicati/Server/webroot/ngax/templates/backends/storj.html b/Duplicati/Server/webroot/ngax/templates/backends/storj.html index 22c2510f6..c9b53c408 100644 --- a/Duplicati/Server/webroot/ngax/templates/backends/storj.html +++ b/Duplicati/Server/webroot/ngax/templates/backends/storj.html @@ -34,10 +34,10 @@
- - + +
- +
diff --git a/Duplicati/WebserverCore/Services/CaptchaService.cs b/Duplicati/WebserverCore/Services/CaptchaService.cs index c3497d2e8..ba253f612 100644 --- a/Duplicati/WebserverCore/Services/CaptchaService.cs +++ b/Duplicati/WebserverCore/Services/CaptchaService.cs @@ -19,6 +19,15 @@ public class CaptchaService : ICaptchaProvider m_disableVisualCaptcha = settings.GetSettings().DisableVisualCaptcha; } + /// + /// List of possible system fonts, ordered by preference + /// + private static readonly Dictionary FontNamePreference = new string[] { + "Arial", "Verdana", "FreeSans", "Tahoma", "Helvetica", "Times New Roman", "Courier New", "Andale Mono" + } + .Select((x, i) => new { Key = x, Value = i }) + .ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase); + private class CaptchaEntry { public readonly string Answer; @@ -115,12 +124,10 @@ public class CaptchaService : ICaptchaProvider /// /// Approximate the size in pixels of text drawn at the given fontsize /// - private static int ApproxTextWidth(string text, string fontFamily, float fontSize) - { - var font = SystemFonts.CreateFont(fontFamily, fontSize); - var textSize = TextMeasurer.MeasureSize(text, new TextOptions(font) { KerningMode = KerningMode.Standard }); - return (int)textSize.Width; - } + /// The text to measure + /// The font to use + private static int ApproxTextWidth(string text, Font font) + => (int)TextMeasurer.MeasureSize(text, new TextOptions(font) { KerningMode = KerningMode.Standard }).Width; /// /// Creates a random answer. @@ -149,8 +156,18 @@ public class CaptchaService : ICaptchaProvider /// The size of the font used to create the captcha, in pixels. public static Image CreateCaptcha(string answer, Size size = default(Size), float fontsize = 40) { - var fontfamily = "Arial"; - var text_width = ApproxTextWidth(answer, fontfamily, fontsize); + var fontFamily = SystemFonts.Collection.Families.OrderBy(x => + { + if (FontNamePreference.TryGetValue(x.Name, out var val)) + return val; + return int.MaxValue; + }).FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(fontFamily.Name)) + throw new Exception("No usable font found"); + + var font = fontFamily.CreateFont(fontsize); + var text_width = ApproxTextWidth(answer, font); if (size.Width == 0 || size.Height == 0) size = new Size((int)(text_width * 1.2), (int)(fontsize * 1.2)); @@ -160,7 +177,6 @@ public class CaptchaService : ICaptchaProvider var stray_y = size.Height / 4; var ans_stray_x = (int)fontsize / 3; var ans_stray_y = size.Height / 6; - var font = SystemFonts.CreateFont(fontfamily, fontsize); image.Mutate(ctx => { @@ -169,7 +185,7 @@ public class CaptchaService : ICaptchaProvider // Apply a background string to make it hard to do OCR foreach (var color in new[] { Color.Yellow, Color.LightGreen, Color.GreenYellow }) { - var backgroundFont = SystemFonts.CreateFont(fontfamily, fontsize); + var backgroundFont = font; ctx.DrawText(CreateRandomAnswer(minlength: answer.Length, maxlength: answer.Length), backgroundFont, color, new PointF(rnd.Next(-stray_x, stray_x), rnd.Next(-stray_y, stray_y))); }