Backblaze B2 support

Adds an uploader for Backblaze B2 Cloud Storage.
This commit is contained in:
tinybarks 2018-09-27 07:49:42 +02:00
parent 35a3cc0dd9
commit e0fb9373b8
No known key found for this signature in database
GPG key ID: D999045C35D5795A
11 changed files with 9914 additions and 1992 deletions

View file

@ -101,6 +101,8 @@ public enum FileDestination
GoogleCloudStorage,
[Description("Azure Storage")]
AzureStorage,
[Description("Backblaze B2")]
BackblazeB2,
[Description("Gfycat")]
Gfycat,
[Description("ownCloud / Nextcloud")]

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View file

@ -0,0 +1,616 @@
#region License Information (GPL v3)
/*
ShareX - A program that allows you to take screenshots and share any file type
Copyright (c) 2007-2018 ShareX Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Optionally you can also view the license at <http://www.gnu.org/licenses/>.
*/
#endregion License Information (GPL v3)
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ShareX.HelpersLib;
using ShareX.UploadersLib.Properties;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
namespace ShareX.UploadersLib.FileUploaders
{
/// <summary>
/// A <see cref="FileUploaderService"/> implementation for the Backblaze B2 Cloud Storage API.
/// </summary>
public class BackblazeB2UploaderService : FileUploaderService
{
public override FileDestination EnumValue => FileDestination.BackblazeB2;
public override Icon ServiceIcon => Resources.BackblazeB2;
public override bool CheckConfig(UploadersConfig config)
{
return
!string.IsNullOrWhiteSpace(config.B2ApplicationKeyId) &&
!string.IsNullOrWhiteSpace(config.B2ApplicationKey) &&
!string.IsNullOrWhiteSpace(config.B2BucketName);
}
public override GenericUploader CreateUploader(UploadersConfig config, TaskReferenceHelper taskInfo)
{
return new BackblazeB2(
applicationKeyId: config.B2ApplicationKeyId,
applicationKey: config.B2ApplicationKey,
bucketName: config.B2BucketName,
uploadPath: config.B2UploadPath,
useCustomUrl: config.B2UseCustomUrl,
customUrl: config.B2CustomUrl
);
}
}
/// <summary>
/// An <see cref="ImageUploader"/> implementation for the Backblaze B2 Cloud Storage API.
/// </summary>
[Localizable(false)]
public sealed class BackblazeB2 : ImageUploader
{
private const string B2AuthorizeAccountUrl = "https://api.backblazeb2.com/b2api/v1/b2_authorize_account";
// after we authorize, we'll get an api url that we need to prepend here
private const string B2GetUploadUrlPath = "/b2api/v1/b2_get_upload_url";
private const string B2ListBucketsPath = "/b2api/v1/b2_list_buckets";
private const string ApplicationJson = "application/json; charset=utf-8";
public string ApplicationKeyId { get; }
public string ApplicationKey { get; }
public string BucketName { get; }
public string UploadPath { get; }
public bool UseCustomUrl { get; }
public string CustomUrl { get; }
public BackblazeB2(string applicationKeyId, string applicationKey, string bucketName, string uploadPath, bool useCustomUrl, string customUrl)
{
ApplicationKeyId = applicationKeyId;
ApplicationKey = applicationKey;
BucketName = bucketName;
UploadPath = uploadPath;
UseCustomUrl = useCustomUrl;
CustomUrl = customUrl;
}
public override UploadResult Upload(Stream stream, string fileName)
{
var parsedUploadPath = NameParser.Parse(NameParserType.FolderPath, UploadPath);
var destinationPath = parsedUploadPath + fileName;
// docs: https://www.backblaze.com/b2/docs/
// STEP 1: authorize, get auth token, api url, download url
DebugHelper.WriteLine($"B2 uploader: Attempting to authorize as '{ApplicationKeyId}'.");
var (authError, auth) = B2ApiAuthorize(ApplicationKeyId, ApplicationKey);
if (authError != null)
{
DebugHelper.WriteLine("B2 uploader: Failed to authorize.");
Errors.Add("Could not authenticate with B2: " + authError);
return null;
}
DebugHelper.WriteLine($"B2 uploader: Authorized, using API server {auth.apiUrl}, download URL {auth.downloadUrl}");
// STEP 1.25: if we have an application key, there will be a bucketId present here, but if
// not, we have an account key and need to find our bucket id ourselves
var bucketId = auth.allowed?.bucketId;
if (bucketId == null)
{
DebugHelper.WriteLine("B2 uploader: This doesn't look like an app key, so I'm looking for a bucket ID.");
var (getBucketError, newBucketId) = B2ApiGetBucketId(auth, BucketName);
if (getBucketError != null)
{
DebugHelper.WriteLine($"B2 uploader: It's {newBucketId}.");
bucketId = newBucketId;
}
}
// STEP 1.5: verify whether we can write to the bucket user wants to write to, with the given prefix
DebugHelper.WriteLine("B2 uploader: Checking clientside whether we have permission to upload.");
var (authCheckError, authCheckOk) = IsAuthorizedForUpload(auth, bucketId, destinationPath);
if (!authCheckOk)
{
DebugHelper.WriteLine("B2 uploader: Key is not suitable for this upload.");
Errors.Add("B2 upload failed: " + authCheckError);
return null;
}
// STEP 1.75: start upload attempt loop
const int maxTries = 5;
B2UploadUrl url = null;
for (var tries = 1; tries <= maxTries; tries++)
{
var newOrSameUrl = (url == null) ? "New URL." : "Same URL.";
DebugHelper.WriteLine($"B2 uploader: Upload attempt {tries} of {maxTries}. {newOrSameUrl}");
// Sloppy
if (tries > 1)
{
var delay = (int)Math.Pow(2, tries - 1) * 1000;
DebugHelper.WriteLine($"Waiting ${delay} ms for backoff.");
Thread.Sleep(delay);
}
// STEP 2: get upload url that we need to POST to in step 3
if (url == null)
{
DebugHelper.WriteLine("B2 uploader: Getting new upload URL.");
string getUrlError;
(getUrlError, url) = B2ApiGetUploadUrl(auth, bucketId);
if (getUrlError != null)
{
// this is guaranteed to be unrecoverable, so bail out
DebugHelper.WriteLine("B2 uploader: Got error trying to get upload URL.");
Errors.Add("Could not get B2 upload URL: " + getUrlError);
return null;
}
}
// STEP 3: upload file and see if anything went wrong
DebugHelper.WriteLine($"B2 uploader: Uploading to URL {url.uploadUrl}");
var (status, uploadError, upload) = B2ApiUploadFile(url, destinationPath, stream);
var expiredTokenCodes = new HashSet<string>(new List<string> { "expired_auth_token", "bad_auth_token" });
if (status == -1)
{
// magic number for "connection failed", should also happen when upload
// caps are exceeded
DebugHelper.WriteLine("B2 uploader: Connection failed, trying with new URL.");
url = null;
continue;
}
else if (status == 401 && expiredTokenCodes.Contains(uploadError.code))
{
// Unauthorized, our token expired
DebugHelper.WriteLine("B2 uploader: Upload auth token expired, trying with new URL.");
url = null;
continue;
}
else if (status == 408)
{
DebugHelper.WriteLine("B2 uploader: Request Timeout, trying with same URL.");
continue;
}
else if (status == 429)
{
DebugHelper.WriteLine("B2 uploader: Too Many Requests, trying with same URL.");
continue;
}
else if (status != 200)
{
// something else happened that wasn't a success, so bail out
DebugHelper.WriteLine("B2 uploader: Unknown error, upload failure.");
Errors.Add("B2 uploader: Unknown error occurred while calling b2_upload_file().");
return null;
}
// success!
// STEP 4: compose:
// the download url (e.g. "https://f700.backblazeb2.com")
// /file/$bucket/$uploadPath
// or
// $customUrl/$uploadPath
var remoteLocation = auth.downloadUrl +
"/file/" +
URLHelpers.URLEncode(BucketName) +
"/" +
upload.fileName;
DebugHelper.WriteLine($"B2 uploader: Successful upload! File should be at: {remoteLocation}");
if (UseCustomUrl)
{
var parsedCustomUrl = NameParser.Parse(NameParserType.FolderPath, CustomUrl);
remoteLocation = parsedCustomUrl + upload.fileName;
DebugHelper.WriteLine($"B2 uploader: But user requested custom URL, which will be: {remoteLocation}");
}
return new UploadResult()
{
IsSuccess = true,
URL = remoteLocation
};
}
DebugHelper.WriteLine($"B2 uploader: Ran out of attempts, aborting.");
Errors.Add($"B2 upload failed: Could not upload file after {maxTries} attempts.");
return null;
}
/// <summary>
/// Attempts to authorize against the B2 API with the given key.
/// </summary>
/// <param name="keyId">The application key ID <b>or</b> account ID.</param>
/// <param name="key">The application key <b>or</b> account master key.</param>
/// <returns>A tuple with either an error set, or a <see cref="B2Authorization"/>.</returns>
private (string error, B2Authorization res) B2ApiAuthorize(string keyId, string key)
{
var base64Key = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{keyId}:{key}"));
var headers = new NameValueCollection() { ["Authorization"] = $"Basic {base64Key}" };
using (var res = GetResponse(HttpMethod.GET, B2AuthorizeAccountUrl, headers: headers, allowNon2xxResponses: true))
{
if (res.StatusCode != HttpStatusCode.OK)
{
return (StringifyB2Error(res), null);
}
var body = new StreamReader(res.GetResponseStream(), Encoding.UTF8).ReadToEnd();
return (null, JsonConvert.DeserializeObject<B2Authorization>(body));
}
}
/// <summary>
/// Gets the bucket ID for the given bucket name. Requires <c>listBuckets</c> permission.
/// </summary>
/// <param name="auth">The B2 API authorization.</param>
/// <param name="bucketName">The bucket to get the ID for.</param>
/// <returns><c>(null, "bucket id")</c> on success, <c>("error message", null)</c> on failure.</returns>
private (string error, string id) B2ApiGetBucketId(B2Authorization auth, string bucketName)
{
var headers = new NameValueCollection()
{
["Authorization"] = auth.authorizationToken
};
var reqBody = new Dictionary<string, string> { ["bucketName"] = bucketName };
using (var data = CreateJsonBody(reqBody))
{
using (var res = GetResponse(HttpMethod.POST, auth.apiUrl + B2ListBucketsPath, contentType: ApplicationJson, headers: headers, data: data, allowNon2xxResponses: true))
{
if (res.StatusCode != HttpStatusCode.OK)
{
return (StringifyB2Error(res), null);
}
var body = new StreamReader(res.GetResponseStream(), Encoding.UTF8).ReadToEnd();
JObject json;
try
{
json = JObject.Parse(body);
}
catch (JsonException e)
{
DebugHelper.WriteLine($"B2 uploader: Could not parse b2_list_buckets response: {e}");
return ("B2 upload failed: Couldn't parse b2_list_buckets response.", null);
}
var bucketId = json
.SelectToken("buckets")
?.FirstOrDefault(b => b["bucketName"].ToString() == bucketName)
?.SelectToken("bucketId")?.ToString() ?? "";
if (bucketId != "")
{
return (null, bucketId);
}
return ($"B2 upload failed: Couldn't find bucket {bucketName}.", null);
}
}
}
/// <summary>
/// Gets a <see cref="B2UploadUrl"/> for the given bucket. Requires <c>writeFile</c> permission.
/// </summary>
/// <param name="auth">The B2 API authorization.</param>
/// <param name="bucketId">The bucket ID to get an upload URL for.</param>
/// <returns><c>(null, B2UploadUrl)</c> on success, <c>("error message", null)</c> on failure.</returns>
private (string error, B2UploadUrl url) B2ApiGetUploadUrl(B2Authorization auth, string bucketId)
{
var headers = new NameValueCollection() { ["Authorization"] = auth.authorizationToken };
var reqBody = new Dictionary<string, string> { ["bucketId"] = bucketId };
using (var data = CreateJsonBody(reqBody))
{
using (var res = GetResponse(HttpMethod.POST, auth.apiUrl + B2GetUploadUrlPath, contentType: ApplicationJson, headers: headers, data: data, allowNon2xxResponses: true))
{
if (res.StatusCode != HttpStatusCode.OK)
{
return (StringifyB2Error(res), null);
}
var body = new StreamReader(res.GetResponseStream(), Encoding.UTF8).ReadToEnd();
return (null, JsonConvert.DeserializeObject<B2UploadUrl>(body));
}
}
}
/// <summary>
/// Given a <see cref="B2UploadUrl"/> returned from the API, attempts to upload a file.
/// </summary>
/// <param name="b2UploadUrl">Information returned by the <c>b2_get_upload_url</c> API.</param>
/// <param name="destinationPath">The remote path to upload to.</param>
/// <param name="file">The file to upload.</param>
/// <returns>
/// <ul>
/// <li><b>If successful:</b> <c>(200, null, B2Upload)</c></li>
/// <li><b>If unsuccessful:</b> <c>(HTTP status, B2Error, null)</c></li>
/// <li><b>If the connection failed:</b> <c>(-1, null, null)</c></li>
/// </ul>
/// </returns>
private (int rc, B2Error error, B2Upload upload) B2ApiUploadFile(B2UploadUrl b2UploadUrl, string destinationPath, Stream file)
{
// we want to send 'Content-Disposition: inline; filename="screenshot.png"'
// this should display the uploaded data inline if possible, but if that fails, present a sensible filename
// conveniently, this class will handle this for us
var contentDisposition = new ContentDisposition("inline") { FileName = URLHelpers.GetFileName(destinationPath) };
DebugHelper.WriteLine($"B2 uploader: Content disposition is '{contentDisposition}'.");
// compute SHA1 hash without loading the file fully into memory
string sha1Hash;
using (var cryptoProvider = new SHA1CryptoServiceProvider())
{
file.Seek(0, SeekOrigin.Begin);
var bytes = cryptoProvider.ComputeHash(file);
sha1Hash = BitConverter.ToString(bytes).Replace("-", "").ToLower();
file.Seek(0, SeekOrigin.Begin);
}
DebugHelper.WriteLine($"B2 uploader: SHA1 hash is '{sha1Hash}'.");
// it's showtime
// https://www.backblaze.com/b2/docs/b2_upload_file.html
var headers = new NameValueCollection()
{
["Authorization"] = b2UploadUrl.authorizationToken,
["X-Bz-File-Name"] = URLHelpers.URLEncode(destinationPath),
["Content-Length"] = file.Length.ToString(),
["X-Bz-Content-Sha1"] = sha1Hash,
["X-Bz-Info-src_last_modified_millis"] = DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString(),
["X-Bz-Info-b2-content-disposition"] = URLHelpers.URLEncode(contentDisposition.ToString()),
};
var contentType = Helpers.GetMimeType(destinationPath);
using (var res = GetResponse(HttpMethod.POST, b2UploadUrl.uploadUrl, contentType: contentType, headers: headers, data: file, allowNon2xxResponses: true))
{
// if connection failed, res will be null, and here we -do- want to check explicitly for this
// since the server might be down
if (res == null)
{
return (-1, null, null);
}
if (res.StatusCode != HttpStatusCode.OK)
{
return ((int)res.StatusCode, ParseB2Error(res), null);
}
var body = new StreamReader(res.GetResponseStream(), Encoding.UTF8).ReadToEnd();
DebugHelper.WriteLine($"B2 uploader: B2ApiUploadFile() reports success! '{body}'");
return ((int)res.StatusCode, null, JsonConvert.DeserializeObject<B2Upload>(body));
}
}
/// <summary>
/// Checks whether the authorization allows uploading to the specific bucket and path.
/// </summary>
/// <param name="auth">The authorization response.</param>
/// <param name="bucket">The bucket to upload to.</param>
/// <param name="destinationPath">The path of the file that will be uploaded.</param>
/// <returns><c>("error message", false)</c> on failure, <c>("", true)</c> on success.</returns>
private static (string error, bool success) IsAuthorizedForUpload(B2Authorization auth, string bucketId, string destinationPath)
{
var allowedBucketId = auth.allowed?.bucketId;
if (allowedBucketId != null && bucketId != allowedBucketId)
{
DebugHelper.WriteLine($"B2 uploader: Error, user is only allowed to access '{allowedBucketId}', " +
$"but user is trying to access '{bucketId}'.");
return ($"No permission to upload to this bucket. Are you using the right application key?", false);
}
var allowedPrefix = auth?.allowed?.namePrefix;
if (allowedPrefix != null && !destinationPath.StartsWith(allowedPrefix))
{
DebugHelper.WriteLine($"B2 uploader: Error, key is restricted to prefix '{allowedPrefix}'.");
return ("Your upload path conflicts with the key's name prefix setting.", false);
}
var caps = auth.allowed?.capabilities;
if (caps != null && !caps.Contains("writeFiles"))
{
DebugHelper.WriteLine($"B2 uploader: No permission to write to '{bucketId}'.");
return ("Your key does not allow uploading to this bucket.", false);
}
return (null, true);
}
/// <summary>
/// Tries to parse a <see cref="B2Error"/> from the given response.
/// </summary>
/// <param name="res">The response that contains an error.</param>
/// <returns>
/// The parse result, or null if the response is successful or cannot be parsed.
/// </returns>
/// <exception cref="IOException">If the response body cannot be read.</exception>
private static B2Error ParseB2Error(HttpWebResponse res)
{
if (Helpers.IsSuccessfulResponse(res)) return null;
try
{
var body = new StreamReader(res.GetResponseStream(), Encoding.UTF8).ReadToEnd();
DebugHelper.WriteLine($"B2 uploader: ParseB2Error() got: {body}");
var err = JsonConvert.DeserializeObject<B2Error>(body);
return err;
}
catch (JsonException)
{
return null;
}
}
/// <summary>
/// Creates a user facing error message from a failed B2 request.
/// </summary>
/// <param name="res">A <see cref="HttpWebResponse"/> with a non-2xx status code.</param>
/// <returns>A string describing the error.</returns>
private static string StringifyB2Error(HttpWebResponse res)
{
var err = ParseB2Error(res);
if (err == null)
{
return $"Status {res.StatusCode}, unknown error.";
}
var colonSpace = string.IsNullOrWhiteSpace(err.message) ? "" : ": ";
return $"Got status {err.status} ({err.code}){colonSpace}{err.message}";
}
/// <summary>
/// Takes key-value pairs and returns a Stream of data that should be sent as body for a request with
/// <c>Content-Type: application/json; charset=utf-8</c>.
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private static Stream CreateJsonBody(Dictionary<string, string> args)
{
var body = JsonConvert.SerializeObject(args);
return new MemoryStream(Encoding.UTF8.GetBytes(body));
}
/// <summary>
/// The b2_authorize_account API's optional 'allowed' field.
/// </summary>
private class B2Allowed
{
public B2Allowed(List<string> capabilities, string bucketId, string namePrefix)
{
this.capabilities = capabilities;
this.bucketId = bucketId;
this.namePrefix = namePrefix;
}
public List<string> capabilities { get; }
public string bucketId { get; } // may be null!
public string namePrefix { get; } // may be null!
}
/// <summary>
/// A parsed JSON response from the b2_authorize_account API.
/// </summary>
private class B2Authorization
{
public B2Authorization(string accountId, string apiUrl, string authorizationToken, string downloadUrl, int minimumPartSize, B2Allowed allowed)
{
this.accountId = accountId;
this.apiUrl = apiUrl;
this.authorizationToken = authorizationToken;
this.downloadUrl = downloadUrl;
this.minimumPartSize = minimumPartSize;
this.allowed = allowed;
}
public string accountId { get; }
public string apiUrl { get; }
public string authorizationToken { get; }
public string downloadUrl { get; }
public int minimumPartSize { get; }
public B2Allowed allowed { get; } // optional
}
/// <summary>
/// A parsed JSON response from failed B2 API calls, describing the error.
/// </summary>
private class B2Error
{
public B2Error(int status, string code, string message)
{
this.status = status;
this.code = code;
this.message = message;
}
public int status { get; }
public string code { get; }
public string message { get; }
}
/// <summary>
/// A parsed JSON response from the b2_get_upload_url API.
/// </summary>
private class B2UploadUrl
{
public B2UploadUrl(string bucketId, string uploadUrl, string authorizationToken)
{
this.bucketId = bucketId;
this.uploadUrl = uploadUrl;
this.authorizationToken = authorizationToken;
}
public string bucketId { get; }
public string uploadUrl { get; }
public string authorizationToken { get; }
}
/// <summary>
/// A parsed JSON response from the b2_upload_file API.
/// </summary>
private class B2Upload
{
public B2Upload(string fileId, string fileName, string accountId, string bucketId,
int contentLength, string contentSha1, string contentType, Dictionary<string, string> fileInfo)
{
this.fileId = fileId;
this.fileName = fileName;
this.accountId = accountId;
this.bucketId = bucketId;
this.contentLength = contentLength;
this.contentSha1 = contentSha1;
this.contentType = contentType;
this.fileInfo = fileInfo;
}
public string fileId { get; }
public string fileName { get; }
public string accountId { get; }
public string bucketId { get; }
public int contentLength { get; }
public string contentSha1 { get; }
public string contentType { get; }
public Dictionary<string, string> fileInfo { get; }
}
}
}

