Skip to content

Instantly share code, notes, and snippets.

@AggressivelyMeows
Last active September 5, 2021 15:45
Show Gist options
  • Save AggressivelyMeows/f909a9e2448f7921093a44776ac57bf0 to your computer and use it in GitHub Desktop.
Save AggressivelyMeows/f909a9e2448f7921093a44776ac57bf0 to your computer and use it in GitHub Desktop.
A worker to upload and read files from a private B2 bucket.
// 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