Last active
September 5, 2021 15:45
-
-
Save AggressivelyMeows/f909a9e2448f7921093a44776ac57bf0 to your computer and use it in GitHub Desktop.
A worker to upload and read files from a private B2 bucket.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A simple guide on how to use this Worker. | |
// To upload to the private bucket, you can send a POST request to the URL you want the file to end up at. | |
// For example: worker-url.com/images/user-1/subfolder/hi.png | |
// Once done, you can test your file by going to: | |
// worker-url.com/images/user-1/subfolder/h1.png | |
// Super simple API to use, much better than dealing with B2 directly, in my opinion. | |
addEventListener('fetch', event => { | |
event.respondWith(handleRequest(event)) | |
}) | |
var b2 = { | |
id: 'B2-KEY-ID', | |
key: 'B2-KEY-SECRET', | |
bucket: 'bucket-name', | |
bucketID: 'id-of-your-bucket', | |
topSecretAPIkey: 'whoa' // The key *you* need to send with your request to upload data. | |
} | |
var authenticate = async () => { | |
// authenticate the current session with the B2 API. | |
// returns the response object for use with our Worker. | |
// See https://www.backblaze.com/b2/docs/b2_authorize_account.html | |
var headers = { | |
// B2 requires us to Base64 encode our id and key. | |
'Authorization': 'Basic ' + btoa(`${b2.id}:${b2.key}`) | |
} | |
return await fetch( | |
'https://api.backblazeb2.com/b2api/v2/b2_authorize_account', | |
{ | |
headers | |
} | |
).then(resp => resp.json()) | |
} | |
// Util functions | |
function hexToString(buffer) { | |
var hexCodes = []; | |
var view = new DataView(buffer); | |
for (var i = 0; i < view.byteLength; i += 4) { | |
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time) | |
var value = view.getUint32(i) | |
// toString(16) will give the hex representation of the number without padding | |
var stringValue = value.toString(16) | |
// We use concatenation and slice for padding | |
var padding = '00000000' | |
var paddedValue = (padding + stringValue).slice(-padding.length) | |
hexCodes.push(paddedValue); | |
} | |
// Join all the hex strings into one | |
return hexCodes.join(""); | |
} | |
async function extentionToMimetype(ext) { | |
// ext is the very end of the url with the . removed. | |
var database = await fetch('https://cdn.jsdelivr.net/gh/jshttp/mime-db@master/db.json').then(resp => resp.json()) | |
var type = '' | |
Object.keys(database).forEach(k => { | |
if (type) { return } | |
var mimetype = database[k] | |
if ('extensions' in mimetype) { | |
if (mimetype.extensions.includes(ext)) { | |
type = k | |
} | |
} | |
}) | |
return type | |
} | |
async function b2fetch(auth, action, options) { | |
var query = new URLSearchParams(options.query || {}).toString() | |
var body = options.body | |
var method = options.method || 'GET' | |
var headers = { | |
'Authorization': auth.authorizationToken, | |
...(options.headers || {}) | |
} | |
return await fetch( | |
auth['apiUrl'] + '/b2api/v2/' + action + query, | |
{ | |
body, | |
method, | |
headers | |
} | |
).then(resp => resp.json()) // Convert Response to JSON. | |
} | |
// Route based functions | |
async function getFileObject(request) { | |
// For future, you may want to add K/V support to cache the key. | |
// This operation takes anywhere from 50-500ms to complete. | |
var auth = await authenticate() | |
// path starts with / so we need to remember that! | |
var path = new URL(request.url).pathname | |
// combine our URL path and our download URL we got | |
// from the authentication process. | |
var url = `${auth.downloadUrl}/file/${b2.bucket}${path}` | |
// Ask the B2 API for the file object using our token we got | |
// from the authentication process. This will allow us to read | |
// the private file object. | |
var resp = await fetch( | |
url, | |
{ | |
headers: { | |
'Authorization': auth.authorizationToken | |
} | |
} | |
) | |
// Make our new response object mutatable. This allows us to update the headers. | |
var modifiedResp = new Response(resp.body, resp) | |
// In this example, we dont want to reveal B2's headers so we remove any | |
// header with "x-bz-" | |
var headers = Object.fromEntries(modifiedResp.headers.entries()) | |
Object.keys(headers).forEach(h => { | |
if (h.includes('x-bz-')) { | |
modifiedResp.headers.delete(h) | |
} | |
}) | |
var ext = path.split('.')[path.split('.').length - 1] | |
modifiedResp.headers.set('Content-Type', await extentionToMimetype(ext)) | |
// return our modified private B2 file object back to the user. | |
return modifiedResp | |
} | |
async function setFileObject(request) { | |
if (request.headers.get('Authorization') != 'heck') { | |
return new Response('{ "error": "You need to be authenicated to the API first." }', { status: 403 }) | |
} | |
// Uploads "put" a new object onto your B2 bucket. | |
// Uses the path as the file name. This will intercept any POST request. | |
// As with before, we need an application key to use the private API. | |
var auth = await authenticate() | |
// Parse request for the things we need. | |
var buffer = await request.arrayBuffer() | |
var key = new URL(request.url).pathname.substring(1) | |
// Custom code to handle the first part of the upload process. | |
var uploadUrl = await b2fetch( | |
auth, | |
'b2_get_upload_url', | |
{ | |
method: 'POST', | |
body: JSON.stringify({bucketId: b2.bucketID}) | |
} | |
) | |
// Set the headers needed for uploading our body. | |
var uploadHeaders = { | |
'Authorization': uploadUrl.authorizationToken, | |
'X-Bz-File-Name': key, | |
'Content-Type': request.headers.get('content-type') || 'b2/x-auto', // Let B2 choose the Content-Type | |
'Content-Length': buffer.byteLength, | |
'X-Bz-Content-Sha1': hexToString(await crypto.subtle.digest("SHA-1", buffer)) | |
} | |
// Store the response from B2 into an output Object. | |
var resp = await fetch( | |
uploadUrl['uploadUrl'], | |
{ | |
body: buffer, | |
headers: uploadHeaders, | |
method: 'POST', | |
} | |
).then(resp => resp.json()) | |
resp['sentHeaders'] = uploadHeaders | |
var output = resp | |
// Return what we got as well as what we sent just in case | |
// something goes wrong. | |
return new Response( | |
JSON.stringify(output), | |
{ headers: {'Content-Type': 'application/json'} } | |
) | |
} | |
async function handleRequest(evt) { | |
if (evt.request.method == 'POST') { | |
return await setFileObject(evt.request) | |
} | |
var cache = caches.default | |
// Check to see if we have alread y served this request. | |
var resp = await cache.match(evt.request) | |
if (!resp) { | |
// Run our B2 API handler, cache the response we get back. | |
var resp = await getFileObject(evt.request) | |
resp.headers.append("Cache-Control", "s-maxage=7200") | |
evt.waitUntil( | |
cache.put(evt.request, resp.clone()) | |
) | |
} | |
return resp | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment