wehub-cloud-tpl-static-site-function
3db74f3c8c
- POST /api/cloud/uploads: presigned PUT URL + pending metadata row - POST /api/cloud/uploads/:id/confirm: mark ready, HEAD object to backfill size - GET /api/cloud/uploads: list metadata - GET /api/cloud/uploads/🆔 metadata + presigned GET download URL - DELETE /api/cloud/uploads/🆔 delete object + metadata - Auto-migrate app.uploads schema - S3 credentials exchange with expiry cache - Frontend: upload (direct PUT), list, download, delete UI - Drop 512MiB big.bin build (no longer needed)
188 行
5.5 KiB
JavaScript
188 行
5.5 KiB
JavaScript
// 前端:上传(presigned PUT 直传对象存储)→ confirm → 列表 → 下载(presigned GET)。
|
||
const API_BASE = "/api/cloud";
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
function esc(v) {
|
||
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
|
||
({ "&": "&", "<": "<", ">": ">", '"': """ }[c])
|
||
);
|
||
}
|
||
|
||
function fmtSize(n) {
|
||
n = Number(n) || 0;
|
||
if (n < 1024) return n + " B";
|
||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KiB";
|
||
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + " MiB";
|
||
return (n / 1024 / 1024 / 1024).toFixed(2) + " GiB";
|
||
}
|
||
|
||
function fmtTime(iso) {
|
||
if (!iso) return "";
|
||
const d = new Date(iso);
|
||
return Number.isNaN(d.getTime()) ? iso : d.toLocaleString();
|
||
}
|
||
|
||
async function api(path, opts = {}) {
|
||
const res = await fetch(API_BASE + path, opts);
|
||
let data;
|
||
try { data = await res.json(); } catch (e) { data = { ok: false, error: "invalid JSON" }; }
|
||
if (!res.ok || data.ok === false) {
|
||
throw new Error(data.error || "HTTP " + res.status);
|
||
}
|
||
return data;
|
||
}
|
||
|
||
// --- 状态 ---
|
||
|
||
async function loadHealth() {
|
||
const el = $("status");
|
||
el.textContent = "加载中…";
|
||
try {
|
||
const d = await api("/health");
|
||
const b = d.bindings || {};
|
||
const rows = [
|
||
["数据库迁移", d.migrated ? "✓" : "✗"],
|
||
["database", b.database ? "已配置" : "未配置"],
|
||
["object_store", b.object_store ? "已配置" : "未配置"],
|
||
];
|
||
el.innerHTML = rows
|
||
.map(
|
||
(r) =>
|
||
'<div class="row"><span class="k">' + esc(r[0]) + '</span><span class="v ' +
|
||
(r[1] === "✓" || r[1] === "已配置" ? "ok" : "no") + '">' + esc(r[1]) + "</span></div>"
|
||
)
|
||
.join("");
|
||
} catch (e) {
|
||
el.innerHTML = '<pre>' + esc(e.message) + "</pre>";
|
||
}
|
||
}
|
||
|
||
// --- 上传 ---
|
||
|
||
async function upload() {
|
||
const fileEl = $("file");
|
||
const out = $("upload-out");
|
||
const btn = $("upload");
|
||
const file = fileEl.files && fileEl.files[0];
|
||
if (!file) {
|
||
out.textContent = "请先选择一个文件";
|
||
return;
|
||
}
|
||
btn.disabled = true;
|
||
out.textContent = "申请上传地址…";
|
||
try {
|
||
const created = await api("/uploads", {
|
||
method: "POST",
|
||
headers: { "content-type": "application/json" },
|
||
body: JSON.stringify({
|
||
filename: file.name,
|
||
content_type: file.type || "application/octet-stream",
|
||
size: file.size,
|
||
}),
|
||
});
|
||
|
||
out.textContent = "直传对象存储…";
|
||
const putRes = await fetch(created.upload_url, {
|
||
method: "PUT",
|
||
headers: { "content-type": file.type || "application/octet-stream" },
|
||
body: file,
|
||
});
|
||
if (!putRes.ok) {
|
||
throw new Error("对象存储 PUT 失败: HTTP " + putRes.status);
|
||
}
|
||
|
||
out.textContent = "确认上传…";
|
||
const confirmed = await api("/uploads/" + created.id + "/confirm", {
|
||
method: "POST",
|
||
headers: { "content-type": "application/json" },
|
||
body: JSON.stringify({ size: file.size }),
|
||
});
|
||
|
||
out.textContent =
|
||
"上传完成 ✓\n" +
|
||
"id=" + confirmed.id + " size=" + fmtSize(confirmed.size) + " status=" + confirmed.status;
|
||
fileEl.value = "";
|
||
loadList();
|
||
} catch (e) {
|
||
out.textContent = "上传失败: " + e.message;
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// --- 列表 ---
|
||
|
||
function renderRow(it) {
|
||
const tag = '<span class="tag ' + esc(it.status) + '">' + esc(it.status) + "</span>";
|
||
const actions =
|
||
'<button type="button" class="btn small" data-act="download" data-id="' + it.id + '">下载</button>' +
|
||
'<button type="button" class="btn small" data-act="delete" data-id="' + it.id + '">删除</button>';
|
||
return (
|
||
"<tr>" +
|
||
"<td>" + it.id + "</td>" +
|
||
"<td>" + esc(it.filename) + "</td>" +
|
||
"<td>" + esc(it.content_type) + "</td>" +
|
||
"<td>" + fmtSize(it.size) + "</td>" +
|
||
"<td>" + tag + "</td>" +
|
||
"<td>" + esc(fmtTime(it.created_at)) + "</td>" +
|
||
'<td class="actions">' + actions + "</td>" +
|
||
"</tr>"
|
||
);
|
||
}
|
||
|
||
async function loadList() {
|
||
const tbody = $("list").querySelector("tbody");
|
||
const out = $("list-out");
|
||
out.textContent = "";
|
||
tbody.innerHTML = '<tr><td colspan="7">加载中…</td></tr>';
|
||
try {
|
||
const d = await api("/uploads");
|
||
if (!d.items.length) {
|
||
tbody.innerHTML = '<tr><td colspan="7">还没有文件</td></tr>';
|
||
return;
|
||
}
|
||
tbody.innerHTML = d.items.map(renderRow).join("");
|
||
} catch (e) {
|
||
tbody.innerHTML = "";
|
||
out.textContent = "列表失败: " + e.message;
|
||
}
|
||
}
|
||
|
||
async function onListClick(e) {
|
||
const btn = e.target.closest("button[data-act]");
|
||
if (!btn) return;
|
||
const id = btn.dataset.id;
|
||
const out = $("list-out");
|
||
out.textContent = "";
|
||
if (btn.dataset.act === "download") {
|
||
try {
|
||
const d = await api("/uploads/" + id);
|
||
if (!d.download_url) {
|
||
out.textContent = "暂无下载链接(状态: " + d.status + ")";
|
||
return;
|
||
}
|
||
window.open(d.download_url, "_blank");
|
||
} catch (err) {
|
||
out.textContent = "下载失败: " + err.message;
|
||
}
|
||
} else if (btn.dataset.act === "delete") {
|
||
if (!confirm("删除 id=" + id + " 的文件与元信息?")) return;
|
||
try {
|
||
await api("/uploads/" + id, { method: "DELETE" });
|
||
loadList();
|
||
} catch (err) {
|
||
out.textContent = "删除失败: " + err.message;
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 绑定 ---
|
||
|
||
$("refresh").addEventListener("click", loadHealth);
|
||
$("upload").addEventListener("click", upload);
|
||
$("reload").addEventListener("click", loadList);
|
||
$("list").querySelector("tbody").addEventListener("click", onListClick);
|
||
|
||
loadHealth();
|
||
loadList();
|