View file

@ -35,6 +35,12 @@ private void InitializeComponent()
this.cbAmazonS3CustomCNAME = new System.Windows.Forms.CheckBox();
this.mbCustomUploaderDestinationType = new ShareX.HelpersLib.MenuButton();
this.cmsCustomUploaderDestinationType = new System.Windows.Forms.ContextMenuStrip(this.components);
this.txtB2CustomUrl = new System.Windows.Forms.TextBox();
this.cbB2CustomUrl = new System.Windows.Forms.CheckBox();
this.txtB2Bucket = new System.Windows.Forms.TextBox();
this.txtB2UploadPath = new System.Windows.Forms.TextBox();
this.txtB2ApplicationKey = new System.Windows.Forms.TextBox();
this.txtB2ApplicationKeyId = new System.Windows.Forms.TextBox();
this.tpOtherUploaders = new System.Windows.Forms.TabPage();
this.tcOtherUploaders = new System.Windows.Forms.TabControl();
this.tpTwitter = new System.Windows.Forms.TabPage();
@ -324,6 +330,14 @@ private void InitializeComponent()
this.lblAzureStorageAccountName = new System.Windows.Forms.Label();
this.txtAzureStorageCustomDomain = new System.Windows.Forms.TextBox();
this.lblAzureStorageCustomDomain = new System.Windows.Forms.Label();
this.tbBackblazeB2 = new System.Windows.Forms.TabPage();
this.lblB2ManageLink = new System.Windows.Forms.LinkLabel();
this.txtB2UrlPreview = new System.Windows.Forms.TextBox();
this.lblB2UrlPreview = new System.Windows.Forms.Label();
this.lblB2Bucket = new System.Windows.Forms.Label();
this.lblB2UploadPath = new System.Windows.Forms.Label();
this.lblB2ApplicationKey = new System.Windows.Forms.Label();
this.lblB2ApplicationKeyId = new System.Windows.Forms.Label();
this.tpGfycat = new System.Windows.Forms.TabPage();
this.cbGfycatIsPublic = new System.Windows.Forms.CheckBox();
this.atcGfycatAccountType = new ShareX.UploadersLib.AccountTypeControl();
@ -689,6 +703,7 @@ private void InitializeComponent()
this.gbAmazonS3Advanced.SuspendLayout();
this.tpGoogleCloudStorage.SuspendLayout();
this.tpAzureStorage.SuspendLayout();
this.tbBackblazeB2.SuspendLayout();
this.tpGfycat.SuspendLayout();
this.tpMega.SuspendLayout();
this.tpOwnCloud.SuspendLayout();
@ -780,6 +795,50 @@ private void InitializeComponent()
this.cmsCustomUploaderDestinationType.Name = "cmsCustomUploaderDestinationType";
resources.ApplyResources(this.cmsCustomUploaderDestinationType, "cmsCustomUploaderDestinationType");
//
// txtB2CustomUrl
//
resources.ApplyResources(this.txtB2CustomUrl, "txtB2CustomUrl");
this.txtB2CustomUrl.Name = "txtB2CustomUrl";
this.ttHelpTip.SetToolTip(this.txtB2CustomUrl, resources.GetString("txtB2CustomUrl.ToolTip"));
this.txtB2CustomUrl.TextChanged += new System.EventHandler(this.txtB2CustomUrl_TextChanged);
//
// cbB2CustomUrl
//
resources.ApplyResources(this.cbB2CustomUrl, "cbB2CustomUrl");
this.cbB2CustomUrl.Name = "cbB2CustomUrl";
this.ttHelpTip.SetToolTip(this.cbB2CustomUrl, resources.GetString("cbB2CustomUrl.ToolTip"));
this.cbB2CustomUrl.UseVisualStyleBackColor = true;
this.cbB2CustomUrl.CheckedChanged += new System.EventHandler(this.cbB2CustomUrl_CheckedChanged);
//
// txtB2Bucket
//
resources.ApplyResources(this.txtB2Bucket, "txtB2Bucket");
this.txtB2Bucket.Name = "txtB2Bucket";
this.ttHelpTip.SetToolTip(this.txtB2Bucket, resources.GetString("txtB2Bucket.ToolTip"));
this.txtB2Bucket.TextChanged += new System.EventHandler(this.txtB2Bucket_TextChanged);
//
// txtB2UploadPath
//
resources.ApplyResources(this.txtB2UploadPath, "txtB2UploadPath");
this.txtB2UploadPath.Name = "txtB2UploadPath";
this.ttHelpTip.SetToolTip(this.txtB2UploadPath, resources.GetString("txtB2UploadPath.ToolTip"));
this.txtB2UploadPath.TextChanged += new System.EventHandler(this.txtB2UploadPath_TextChanged);
//
// txtB2ApplicationKey
//
resources.ApplyResources(this.txtB2ApplicationKey, "txtB2ApplicationKey");
this.txtB2ApplicationKey.Name = "txtB2ApplicationKey";
this.ttHelpTip.SetToolTip(this.txtB2ApplicationKey, resources.GetString("txtB2ApplicationKey.ToolTip"));
this.txtB2ApplicationKey.UseSystemPasswordChar = true;
this.txtB2ApplicationKey.TextChanged += new System.EventHandler(this.txtB2ApplicationKey_TextChanged);
//
// txtB2ApplicationKeyId
//
resources.ApplyResources(this.txtB2ApplicationKeyId, "txtB2ApplicationKeyId");
this.txtB2ApplicationKeyId.Name = "txtB2ApplicationKeyId";
this.ttHelpTip.SetToolTip(this.txtB2ApplicationKeyId, resources.GetString("txtB2ApplicationKeyId.ToolTip"));
this.txtB2ApplicationKeyId.TextChanged += new System.EventHandler(this.txtB2ApplicationKeyId_TextChanged);
//
// tpOtherUploaders
//
this.tpOtherUploaders.BackColor = System.Drawing.SystemColors.Window;
@ -1807,6 +1866,7 @@ private void InitializeComponent()
this.tcFileUploaders.Controls.Add(this.tpAmazonS3);
this.tcFileUploaders.Controls.Add(this.tpGoogleCloudStorage);
this.tcFileUploaders.Controls.Add(this.tpAzureStorage);
this.tcFileUploaders.Controls.Add(this.tbBackblazeB2);
this.tcFileUploaders.Controls.Add(this.tpGfycat);
this.tcFileUploaders.Controls.Add(this.tpMega);
this.tcFileUploaders.Controls.Add(this.tpOwnCloud);
@ -2931,6 +2991,64 @@ private void InitializeComponent()
resources.ApplyResources(this.lblAzureStorageCustomDomain, "lblAzureStorageCustomDomain");
this.lblAzureStorageCustomDomain.Name = "lblAzureStorageCustomDomain";
//
// tbBackblazeB2
//
this.tbBackblazeB2.BackColor = System.Drawing.SystemColors.Window;
this.tbBackblazeB2.Controls.Add(this.lblB2ManageLink);
this.tbBackblazeB2.Controls.Add(this.txtB2UrlPreview);
this.tbBackblazeB2.Controls.Add(this.txtB2CustomUrl);
this.tbBackblazeB2.Controls.Add(this.lblB2UrlPreview);
this.tbBackblazeB2.Controls.Add(this.cbB2CustomUrl);
this.tbBackblazeB2.Controls.Add(this.lblB2Bucket);
this.tbBackblazeB2.Controls.Add(this.txtB2Bucket);
this.tbBackblazeB2.Controls.Add(this.txtB2UploadPath);
this.tbBackblazeB2.Controls.Add(this.lblB2UploadPath);
this.tbBackblazeB2.Controls.Add(this.txtB2ApplicationKey);
this.tbBackblazeB2.Controls.Add(this.lblB2ApplicationKey);
this.tbBackblazeB2.Controls.Add(this.lblB2ApplicationKeyId);
this.tbBackblazeB2.Controls.Add(this.txtB2ApplicationKeyId);
resources.ApplyResources(this.tbBackblazeB2, "tbBackblazeB2");
this.tbBackblazeB2.Name = "tbBackblazeB2";
//
// lblB2ManageLink
//
resources.ApplyResources(this.lblB2ManageLink, "lblB2ManageLink");
this.lblB2ManageLink.Name = "lblB2ManageLink";
this.lblB2ManageLink.TabStop = true;
this.lblB2ManageLink.VisitedLinkColor = System.Drawing.Color.Blue;
this.lblB2ManageLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.lblB2ManageLink_LinkClicked);
//
// txtB2UrlPreview
//
resources.ApplyResources(this.txtB2UrlPreview, "txtB2UrlPreview");
this.txtB2UrlPreview.Name = "txtB2UrlPreview";
this.txtB2UrlPreview.ReadOnly = true;
//
// lblB2UrlPreview
//
resources.ApplyResources(this.lblB2UrlPreview, "lblB2UrlPreview");
this.lblB2UrlPreview.Name = "lblB2UrlPreview";
//
// lblB2Bucket
//
resources.ApplyResources(this.lblB2Bucket, "lblB2Bucket");
this.lblB2Bucket.Name = "lblB2Bucket";
//
// lblB2UploadPath
//
resources.ApplyResources(this.lblB2UploadPath, "lblB2UploadPath");
this.lblB2UploadPath.Name = "lblB2UploadPath";
//
// lblB2ApplicationKey
//
resources.ApplyResources(this.lblB2ApplicationKey, "lblB2ApplicationKey");
this.lblB2ApplicationKey.Name = "lblB2ApplicationKey";
//
// lblB2ApplicationKeyId
//
resources.ApplyResources(this.lblB2ApplicationKeyId, "lblB2ApplicationKeyId");
this.lblB2ApplicationKeyId.Name = "lblB2ApplicationKeyId";
//
// tpGfycat
//
this.tpGfycat.BackColor = System.Drawing.SystemColors.Window;
@ -5378,6 +5496,8 @@ private void InitializeComponent()
this.tpGoogleCloudStorage.PerformLayout();
this.tpAzureStorage.ResumeLayout(false);
this.tpAzureStorage.PerformLayout();
this.tbBackblazeB2.ResumeLayout(false);
this.tbBackblazeB2.PerformLayout();
this.tpGfycat.ResumeLayout(false);
this.tpGfycat.PerformLayout();
this.tpMega.ResumeLayout(false);
@ -6097,5 +6217,19 @@ private void InitializeComponent()
private System.Windows.Forms.Label lblAzureStorageURLPreview;
private System.Windows.Forms.Label lblAzureStorageURLPreviewLabel;
private System.Windows.Forms.Label lblFirebaseDomainExample;
internal System.Windows.Forms.TabPage tbBackblazeB2;
private System.Windows.Forms.TextBox txtB2CustomUrl;
private System.Windows.Forms.Label lblB2UrlPreview;
private System.Windows.Forms.CheckBox cbB2CustomUrl;
private System.Windows.Forms.Label lblB2Bucket;
private System.Windows.Forms.TextBox txtB2Bucket;
private System.Windows.Forms.TextBox txtB2UploadPath;
private System.Windows.Forms.Label lblB2UploadPath;
private System.Windows.Forms.TextBox txtB2ApplicationKey;
private System.Windows.Forms.Label lblB2ApplicationKey;
private System.Windows.Forms.Label lblB2ApplicationKeyId;
private System.Windows.Forms.TextBox txtB2ApplicationKeyId;
private System.Windows.Forms.TextBox txtB2UrlPreview;
private System.Windows.Forms.LinkLabel lblB2ManageLink;
}
}

