Cloudflare R2 作为一款出色的对象存储服务,以其慷慨的免费额度和低廉的价格(前提是合理设置数据访问频率,不然就和我一样被账单吓到哭泣)赢得了广泛青睐。然而,其简陋的图形界面却给日常管理带来了诸多不便,如无法直接移动文件、缺乏缩略图预览等功能。为了解决这些痛点,GitHub 上应运而生了 R2 Uploader 项目,它不仅弥补了 R2 的不足,还带来了一系列实用功能:
-
大文件上传:突破 R2 dashboard 300MB 的上传限制,R2 Uploader 理论上可支持高达 100GB 的单文件上传。
-
图像压缩:针对将 R2 用作 CDN 的用户,内置图像压缩功能显著提升了图片加载速度。
-
多数据桶快速切换:支持为不同数据桶设置多个 Worker,并实现它们之间的便捷切换。
-
跨设备安全同步:通过 GitHub 登录,实现配置的安全同步。所有数据在传输到数据库前都经过本地 AES 加密处理。
在实际使用过程中,我发现 R2 Uploader 还能与 WebP Cloud 完美配合,进一步增强了其实用性。
Table of contents
Open Table of contents
配置 R2 Uploader Worker
-
前往 Cloudflare 控制面板。
-
左侧面板上有一个名为 “Workers 和 Pages” 的部分,点击它,进入到「概述」页面,再点击「创建」按钮。
-
然后点击「创建 Worker」按钮。
-
在名称部分,可以输入喜欢的名称,也可以忽略,直接点击「部署」按钮。
-
之后点击「编辑代码」按钮,删除掉代码编辑器中的内容。
-
在代码编辑器内,粘贴以下代码(源码可以在这里找到,也可以据此自行构建代码),然后点击「部署」按钮。
点击查看代码
var te = async (e, t = Object.create(null)) => {
let { all: s = !1, dot: r = !1 } = t,
o = (e instanceof S ? e.raw.headers : e.headers).get("Content-Type");
return (o !== null && o.startsWith("multipart/form-data")) ||
(o !== null && o.startsWith("application/x-www-form-urlencoded"))
? Pe(e, { all: s, dot: r })
: {};
};
async function Pe(e, t) {
let s = await e.formData();
return s ? Ae(s, t) : {};
}
function Ae(e, t) {
let s = Object.create(null);
return (
e.forEach((r, n) => {
t.all || n.endsWith("[]") ? He(s, n, r) : (s[n] = r);
}),
t.dot &&
Object.entries(s).forEach(([r, n]) => {
r.includes(".") && (Te(s, r, n), delete s[r]);
}),
s
);
}
var He = (e, t, s) => {
e[t] !== void 0
? Array.isArray(e[t])
? e[t].push(s)
: (e[t] = [e[t], s])
: (e[t] = s);
},
Te = (e, t, s) => {
let r = e,
n = t.split(".");
n.forEach((o, c) => {
c === n.length - 1
? (r[o] = s)
: ((!r[o] ||
typeof r[o] != "object" ||
Array.isArray(r[o]) ||
r[o] instanceof File) &&
(r[o] = Object.create(null)),
(r = r[o]));
});
};
var D = e => {
let t = e.split("/");
return t[0] === "" && t.shift(), t;
},
re = e => {
let { groups: t, path: s } = je(e),
r = D(s);
return Se(r, t);
},
je = e => {
let t = [];
return (
(e = e.replace(/\{[^}]+\}/g, (s, r) => {
let n = `@${r}`;
return t.push([n, s]), n;
})),
{ groups: t, path: e }
);
},
Se = (e, t) => {
for (let s = t.length - 1; s >= 0; s--) {
let [r] = t[s];
for (let n = e.length - 1; n >= 0; n--)
if (e[n].includes(r)) {
e[n] = e[n].replace(r, t[s][1]);
break;
}
}
return e;
},
_ = {},
$ = e => {
if (e === "*") return "*";
let t = e.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
return t
? (_[e] ||
(t[2]
? (_[e] = [e, t[1], new RegExp("^" + t[2] + "$")])
: (_[e] = [e, t[1], !0])),
_[e])
: null;
},
_e = e => {
try {
return decodeURI(e);
} catch {
return e.replace(/(?:%[0-9A-Fa-f]{2})+/g, t => {
try {
return decodeURI(t);
} catch {
return t;
}
});
}
},
K = e => {
let t = e.url,
s = t.indexOf("/", 8),
r = s;
for (; r < t.length; r++) {
let n = t.charCodeAt(r);
if (n === 37) {
let o = t.indexOf("?", r),
c = t.slice(s, o === -1 ? void 0 : o);
return _e(c.includes("%25") ? c.replace(/%25/g, "%2525") : c);
} else if (n === 63) break;
}
return t.slice(s, r);
};
var se = e => {
let t = K(e);
return t.length > 1 && t[t.length - 1] === "/" ? t.slice(0, -1) : t;
},
b = (...e) => {
let t = "",
s = !1;
for (let r of e)
t[t.length - 1] === "/" && ((t = t.slice(0, -1)), (s = !0)),
r[0] !== "/" && (r = `/${r}`),
r === "/" && s ? (t = `${t}/`) : r !== "/" && (t = `${t}${r}`),
r === "/" && t === "" && (t = "/");
return t;
},
k = e => {
if (!e.match(/\:.+\?$/)) return null;
let t = e.split("/"),
s = [],
r = "";
return (
t.forEach(n => {
if (n !== "" && !/\:/.test(n)) r += "/" + n;
else if (/\:/.test(n))
if (/\?/.test(n)) {
s.length === 0 && r === "" ? s.push("/") : s.push(r);
let o = n.replace("?", "");
(r += "/" + o), s.push(r);
} else r += "/" + n;
}),
s.filter((n, o, c) => c.indexOf(n) === o)
);
},
L = e =>
/[%+]/.test(e)
? (e.indexOf("+") !== -1 && (e = e.replace(/\+/g, " ")),
/%/.test(e) ? M(e) : e)
: e,
ne = (e, t, s) => {
let r;
if (!s && t && !/[%+]/.test(t)) {
let c = e.indexOf(`?${t}`, 8);
for (c === -1 && (c = e.indexOf(`&${t}`, 8)); c !== -1; ) {
let a = e.charCodeAt(c + t.length + 1);
if (a === 61) {
let i = c + t.length + 2,
l = e.indexOf("&", i);
return L(e.slice(i, l === -1 ? void 0 : l));
} else if (a == 38 || isNaN(a)) return "";
c = e.indexOf(`&${t}`, c + 1);
}
if (((r = /[%+]/.test(e)), !r)) return;
}
let n = {};
r ??= /[%+]/.test(e);
let o = e.indexOf("?", 8);
for (; o !== -1; ) {
let c = e.indexOf("&", o + 1),
a = e.indexOf("=", o);
a > c && c !== -1 && (a = -1);
let i = e.slice(o + 1, a === -1 ? (c === -1 ? void 0 : c) : a);
if ((r && (i = L(i)), (o = c), i === "")) continue;
let l;
a === -1
? (l = "")
: ((l = e.slice(a + 1, c === -1 ? void 0 : c)), r && (l = L(l))),
s
? ((n[i] && Array.isArray(n[i])) || (n[i] = []), n[i].push(l))
: (n[i] ??= l);
}
return t ? n[t] : n;
},
oe = ne,
ie = (e, t) => ne(e, t, !0),
M = decodeURIComponent;
var S = class {
raw;
#r;
#s;
routeIndex = 0;
path;
bodyCache = {};
constructor(e, t = "/", s = [[]]) {
(this.raw = e), (this.path = t), (this.#s = s), (this.#r = {});
}
param(e) {
return e ? this.getDecodedParam(e) : this.getAllDecodedParams();
}
getDecodedParam(e) {
let t = this.#s[0][this.routeIndex][1][e],
s = this.getParamValue(t);
return s ? (/\%/.test(s) ? M(s) : s) : void 0;
}
getAllDecodedParams() {
let e = {},
t = Object.keys(this.#s[0][this.routeIndex][1]);
for (let s of t) {
let r = this.getParamValue(this.#s[0][this.routeIndex][1][s]);
r && typeof r == "string" && (e[s] = /\%/.test(r) ? M(r) : r);
}
return e;
}
getParamValue(e) {
return this.#s[1] ? this.#s[1][e] : e;
}
query(e) {
return oe(this.url, e);
}
queries(e) {
return ie(this.url, e);
}
header(e) {
if (e) return this.raw.headers.get(e.toLowerCase()) ?? void 0;
let t = {};
return (
this.raw.headers.forEach((s, r) => {
t[r] = s;
}),
t
);
}
async parseBody(e) {
return (this.bodyCache.parsedBody ??= await te(this, e));
}
cachedBody = e => {
let { bodyCache: t, raw: s } = this,
r = t[e];
if (r) return r;
let n = Object.keys(t)[0];
return n
? t[n].then(
o => (n === "json" && (o = JSON.stringify(o)), new Response(o)[e]())
)
: (t[e] = s[e]());
};
json() {
return this.cachedBody("json");
}
text() {
return this.cachedBody("text");
}
arrayBuffer() {
return this.cachedBody("arrayBuffer");
}
blob() {
return this.cachedBody("blob");
}
formData() {
return this.cachedBody("formData");
}
addValidatedData(e, t) {
this.#r[e] = t;
}
valid(e) {
return this.#r[e];
}
get url() {
return this.raw.url;
}
get method() {
return this.raw.method;
}
get matchedRoutes() {
return this.#s[0].map(([[, e]]) => e);
}
get routePath() {
return this.#s[0].map(([[, e]]) => e)[this.routeIndex].path;
}
};
var ae = { Stringify: 1, BeforeStream: 2, Stream: 3 },
ke = (e, t) => {
let s = new String(e);
return (s.isEscaped = !0), (s.callbacks = t), s;
};
var F = async (e, t, s, r, n) => {
let o = e.callbacks;
if (!o?.length) return Promise.resolve(e);
n ? (n[0] += e) : (n = [e]);
let c = Promise.all(o.map(a => a({ phase: t, buffer: n, context: r }))).then(
a =>
Promise.all(a.filter(Boolean).map(i => F(i, t, !1, r, n))).then(
() => n[0]
)
);
return s ? ke(await c, o) : c;
};
var Me = "text/plain; charset=UTF-8",
V = (e, t = {}) => (Object.entries(t).forEach(([s, r]) => e.set(s, r)), e),
v = class {
#r;
#s;
env = {};
#a;
finalized = !1;
error;
#c = 200;
#o;
#e;
#t;
#n;
#i = !0;
#u;
#l;
#h;
#d;
#f;
constructor(e, t) {
(this.#r = e),
t &&
((this.#o = t.executionCtx),
(this.env = t.env),
(this.#h = t.notFoundHandler),
(this.#f = t.path),
(this.#d = t.matchResult));
}
get req() {
return (this.#s ??= new S(this.#r, this.#f, this.#d)), this.#s;
}
get event() {
if (this.#o && "respondWith" in this.#o) return this.#o;
throw Error("This context has no FetchEvent");
}
get executionCtx() {
if (this.#o) return this.#o;
throw Error("This context has no ExecutionContext");
}
get res() {
return (
(this.#i = !1),
(this.#n ||= new Response("404 Not Found", { status: 404 }))
);
}
set res(e) {
if (((this.#i = !1), this.#n && e)) {
this.#n.headers.delete("content-type");
for (let [t, s] of this.#n.headers.entries())
if (t === "set-cookie") {
let r = this.#n.headers.getSetCookie();
e.headers.delete("set-cookie");
for (let n of r) e.headers.append("set-cookie", n);
} else e.headers.set(t, s);
}
(this.#n = e), (this.finalized = !0);
}
render = (...e) => ((this.#l ??= t => this.html(t)), this.#l(...e));
setLayout = e => (this.#u = e);
getLayout = () => this.#u;
setRenderer = e => {
this.#l = e;
};
header = (e, t, s) => {
if (t === void 0) {
this.#e
? this.#e.delete(e)
: this.#t && delete this.#t[e.toLocaleLowerCase()],
this.finalized && this.res.headers.delete(e);
return;
}
s?.append
? (this.#e ||
((this.#i = !1), (this.#e = new Headers(this.#t)), (this.#t = {})),
this.#e.append(e, t))
: this.#e
? this.#e.set(e, t)
: ((this.#t ??= {}), (this.#t[e.toLowerCase()] = t)),
this.finalized &&
(s?.append
? this.res.headers.append(e, t)
: this.res.headers.set(e, t));
};
status = e => {
(this.#i = !1), (this.#c = e);
};
set = (e, t) => {
(this.#a ??= {}), (this.#a[e] = t);
};
get = e => (this.#a ? this.#a[e] : void 0);
get var() {
return { ...this.#a };
}
newResponse = (e, t, s) => {
if (this.#i && !s && !t && this.#c === 200)
return new Response(e, { headers: this.#t });
if (t && typeof t != "number") {
let n = new Headers(t.headers);
this.#e &&
this.#e.forEach((c, a) => {
a === "set-cookie" ? n.append(a, c) : n.set(a, c);
});
let o = V(n, this.#t);
return new Response(e, { headers: o, status: t.status ?? this.#c });
}
let r = typeof t == "number" ? t : this.#c;
(this.#t ??= {}),
(this.#e ??= new Headers()),
V(this.#e, this.#t),
this.#n &&
(this.#n.headers.forEach((n, o) => {
o === "set-cookie" ? this.#e?.append(o, n) : this.#e?.set(o, n);
}),
V(this.#e, this.#t)),
(s ??= {});
for (let [n, o] of Object.entries(s))
if (typeof o == "string") this.#e.set(n, o);
else {
this.#e.delete(n);
for (let c of o) this.#e.append(n, c);
}
return new Response(e, { status: r, headers: this.#e });
};
body = (e, t, s) =>
typeof t == "number" ? this.newResponse(e, t, s) : this.newResponse(e, t);
text = (e, t, s) => {
if (!this.#t) {
if (this.#i && !s && !t) return new Response(e);
this.#t = {};
}
return (
(this.#t["content-type"] = Me),
typeof t == "number"
? this.newResponse(e, t, s)
: this.newResponse(e, t)
);
};
json = (e, t, s) => {
let r = JSON.stringify(e);
return (
(this.#t ??= {}),
(this.#t["content-type"] = "application/json; charset=UTF-8"),
typeof t == "number"
? this.newResponse(r, t, s)
: this.newResponse(r, t)
);
};
html = (e, t, s) => (
(this.#t ??= {}),
(this.#t["content-type"] = "text/html; charset=UTF-8"),
typeof e == "object" &&
(e instanceof Promise || (e = e.toString()), e instanceof Promise)
? e
.then(r => F(r, ae.Stringify, !1, {}))
.then(r =>
typeof t == "number"
? this.newResponse(r, t, s)
: this.newResponse(r, t)
)
: typeof t == "number"
? this.newResponse(e, t, s)
: this.newResponse(e, t)
);
redirect = (e, t) => (
(this.#e ??= new Headers()),
this.#e.set("Location", e),
this.newResponse(null, t ?? 302)
);
notFound = () => ((this.#h ??= () => new Response()), this.#h(this));
};
var W = (e, t, s) => (r, n) => {
let o = -1;
return c(0);
async function c(a) {
if (a <= o) throw new Error("next() called multiple times");
o = a;
let i,
l = !1,
h;
if (
(e[a]
? ((h = e[a][0][0]), r instanceof v && (r.req.routeIndex = a))
: (h = (a === e.length && n) || void 0),
!h)
)
r instanceof v && r.finalized === !1 && s && (i = await s(r));
else
try {
i = await h(r, () => c(a + 1));
} catch (u) {
if (u instanceof Error && r instanceof v && t)
(r.error = u), (i = await t(u, r)), (l = !0);
else throw u;
}
return i && (r.finalized === !1 || l) && (r.res = i), r;
}
};
var d = "ALL",
ce = "all",
le = ["get", "post", "put", "delete", "options", "patch"],
B = "Can not add a route since the matcher is already built.",
I = class extends Error {};
var Be = Symbol("composedHandler"),
Ie = e => e.text("404 Not Found", 404),
he = (e, t) =>
"getResponse" in e
? e.getResponse()
: (console.error(e), t.text("Internal Server Error", 500)),
G = class {
get;
post;
put;
delete;
options;
patch;
all;
on;
use;
router;
getPath;
_basePath = "/";
#r = "/";
routes = [];
constructor(e = {}) {
[...le, ce].forEach(r => {
this[r] = (n, ...o) => (
typeof n == "string" ? (this.#r = n) : this.addRoute(r, this.#r, n),
o.forEach(c => {
typeof c != "string" && this.addRoute(r, this.#r, c);
}),
this
);
}),
(this.on = (r, n, ...o) => {
for (let c of [n].flat()) {
this.#r = c;
for (let a of [r].flat())
o.map(i => {
this.addRoute(a.toUpperCase(), this.#r, i);
});
}
return this;
}),
(this.use = (r, ...n) => (
typeof r == "string"
? (this.#r = r)
: ((this.#r = "*"), n.unshift(r)),
n.forEach(o => {
this.addRoute(d, this.#r, o);
}),
this
));
let s = e.strict ?? !0;
delete e.strict,
Object.assign(this, e),
(this.getPath = s ? (e.getPath ?? K) : se);
}
clone() {
let e = new G({ router: this.router, getPath: this.getPath });
return (e.routes = this.routes), e;
}
notFoundHandler = Ie;
errorHandler = he;
route(e, t) {
let s = this.basePath(e);
return (
t.routes.map(r => {
let n;
t.errorHandler === he
? (n = r.handler)
: ((n = async (o, c) =>
(await W([], t.errorHandler)(o, () => r.handler(o, c))).res),
(n[Be] = r.handler)),
s.addRoute(r.method, r.path, n);
}),
this
);
}
basePath(e) {
let t = this.clone();
return (t._basePath = b(this._basePath, e)), t;
}
onError = e => ((this.errorHandler = e), this);
notFound = e => ((this.notFoundHandler = e), this);
mount(e, t, s) {
let r, n;
s &&
(typeof s == "function"
? (n = s)
: ((n = s.optionHandler), (r = s.replaceRequest)));
let o = n
? a => {
let i = n(a);
return Array.isArray(i) ? i : [i];
}
: a => {
let i;
try {
i = a.executionCtx;
} catch {}
return [a.env, i];
};
r ||= (() => {
let a = b(this._basePath, e),
i = a === "/" ? 0 : a.length;
return l => {
let h = new URL(l.url);
return (h.pathname = h.pathname.slice(i) || "/"), new Request(h, l);
};
})();
let c = async (a, i) => {
let l = await t(r(a.req.raw), ...o(a));
if (l) return l;
await i();
};
return this.addRoute(d, b(e, "*"), c), this;
}
addRoute(e, t, s) {
(e = e.toUpperCase()), (t = b(this._basePath, t));
let r = { path: t, method: e, handler: s };
this.router.add(e, t, [s, r]), this.routes.push(r);
}
matchRoute(e, t) {
return this.router.match(e, t);
}
handleError(e, t) {
if (e instanceof Error) return this.errorHandler(e, t);
throw e;
}
dispatch(e, t, s, r) {
if (r === "HEAD")
return (async () =>
new Response(null, await this.dispatch(e, t, s, "GET")))();
let n = this.getPath(e, { env: s }),
o = this.matchRoute(r, n),
c = new v(e, {
path: n,
matchResult: o,
env: s,
executionCtx: t,
notFoundHandler: this.notFoundHandler,
});
if (o[0].length === 1) {
let i;
try {
i = o[0][0][0][0](c, async () => {
c.res = await this.notFoundHandler(c);
});
} catch (l) {
return this.handleError(l, c);
}
return i instanceof Promise
? i
.then(l => l || (c.finalized ? c.res : this.notFoundHandler(c)))
.catch(l => this.handleError(l, c))
: (i ?? this.notFoundHandler(c));
}
let a = W(o[0], this.errorHandler, this.notFoundHandler);
return (async () => {
try {
let i = await a(c);
if (!i.finalized)
throw new Error(
"Context is not finalized. Did you forget to return a Response object or `await next()`?"
);
return i.res;
} catch (i) {
return this.handleError(i, c);
}
})();
}
fetch = (e, ...t) => this.dispatch(e, t[1], t[0], e.method);
request = (e, t, s, r) => {
if (e instanceof Request)
return t !== void 0 && (e = new Request(e, t)), this.fetch(e, s, r);
e = e.toString();
let n = /^https?:\/\//.test(e) ? e : `http://localhost${b("/", e)}`,
o = new Request(n, t);
return this.fetch(o, s, r);
};
fire = () => {
addEventListener("fetch", e => {
e.respondWith(this.dispatch(e.request, e, void 0, e.request.method));
});
};
};
var U = "[^/]+",
H = ".*",
T = "(?:|/.*)",
C = Symbol(),
Ue = new Set(".\\+*[^]$()");
function qe(e, t) {
return e.length === 1
? t.length === 1
? e < t
? -1
: 1
: -1
: t.length === 1 || e === H || e === T
? 1
: t === H || t === T
? -1
: e === U
? 1
: t === U
? -1
: e.length === t.length
? e < t
? -1
: 1
: t.length - e.length;
}
var q = class {
index;
varIndex;
children = Object.create(null);
insert(e, t, s, r, n) {
if (e.length === 0) {
if (this.index !== void 0) throw C;
if (n) return;
this.index = t;
return;
}
let [o, ...c] = e,
a =
o === "*"
? c.length === 0
? ["", "", H]
: ["", "", U]
: o === "/*"
? ["", "", T]
: o.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/),
i;
if (a) {
let l = a[1],
h = a[2] || U;
if (
l &&
a[2] &&
((h = h.replace(/^\((?!\?:)(?=[^)]+\)$)/, "(?:")), /\((?!\?:)/.test(h))
)
throw C;
if (((i = this.children[h]), !i)) {
if (Object.keys(this.children).some(u => u !== H && u !== T)) throw C;
if (n) return;
(i = this.children[h] = new q()),
l !== "" && (i.varIndex = r.varIndex++);
}
!n && l !== "" && s.push([l, i.varIndex]);
} else if (((i = this.children[o]), !i)) {
if (
Object.keys(this.children).some(l => l.length > 1 && l !== H && l !== T)
)
throw C;
if (n) return;
i = this.children[o] = new q();
}
i.insert(c, t, s, r, n);
}
buildRegExpStr() {
let t = Object.keys(this.children)
.sort(qe)
.map(s => {
let r = this.children[s];
return (
(typeof r.varIndex == "number"
? `(${s})@${r.varIndex}`
: Ue.has(s)
? `\\${s}`
: s) + r.buildRegExpStr()
);
});
return (
typeof this.index == "number" && t.unshift(`#${this.index}`),
t.length === 0 ? "" : t.length === 1 ? t[0] : "(?:" + t.join("|") + ")"
);
}
};
var ue = class {
context = { varIndex: 0 };
root = new q();
insert(e, t, s) {
let r = [],
n = [];
for (let c = 0; ; ) {
let a = !1;
if (
((e = e.replace(/\{[^}]+\}/g, i => {
let l = `@\\${c}`;
return (n[c] = [l, i]), c++, (a = !0), l;
})),
!a)
)
break;
}
let o = e.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
for (let c = n.length - 1; c >= 0; c--) {
let [a] = n[c];
for (let i = o.length - 1; i >= 0; i--)
if (o[i].indexOf(a) !== -1) {
o[i] = o[i].replace(a, n[c][1]);
break;
}
}
return this.root.insert(o, t, r, this.context, s), r;
}
buildRegExp() {
let e = this.root.buildRegExpStr();
if (e === "") return [/^$/, [], []];
let t = 0,
s = [],
r = [];
return (
(e = e.replace(/#(\d+)|@(\d+)|\.\*\$/g, (n, o, c) =>
typeof o < "u"
? ((s[++t] = Number(o)), "$()")
: (typeof c < "u" && (r[Number(c)] = ++t), "")
)),
[new RegExp(`^${e}`), s, r]
);
}
};
var de = [],
Ne = [/^$/, [], Object.create(null)],
fe = Object.create(null);
function pe(e) {
return (fe[e] ??= new RegExp(
e === "*"
? ""
: `^${e.replace(/\/\*$|([.\\+*[^\]$()])/g, (t, s) => (s ? `\\${s}` : "(?:|/.*)"))}$`
));
}
function Le() {
fe = Object.create(null);
}
function De(e) {
let t = new ue(),
s = [];
if (e.length === 0) return Ne;
let r = e
.map(l => [!/\*|\/:/.test(l[0]), ...l])
.sort(([l, h], [u, p]) => (l ? 1 : u ? -1 : h.length - p.length)),
n = Object.create(null);
for (let l = 0, h = -1, u = r.length; l < u; l++) {
let [p, R, f] = r[l];
p ? (n[R] = [f.map(([y]) => [y, Object.create(null)]), de]) : h++;
let m;
try {
m = t.insert(R, h, p);
} catch (y) {
throw y === C ? new I(R) : y;
}
p ||
(s[h] = f.map(([y, E]) => {
let P = Object.create(null);
for (E -= 1; E >= 0; E--) {
let [x, j] = m[E];
P[x] = j;
}
return [y, P];
}));
}
let [o, c, a] = t.buildRegExp();
for (let l = 0, h = s.length; l < h; l++)
for (let u = 0, p = s[l].length; u < p; u++) {
let R = s[l][u]?.[1];
if (!R) continue;
let f = Object.keys(R);
for (let m = 0, y = f.length; m < y; m++) R[f[m]] = a[R[f[m]]];
}
let i = [];
for (let l in c) i[l] = s[c[l]];
return [o, i, n];
}
function O(e, t) {
if (e) {
for (let s of Object.keys(e).sort((r, n) => n.length - r.length))
if (pe(s).test(t)) return [...e[s]];
}
}
var z = class {
name = "RegExpRouter";
middleware;
routes;
constructor() {
(this.middleware = { [d]: Object.create(null) }),
(this.routes = { [d]: Object.create(null) });
}
add(e, t, s) {
let { middleware: r, routes: n } = this;
if (!r || !n) throw new Error(B);
r[e] ||
[r, n].forEach(a => {
(a[e] = Object.create(null)),
Object.keys(a[d]).forEach(i => {
a[e][i] = [...a[d][i]];
});
}),
t === "/*" && (t = "*");
let o = (t.match(/\/:/g) || []).length;
if (/\*$/.test(t)) {
let a = pe(t);
e === d
? Object.keys(r).forEach(i => {
r[i][t] ||= O(r[i], t) || O(r[d], t) || [];
})
: (r[e][t] ||= O(r[e], t) || O(r[d], t) || []),
Object.keys(r).forEach(i => {
(e === d || e === i) &&
Object.keys(r[i]).forEach(l => {
a.test(l) && r[i][l].push([s, o]);
});
}),
Object.keys(n).forEach(i => {
(e === d || e === i) &&
Object.keys(n[i]).forEach(l => a.test(l) && n[i][l].push([s, o]));
});
return;
}
let c = k(t) || [t];
for (let a = 0, i = c.length; a < i; a++) {
let l = c[a];
Object.keys(n).forEach(h => {
(e === d || e === h) &&
((n[h][l] ||= [...(O(r[h], l) || O(r[d], l) || [])]),
n[h][l].push([s, o - i + a + 1]));
});
}
}
match(e, t) {
Le();
let s = this.buildAllMatchers();
return (
(this.match = (r, n) => {
let o = s[r] || s[d],
c = o[2][n];
if (c) return c;
let a = n.match(o[0]);
if (!a) return [[], de];
let i = a.indexOf("", 1);
return [o[1][i], a];
}),
this.match(e, t)
);
}
buildAllMatchers() {
let e = Object.create(null);
return (
[...Object.keys(this.routes), ...Object.keys(this.middleware)].forEach(
t => {
e[t] ||= this.buildMatcher(t);
}
),
(this.middleware = this.routes = void 0),
e
);
}
buildMatcher(e) {
let t = [],
s = e === d;
return (
[this.middleware, this.routes].forEach(r => {
let n = r[e] ? Object.keys(r[e]).map(o => [o, r[e][o]]) : [];
n.length !== 0
? ((s ||= !0), t.push(...n))
: e !== d && t.push(...Object.keys(r[d]).map(o => [o, r[d][o]]));
}),
s ? De(t) : null
);
}
};
var Y = class {
name = "SmartRouter";
routers = [];
routes = [];
constructor(e) {
Object.assign(this, e);
}
add(e, t, s) {
if (!this.routes) throw new Error(B);
this.routes.push([e, t, s]);
}
match(e, t) {
if (!this.routes) throw new Error("Fatal error");
let { routers: s, routes: r } = this,
n = s.length,
o = 0,
c;
for (; o < n; o++) {
let a = s[o];
try {
r.forEach(i => {
a.add(...i);
}),
(c = a.match(e, t));
} catch (i) {
if (i instanceof I) continue;
throw i;
}
(this.match = a.match.bind(a)),
(this.routers = [a]),
(this.routes = void 0);
break;
}
if (o === n) throw new Error("Fatal error");
return (this.name = `SmartRouter + ${this.activeRouter.name}`), c;
}
get activeRouter() {
if (this.routes || this.routers.length !== 1)
throw new Error("No active router has been determined yet.");
return this.routers[0];
}
};
var Q = class {
methods;
children;
patterns;
order = 0;
name;
params = Object.create(null);
constructor(e, t, s) {
if (
((this.children = s || Object.create(null)),
(this.methods = []),
(this.name = ""),
e && t)
) {
let r = Object.create(null);
(r[e] = { handler: t, possibleKeys: [], score: 0, name: this.name }),
(this.methods = [r]);
}
this.patterns = [];
}
insert(e, t, s) {
(this.name = `${e} ${t}`), (this.order = ++this.order);
let r = this,
n = re(t),
o = [];
for (let i = 0, l = n.length; i < l; i++) {
let h = n[i];
if (Object.keys(r.children).includes(h)) {
r = r.children[h];
let p = $(h);
p && o.push(p[1]);
continue;
}
r.children[h] = new Q();
let u = $(h);
u && (r.patterns.push(u), o.push(u[1])), (r = r.children[h]);
}
r.methods.length || (r.methods = []);
let c = Object.create(null),
a = {
handler: s,
possibleKeys: o.filter((i, l, h) => h.indexOf(i) === l),
name: this.name,
score: this.order,
};
return (c[e] = a), r.methods.push(c), r;
}
gHSets(e, t, s, r) {
let n = [];
for (let o = 0, c = e.methods.length; o < c; o++) {
let a = e.methods[o],
i = a[t] || a[d],
l = Object.create(null);
i !== void 0 &&
((i.params = Object.create(null)),
i.possibleKeys.forEach(h => {
let u = l[i.name];
(i.params[h] = r[h] && !u ? r[h] : (s[h] ?? r[h])), (l[i.name] = !0);
}),
n.push(i));
}
return n;
}
search(e, t) {
let s = [];
this.params = Object.create(null);
let n = [this],
o = D(t);
for (let a = 0, i = o.length; a < i; a++) {
let l = o[a],
h = a === i - 1,
u = [];
for (let p = 0, R = n.length; p < R; p++) {
let f = n[p],
m = f.children[l];
m &&
((m.params = f.params),
h === !0
? (m.children["*"] &&
s.push(
...this.gHSets(
m.children["*"],
e,
f.params,
Object.create(null)
)
),
s.push(...this.gHSets(m, e, f.params, Object.create(null))))
: u.push(m));
for (let y = 0, E = f.patterns.length; y < E; y++) {
let P = f.patterns[y],
x = { ...f.params };
if (P === "*") {
let N = f.children["*"];
N &&
(s.push(...this.gHSets(N, e, f.params, Object.create(null))),
u.push(N));
continue;
}
if (l === "") continue;
let [j, Z, A] = P,
w = f.children[j],
ee = o.slice(a).join("/");
if (A instanceof RegExp && A.test(ee)) {
(x[Z] = ee), s.push(...this.gHSets(w, e, f.params, x));
continue;
}
(A === !0 || (A instanceof RegExp && A.test(l))) &&
typeof j == "string" &&
((x[Z] = l),
h === !0
? (s.push(...this.gHSets(w, e, x, f.params)),
w.children["*"] &&
s.push(...this.gHSets(w.children["*"], e, x, f.params)))
: ((w.params = x), u.push(w)));
}
}
n = u;
}
return [
s
.sort((a, i) => a.score - i.score)
.map(({ handler: a, params: i }) => [a, i]),
];
}
};
var X = class {
name = "TrieRouter";
node;
constructor() {
this.node = new Q();
}
add(e, t, s) {
let r = k(t);
if (r) {
for (let n of r) this.node.insert(e, n, s);
return;
}
this.node.insert(e, t, s);
}
match(e, t) {
return this.node.search(e, t);
}
};
var J = class extends G {
constructor(e = {}) {
super(e),
(this.router = e.router ?? new Y({ routers: [new z(), new X()] }));
}
};
var me = e => {
let s = {
...{
origin: "*",
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
allowHeaders: [],
exposeHeaders: [],
},
...e,
},
r = (n =>
typeof n == "string"
? () => n
: typeof n == "function"
? n
: o => (n.includes(o) ? o : n[0]))(s.origin);
return async function (o, c) {
function a(l, h) {
o.res.headers.set(l, h);
}
let i = r(o.req.header("origin") || "", o);
if ((i && a("Access-Control-Allow-Origin", i), s.origin !== "*")) {
let l = o.req.header("Vary");
l ? a("Vary", l) : a("Vary", "Origin");
}
if (
(s.credentials && a("Access-Control-Allow-Credentials", "true"),
s.exposeHeaders?.length &&
a("Access-Control-Expose-Headers", s.exposeHeaders.join(",")),
o.req.method === "OPTIONS")
) {
s.maxAge != null && a("Access-Control-Max-Age", s.maxAge.toString()),
s.allowMethods?.length &&
a("Access-Control-Allow-Methods", s.allowMethods.join(","));
let l = s.allowHeaders;
if (!l?.length) {
let h = o.req.header("Access-Control-Request-Headers");
h && (l = h.split(/\s*,\s*/));
}
return (
l?.length &&
(a("Access-Control-Allow-Headers", l.join(",")),
o.res.headers.append("Vary", "Access-Control-Request-Headers")),
o.res.headers.delete("Content-Length"),
o.res.headers.delete("Content-Type"),
new Response(null, {
headers: o.res.headers,
status: 204,
statusText: o.res.statusText,
})
);
}
await c();
};
};
async function ge(e) {
let t = e.req.param("key"),
s = await e.env.R2_BUCKET.get(t);
if (s === null) return new Response("Object Not Found", { status: 404 });
let r = new Headers();
return (
s.writeHttpMetadata(r),
r.set("etag", s.httpEtag),
r.set("Access-Control-Allow-Origin", "*"),
new Response(s.body, { headers: r })
);
}
async function ye(e) {
let t = e.req.query("cursor"),
s = await e.env.R2_BUCKET.list({ cursor: t || void 0 });
return e.json(s);
}
async function Re(e) {
let t = e.req.param("key"),
s = await e.req.blob();
return t
? (await e.env.R2_BUCKET.put(t, s), e.text("Done"))
: e.text("file name is required", 400);
}
async function xe(e) {
let t = e.req.param("key");
return await e.env.R2_BUCKET.delete(t), new Response(null, { status: 204 });
}
async function Ee(e) {
let t = e.req.param("key"),
s = await e.env.R2_BUCKET.createMultipartUpload(t);
return e.json({ key: s.key, uploadId: s.uploadId });
}
async function we(e) {
let t = e.req.query("uploadId"),
s = e.req.query("partNumber") || "",
r = e.req.param("key");
if (!t) return e.text("uploadId is required", 400);
if (!s) return e.text("partNumber is required", 400);
let n = Number(s);
if (isNaN(n)) return e.text("partNumber must be a number", 400);
let o = e.env.R2_BUCKET.resumeMultipartUpload(r, t);
try {
let c = await o.uploadPart(n, e.req.raw.body);
return new Response(JSON.stringify(c));
} catch (c) {
return new Response(c.message, { status: 400 });
}
}
async function be(e) {
let t = e.req.param("key"),
s = e.req.query("uploadId");
if (!s) return e.text("uploadId is required", 400);
let r = e.env.R2_BUCKET.resumeMultipartUpload(t, s);
try {
await r.abort();
} catch (n) {
return new Response(n.message, { status: 400 });
}
return new Response(null, { status: 204 });
}
async function ve(e) {
let t = e.req.param("key"),
s = e.req.query("uploadId");
if (!s) return e.text("uploadId is required", 400);
let r = e.env.R2_BUCKET.resumeMultipartUpload(t, s),
n;
try {
n = await e.req.json();
} catch (o) {
return (
console.log("parsing complete body failed"),
console.log(o),
e.text("invalid json", 400)
);
}
try {
let o = await r.complete(n.parts);
return new Response(null, { headers: { etag: o.httpEtag } });
} catch (o) {
return new Response(o.message, { status: 400 });
}
}
function Ce(e) {
return e.text("yes");
}
function $e(e) {
if (!e.env.AUTH_KEY_SECRET) return e.text("AUTH_KEY_SECRET is not set", 403);
let t = e.req.header("x-api-key") === e.env.AUTH_KEY_SECRET;
return e.req.method === "GET"
? e.env.PRIVATE_BUCKET
? t
: !0
: ["POST", "PATCH", "PUT", "DELETE"].includes(e.req.method)
? t
: e.req.method === "OPTIONS";
}
async function Oe(e, t) {
return (
$e(e) && (console.log("Header is valid"), await t()),
e.json({ status: 401, message: "Unauthorized" })
);
}
var g = new J();
g.use("*", Oe);
g.use(me());
g.get("/", e => e.text("Hello R2! v2024.07.12"));
g.get("/support_mpu", Ce);
g.post("/mpu/create/:key{.*}", Ee);
g.put("/mpu/:key{.*}", we);
g.delete("/mpu/:key{.*}", be);
g.post("/mpu/complete/:key{.*}", ve);
g.get("/:key{.*}", ge);
g.patch("/", ye);
g.put("/:key{.*}", Re);
g.delete("/:key{.*}", xe);
g.all("*", e => e.text("404 Not Found"));
var gr = g;
export { gr as default };
//# sourceMappingURL=index.js.map
-
进入到「设置」页面,选择「变量」,然后点击「编辑变量」按钮,添加环境变量。首先设置第一个变量名为
AUTH_KEY_SECRET
,变量值为随机字符,可以在此处生成一个。然后设置第二个变量名为PRIVATE_BUCKET
,变量值为true
。因为默认情况下,Worker 会允许所有 GET 请求通过,这意味着只要知道 Woker 的 URL,任何人都可以访问您的文件。所以添加这个环境变量,将使 Worker 检查每个请求的x-api-key
标头,并只允许带有正确 API 密钥的请求通过。添加完成后,点击「部署」按钮。 -
下拉这个页面找到「R2 存储桶绑定」,点击「编辑变量」按钮,添加存储桶。在变量名称初填写
R2_BUCKET
,然后选择您的存储桶,点击「部署」按钮。 -
在这个页面中选择「触发器」,点击「添加自定义域」按钮,为 Worker 添加一个域名。如果您身处中国大陆,建议添加,因为默认的
workers.dev
域名被 GFW 屏蔽了,不能正常工作。您也可以不添加自定义域名,那么就是使用红框中的路由地址。 -
大功告成,R2 Uploader 的 Worker 就配置好了。现在点击您在上一步中添加的自定义域名或者路由地址,就会在新页面中看到
{"status":401,"message":"Unauthorized"}
的字样,这是因为在第七步中我们添加了存储桶私有的环境变量。如果您在第七步中没有添加该变量,将会看到Hello R2!
的字样。
部署前端界面
理论上,当配置完 R2 Uploader 的 Worker 后,就可以使用了。例如,您可以直接使用该项目的开发者提供的前端界面。但我更建议自己部署一个前端界面。 R2 Uploader 的社区贡献者提供了使用 Docker 部署的方式。
以在 Zeabur 上部署为例,简单介绍。您可以选择在 VPS 上部署。
-
在 GitHub 上打开 R2 Uploader 的项目页面,将其 fork 到自己的仓库。
-
在 Zeabur 上新建项目,选择从 GitHub 仓库部署,选择刚 fork 的 R2 Uploader 项目。
-
点击「部署」按钮,等待部署完成。
-
在「网络」中生成一个域名或添加自定义域名。
-
大功告成,现在您就可以访问前端界面了。
在前端界面进行设置
-
展开「Endpoints」
-
在 Wokers Endpoint 中输入您在配置 R2 Uploader Worker 时,在「触发器」中自定义的域名或者 worker 的默认路由。
-
在 Workers Endpoint API Key 中输入您在配置 R2 Uploader Worker 时,在第七步中设置的环境变量
AUTH_KEY_SECRET
的值,是您生成的一段随机字符,可以在 Woker 的变量中查看。 -
点击 Save To LocalStorage。R2 Uploader 不会将您的 Endpoint 或 API Key 存储在云中,而是存储在浏览器的 LocalStorage 中,这意味着只有您才能访问。所有流量都会经过 Worker 和您的 R2 存储桶。
-
(可选)您可以选择登陆 GitHub,保存您的 Endpoints 中的设置的数据。不过我并没有这样做,而是将这些数据保存在 Bitwarden 中。
现在您就可以在这个前端页面中看到您在 R2 中存储的文件了。
在 Upload Files 中可以选择上传单个文件或者文件夹。
File List 中会显示您的存储桶中有多少个文件,使用了多少空间。文件列表默认按照文件夹的形式进行展示,您可以点击文件名称在新页面打开文件,或者点击 “Delete” 按钮删除文件。如果您不像以文件夹的形式展示,取消 “Folder Structure” 的勾选即可。
如果您和我一样,按照 Pseudoyu 的教程为 R2 中保存的文件配置了 WebP Cloud,您会发现点击文件名无法打开图片。这是因为在配置 WebP Cloud 时,在 Cloudflare 的防火墙中设置了禁止请求文件源站。
不过不必担心,在 Endpoints 中点击 “edit” 按钮,在 Custom Domain 中输入 WebP Cloud 为您分配的域名(不需要有 “https://”),然后点击保存即可。
现在您在点击文件名,就可以直接通过 WebP Cloud 的代理打开图片了。
另外在提一点,WebP Cloud 刚刚上线了「自适应调整大小」的功能,在 Proxy 中打开 Adaptive Resizer 即可。WebP Cloud 将根据访问者的设备处理不同尺寸的图片渲染。
以上就是部署和配置 R2 Uploader 的全部步骤。