- 登录B2创建储存桶→桶设定
- 鉴于CloudflareWorkers每天只有(其实很多)10万次免费请求,而且每次都重新请求时效性不高,所以我们可以人CloudflareCDN缓存下来,打开Backblaze,找到你刚刚创建的桶,点击桶设定,在桶信息里添加如下内容:
{"cache-control":"public, max-age=15552000"} - 设置文件的生命周期(如果不设置B2会把所有版本的文件都保存下来包括删除的文件)→生命周期的设置(在桶设定的下面)→只保留了最后版本的文件→点击更新桶
- 点击左侧的菜单栏→应用密钥→点击添加新的应用程序秘钥→请保存好keyID和applicationKey
- 登录CF,左侧主菜单选择计算(Workers)→Workers和Pages→创建→开始使用Workers→从 Hello World! 开始→开始使用→Worker 名称(可以随便填写用于区分项目)→部署
- 部署成功后点击编辑代码,将里面的代码删除干净,然后放入以下代码
-
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var encoder = new TextEncoder(); var HOST_SERVICES = { appstream2: "appstream", cloudhsmv2: "cloudhsm", email: "ses", marketplace: "aws-marketplace", mobile: "AWSMobileHubService", pinpoint: "mobiletargeting", queue: "sqs", "git-codecommit": "codecommit", "mturk-requester-sandbox": "mturk-requester", "personalize-runtime": "personalize" }; var UNSIGNABLE_HEADERS = /* @__PURE__ */ new Set([ "authorization", "content-type", "content-length", "user-agent", "presigned-expires", "expect", "x-amzn-trace-id", "range", "connection" ]); var AwsClient = class { static { __name(this, "AwsClient"); } constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) { if (accessKeyId == null) throw new TypeError("accessKeyId is a required option"); if (secretAccessKey == null) throw new TypeError("secretAccessKey is a required option"); this.accessKeyId = accessKeyId; this.secretAccessKey = secretAccessKey; this.sessionToken = sessionToken; this.service = service; this.region = region; this.cache = cache || /* @__PURE__ */ new Map(); this.retries = retries != null ? retries : 10; this.initRetryMs = initRetryMs || 50; } async sign(input, init) { if (input instanceof Request) { const { method, url, headers, body } = input; init = Object.assign({ method, url, headers }, init); if (init.body == null && headers.has("Content-Type")) { init.body = body != null && headers.has("X-Amz-Content-Sha256") ? body : await input.clone().arrayBuffer(); } input = url; } const signer = new AwsV4Signer(Object.assign({ url: input.toString() }, init, this, init && init.aws)); const signed = Object.assign({}, init, await signer.sign()); delete signed.aws; try { return new Request(signed.url.toString(), signed); } catch (e) { if (e instanceof TypeError) { return new Request(signed.url.toString(), Object.assign({ duplex: "half" }, signed)); } throw e; } } async fetch(input, init) { for (let i = 0; i <= this.retries; i++) { const fetched = fetch(await this.sign(input, init)); if (i === this.retries) { return fetched; } const res = await fetched; if (res.status < 500 && res.status !== 429) { return res; } await new Promise((resolve) => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); } throw new Error("An unknown error occurred, ensure retries is not negative"); } }; var AwsV4Signer = class { static { __name(this, "AwsV4Signer"); } constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { if (url == null) throw new TypeError("url is a required option"); if (accessKeyId == null) throw new TypeError("accessKeyId is a required option"); if (secretAccessKey == null) throw new TypeError("secretAccessKey is a required option"); this.method = method || (body ? "POST" : "GET"); this.url = new URL(url); this.headers = new Headers(headers || {}); this.body = body; this.accessKeyId = accessKeyId; this.secretAccessKey = secretAccessKey; this.sessionToken = sessionToken; let guessedService, guessedRegion; if (!service || !region) { [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); } this.service = service || guessedService || ""; this.region = region || guessedRegion || "us-east-1"; this.cache = cache || /* @__PURE__ */ new Map(); this.datetime = datetime || (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, ""); this.signQuery = signQuery; this.appendSessionToken = appendSessionToken || this.service === "iotdevicegateway"; this.headers.delete("Host"); if (this.service === "s3" && !this.signQuery && !this.headers.has("X-Amz-Content-Sha256")) { this.headers.set("X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD"); } const params = this.signQuery ? this.url.searchParams : this.headers; params.set("X-Amz-Date", this.datetime); if (this.sessionToken && !this.appendSessionToken) { params.set("X-Amz-Security-Token", this.sessionToken); } this.signableHeaders = ["host", ...this.headers.keys()].filter((header) => allHeaders || !UNSIGNABLE_HEADERS.has(header)).sort(); this.signedHeaders = this.signableHeaders.join(";"); this.canonicalHeaders = this.signableHeaders.map((header) => header + ":" + (header === "host" ? this.url.host : (this.headers.get(header) || "").replace(/\s+/g, " "))).join("\n"); this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, "aws4_request"].join("/"); if (this.signQuery) { if (this.service === "s3" && !params.has("X-Amz-Expires")) { params.set("X-Amz-Expires", "86400"); } params.set("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); params.set("X-Amz-Credential", this.accessKeyId + "/" + this.credentialString); params.set("X-Amz-SignedHeaders", this.signedHeaders); } if (this.service === "s3") { try { this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, " ")); } catch (e) { this.encodedPath = this.url.pathname; } } else { this.encodedPath = this.url.pathname.replace(/\/+/g, "/"); } if (!singleEncode) { this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, "/"); } this.encodedPath = encodeRfc3986(this.encodedPath); const seenKeys = /* @__PURE__ */ new Set(); this.encodedSearch = [...this.url.searchParams].filter(([k]) => { if (!k) return false; if (this.service === "s3") { if (seenKeys.has(k)) return false; seenKeys.add(k); } return true; }).map((pair) => pair.map((p) => encodeRfc3986(encodeURIComponent(p)))).sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0).map((pair) => pair.join("=")).join("&"); } async sign() { if (this.signQuery) { this.url.searchParams.set("X-Amz-Signature", await this.signature()); if (this.sessionToken && this.appendSessionToken) { this.url.searchParams.set("X-Amz-Security-Token", this.sessionToken); } } else { this.headers.set("Authorization", await this.authHeader()); } return { method: this.method, url: this.url, headers: this.headers, body: this.body }; } async authHeader() { return [ "AWS4-HMAC-SHA256 Credential=" + this.accessKeyId + "/" + this.credentialString, "SignedHeaders=" + this.signedHeaders, "Signature=" + await this.signature() ].join(", "); } async signature() { const date = this.datetime.slice(0, 8); const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); let kCredentials = this.cache.get(cacheKey); if (!kCredentials) { const kDate = await hmac("AWS4" + this.secretAccessKey, date); const kRegion = await hmac(kDate, this.region); const kService = await hmac(kRegion, this.service); kCredentials = await hmac(kService, "aws4_request"); this.cache.set(cacheKey, kCredentials); } return buf2hex(await hmac(kCredentials, await this.stringToSign())); } async stringToSign() { return [ "AWS4-HMAC-SHA256", this.datetime, this.credentialString, buf2hex(await hash(await this.canonicalString())) ].join("\n"); } async canonicalString() { return [ this.method.toUpperCase(), this.encodedPath, this.encodedSearch, this.canonicalHeaders + "\n", this.signedHeaders, await this.hexBodyHash() ].join("\n"); } async hexBodyHash() { let hashHeader = this.headers.get("X-Amz-Content-Sha256") || (this.service === "s3" && this.signQuery ? "UNSIGNED-PAYLOAD" : null); if (hashHeader == null) { if (this.body && typeof this.body !== "string" && !("byteLength" in this.body)) { throw new Error("body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header"); } hashHeader = buf2hex(await hash(this.body || "")); } return hashHeader; } }; async function hmac(key, string) { const cryptoKey = await crypto.subtle.importKey( "raw", typeof key === "string" ? encoder.encode(key) : key, { name: "HMAC", hash: { name: "SHA-256" } }, false, ["sign"] ); return crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(string)); } __name(hmac, "hmac"); async function hash(content) { return crypto.subtle.digest("SHA-256", typeof content === "string" ? encoder.encode(content) : content); } __name(hash, "hash"); var HEX_CHARS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"]; function buf2hex(arrayBuffer) { const buffer = new Uint8Array(arrayBuffer); let out = ""; for (let idx = 0; idx < buffer.length; idx++) { const n = buffer[idx]; out += HEX_CHARS[n >>> 4 & 15]; out += HEX_CHARS[n & 15]; } return out; } __name(buf2hex, "buf2hex"); function encodeRfc3986(urlEncodedStr) { return urlEncodedStr.replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()); } __name(encodeRfc3986, "encodeRfc3986"); function guessServiceRegion(url, headers) { const { hostname, pathname } = url; if (hostname.endsWith(".on.aws")) { const match2 = hostname.match(/^[^.]{1,63}\.lambda-url\.([^.]{1,63})\.on\.aws$/); return match2 != null ? ["lambda", match2[1] || ""] : ["", ""]; } if (hostname.endsWith(".r2.cloudflarestorage.com")) { return ["s3", "auto"]; } if (hostname.endsWith(".backblazeb2.com")) { const match2 = hostname.match(/^(?:[^.]{1,63}\.)?s3\.([^.]{1,63})\.backblazeb2\.com$/); return match2 != null ? ["s3", match2[1] || ""] : ["", ""]; } const match = hostname.replace("dualstack.", "").match(/([^.]{1,63})\.(?:([^.]{0,63})\.)?amazonaws\.com(?:\.cn)?$/); let service = match && match[1] || ""; let region = match && match[2]; if (region === "us-gov") { region = "us-gov-west-1"; } else if (region === "s3" || region === "s3-accelerate") { region = "us-east-1"; service = "s3"; } else if (service === "iot") { if (hostname.startsWith("iot.")) { service = "execute-api"; } else if (hostname.startsWith("data.jobs.iot.")) { service = "iot-jobs-data"; } else { service = pathname === "/mqtt" ? "iotdevicegateway" : "iotdata"; } } else if (service === "autoscaling") { const targetPrefix = (headers.get("X-Amz-Target") || "").split(".")[0]; if (targetPrefix === "AnyScaleFrontendService") { service = "application-autoscaling"; } else if (targetPrefix === "AnyScaleScalingPlannerFrontendService") { service = "autoscaling-plans"; } } else if (region == null && service.startsWith("s3-")) { region = service.slice(3).replace(/^fips-|^external-1/, ""); service = "s3"; } else if (service.endsWith("-fips")) { service = service.slice(0, -5); } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { [service, region] = [region, service]; } return [HOST_SERVICES[service] || service, region || ""]; } __name(guessServiceRegion, "guessServiceRegion"); // index.js var UNSIGNABLE_HEADERS2 = [ "x-forwarded-proto", "x-real-ip", "accept-encoding", "if-match", "if-modified-since", "if-none-match", "if-range", "if-unmodified-since" ]; var HTTPS_PROTOCOL = "https:"; var HTTPS_PORT = "443"; function filterHeaders(headers, env) { return new Headers( Array.from(headers.entries()).filter((pair) => !(UNSIGNABLE_HEADERS2.includes(pair[0]) || pair[0].startsWith("cf-") || "ALLOWED_HEADERS" in env && !env["ALLOWED_HEADERS"].includes(pair[0]))) ); } __name(filterHeaders, "filterHeaders"); function createHeadResponse(response) { return new Response(null, { headers: response.headers, status: response.status, statusText: response.statusText }); } __name(createHeadResponse, "createHeadResponse"); var index_default = { async fetch(request, env) { if (!["GET", "HEAD"].includes(request.method)) { return new Response(null, { status: 405, statusText: "Method Not Allowed" }); } const url = new URL(request.url); url.protocol = HTTPS_PROTOCOL; url.port = HTTPS_PORT; let path = url.pathname.replace(/^\//, "").replace(/\/$/, ""); const isRootRequest = path === ""; if (isRootRequest || url.pathname.startsWith("/api/")) { const objectPath = isRootRequest ? "" : url.pathname.replace(/^\/api\//, ""); url.pathname = objectPath; url.hostname = `${env["BUCKET_NAME"]}.${env["B2_ENDPOINT"]}`; const headers = filterHeaders(request.headers, env); const client = new AwsClient({ "accessKeyId": env["B2_APPLICATION_KEY_ID"], "secretAccessKey": env["B2_APPLICATION_KEY"], "service": "s3" }); const signedRequest = await client.sign(url.toString(), { method: request.method, headers }); const response = await fetch(signedRequest); if (request.method === "HEAD") { return createHeadResponse(response); } return response; } return new Response("Not Found", { status: 404 }); } }; export { index_default as default }; - 点击部署→部署完成后→在设置-变量与机密里设置变量名,依次填入刚刚创建的只读密钥Key、存储桶名称,存储桶访问Endpoint是一个域名,在你的桶信息里(keyID和key不要弄反了)
-
纯文本 ALLOW_LIST_BUCKET true 纯文本 B2_APPLICATION_KEY 你的Key 纯文本 B2_APPLICATION_KEY_ID 你的KeyID 纯文本 B2_ENDPOINT 你的访问ENDPOINT 纯文本 BUCKET_NAME 存储桶名称 - 全部添加好后你可以打开Workers自带的域名后面加上/api/,例如:
cloudflare-b2.abab.workers.dev/api/
如果出现了如下内容就说明成功了! - 设置自定义域→CF主页,左侧主菜单选择计算(Workers)→Workers和Pages→选择你刚刚创建的项目→设置→点击(域和路由)→添加→输入自定义域→点击添加域即可
- 如果不想要后面的/api/请进行如下操作→CF主页,左侧主菜单选择计算(Workers)→Workers和Pages→选择你刚刚创建的项目→部署→点击右上角的编辑代码进入代码页面→键盘CTRL+F搜索api
-
if (isRootRequest || url.pathname.startsWith("/api/")) { const objectPath = isRootRequest ? "" : url.pathname.replace(/^\/api\//, ""); - 把上面的代码换成下面的
-
if (isRootRequest || url.pathname.startsWith("")) { const objectPath = isRootRequest ? "" : url.pathname.replace(/^\/\//, ""); - 也可以直接删掉api或者换成自己喜欢的后缀(注意斜杠)
- 大功告成!!!
THE END