View file

@ -673,6 +673,19 @@ public void LoadSettings()
#endregion Azure Storage
#region Backblaze B2
txtB2ApplicationKeyId.Text = Config.B2ApplicationKeyId;
txtB2ApplicationKey.Text = Config.B2ApplicationKey;
txtB2Bucket.Text = Config.B2BucketName;
txtB2UploadPath.Text = Config.B2UploadPath;
cbB2CustomUrl.Checked = Config.B2UseCustomUrl;
txtB2CustomUrl.ReadOnly = !cbB2CustomUrl.Checked;
txtB2CustomUrl.Text = Config.B2CustomUrl;
B2UpdateCustomDomainPreview();
#endregion Backblaze B2
#region Plik
txtPlikAPIKey.Text = Config.PlikSettings.APIKey;
@ -2876,6 +2889,50 @@ private void txtAzureStorageCustomDomain_TextChanged(object sender, EventArgs e)
#endregion Azure Storage
#region Backblaze B2
private void txtB2ApplicationKeyId_TextChanged(object sender, EventArgs e)
{
Config.B2ApplicationKeyId = txtB2ApplicationKeyId.Text.Trim();
}
private void txtB2ApplicationKey_TextChanged(object sender, EventArgs e)
{
Config.B2ApplicationKey = txtB2ApplicationKey.Text.Trim();
}
private void cbB2CustomUrl_CheckedChanged(object sender, EventArgs e)
{
txtB2CustomUrl.ReadOnly = !cbB2CustomUrl.Checked;
Config.B2UseCustomUrl = cbB2CustomUrl.Checked;
B2UpdateCustomDomainPreview();
}
private void txtB2CustomUrl_TextChanged(object sender, EventArgs e)
{
Config.B2CustomUrl = txtB2CustomUrl.Text.Trim();
B2UpdateCustomDomainPreview();
}
private void lblB2ManageLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
URLHelpers.OpenURL("https://secure.backblaze.com/b2_buckets.htm");
}
private void txtB2UploadPath_TextChanged(object sender, EventArgs e)
{
Config.B2UploadPath = txtB2UploadPath.Text.Trim();
B2UpdateCustomDomainPreview();
}
private void txtB2Bucket_TextChanged(object sender, EventArgs e)
{
Config.B2BucketName = txtB2Bucket.Text.Trim();
B2UpdateCustomDomainPreview();
}
#endregion Backblaze B2
#region Plik
private void txtPlikURL_TextChanged(object sender, EventArgs e)

File diff suppressed because it is too large Load diff

View file

@ -281,6 +281,34 @@ private void UpdateAzureStorageStatus()
#endregion Azure Storage
#region Backblaze B2
private void B2UpdateCustomDomainPreview()
{
var uploadPath = NameParser.Parse(NameParserType.FolderPath, Config.B2UploadPath);
if (cbB2CustomUrl.Checked)
{
var customUrl = NameParser.Parse(NameParserType.FolderPath, Config.B2CustomUrl);
if (URLHelpers.IsValidURL(customUrl))
{
txtB2UrlPreview.Text = customUrl + uploadPath + "example.png";
}
else
{
txtB2UrlPreview.Text = "invalid custom URL";
}
}
else
{
var bucket = URLHelpers.URLEncode(Config.B2BucketName);
var url = $"https://f001.backblazeb2.com/file/{bucket}/{uploadPath}example.png";
txtB2UrlPreview.Text = url;
}
}
#endregion Backblaze B2
#region Google Drive
private void GoogleDriveRefreshFolders()

View file

@ -90,6 +90,16 @@ internal class Resources {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
internal static System.Drawing.Icon BackblazeB2 {
get {
object obj = ResourceManager.GetObject("BackblazeB2", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>

View file

@ -407,4 +407,7 @@ Created folders:</value>
<data name="GoogleCloud" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Favicons\GoogleCloud.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="BackblazeB2" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Favicons\backblazeb2.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

View file

@ -129,6 +129,7 @@
<Compile Include="FileUploaders\AmazonS3Endpoint.cs" />
<Compile Include="FileUploaders\AmazonS3Settings.cs" />
<Compile Include="FileUploaders\AzureStorage.cs" />
<Compile Include="FileUploaders\BackblazeB2.cs" />
<Compile Include="FileUploaders\Box.cs" />
<Compile Include="FileUploaders\GoogleCloudStorage.cs" />
<Compile Include="FileUploaders\Lithiio.cs" />
@ -1011,6 +1012,12 @@
<PackageReference Include="SSH.NET">
<Version>2016.1.0</Version>
</PackageReference>
<PackageReference Include="System.ValueTuple">
<Version>4.5.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Content Include="Favicons\BackblazeB2.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>

View file

@ -376,6 +376,17 @@ public class UploadersConfig : SettingsBase<UploadersConfig>
#endregion Azure Storage
#region Backblaze B2
public string B2ApplicationKeyId = "";
public string B2ApplicationKey = "";
public string B2BucketName = "mybucket";
public string B2UploadPath = "ShareX/%y/%mo/";
public bool B2UseCustomUrl = false;
public string B2CustomUrl = "https://example.com/";
#endregion Backblaze B2
#region Plik
public PlikSettings PlikSettings = new PlikSettings();