feat: Implement a global upload dialog and refactor the upload UI/UX, including manifest size tracking.

This commit is contained in:
2026-02-27 03:49:54 +07:00
parent ff1d4902bc
commit a5b4028bc8
19 changed files with 538 additions and 432 deletions

View File

@@ -17,6 +17,7 @@ export type Manifest = {
parts: Part[]
createdAt: number
expiresAt: number
size: number
}
// ---------------------------------------------------------------------------
@@ -42,7 +43,7 @@ const OBJECT_KEY = (id: string) => `${id}.json`
/** Persist a manifest as JSON in MinIO. */
export async function saveManifest(manifest: Manifest): Promise<void> {
const url = `${S3_ENDPOINT}/${BUCKET_NAME}/${OBJECT_KEY(manifest.id)}`;
const response = await aws.fetch(url, {
method: 'PUT',
headers: {
@@ -50,7 +51,7 @@ export async function saveManifest(manifest: Manifest): Promise<void> {
},
body: JSON.stringify(manifest),
});
if (!response.ok) {
throw new Error(`Failed to save manifest: ${response.status} ${await response.text()}`)
}
@@ -59,28 +60,28 @@ export async function saveManifest(manifest: Manifest): Promise<void> {
/** Fetch a manifest from MinIO. */
export async function getManifest(id: string): Promise<Manifest | null> {
const url = `${S3_ENDPOINT}/${BUCKET_NAME}/${OBJECT_KEY(id)}`;
try {
const response = await aws.fetch(url, {
method: 'GET',
});
if (response.status === 404) {
return null
}
if (!response.ok) {
throw new Error(`Failed to get manifest: ${response.status}`)
}
const text = await response.text()
const manifest: Manifest = JSON.parse(text)
if (manifest.expiresAt < Date.now()) {
await deleteManifest(id).catch(() => {})
await deleteManifest(id).catch(() => { })
return null
}
return manifest
} catch (error) {
return null
@@ -90,11 +91,11 @@ export async function getManifest(id: string): Promise<Manifest | null> {
/** Remove a manifest object from MinIO. */
export async function deleteManifest(id: string): Promise<void> {
const url = `${S3_ENDPOINT}/${BUCKET_NAME}/${OBJECT_KEY(id)}`;
const response = await aws.fetch(url, {
method: 'DELETE',
});
if (!response.ok && response.status !== 404) {
throw new Error(`Failed to delete manifest: ${response.status}`)
}
@@ -158,6 +159,7 @@ export async function getListFiles(): Promise<string[]> {
export function createManifest(
filename: string,
chunks: string[],
size: number,
ttlMs = 60 * 60 * 1000,
): Manifest {
const id = crypto.randomUUID()
@@ -170,6 +172,7 @@ export function createManifest(
parts: chunks.map((url, index) => ({ index, host: detectHost(url), url: formatUrl(url) })),
createdAt: now,
expiresAt: now + ttlMs,
size,
}
}

View File

@@ -1,8 +1,8 @@
import { baseAPIURL } from '@/api/httpClientAdapter.server';
import {
createManifest,
saveManifest,
validateChunkUrls
import {
createManifest,
saveManifest,
validateChunkUrls
} from '@/server/modules/merge';
import type { Hono, MiddlewareHandler } from 'hono';
@@ -10,29 +10,27 @@ const authMiddleware: MiddlewareHandler = async (c, next) => {
const headers = new Headers(c.req.header());
headers.delete("host");
headers.delete("connection");
try {
const res = await fetch(`${baseAPIURL}/me`, {
method: 'GET',
headers: headers,
credentials: 'include'
});
const data = await res.json();
if (data.data?.user) {
return await next();
return fetch(`${baseAPIURL}/me`, {
method: 'GET',
headers: headers,
credentials: 'include'
}).then(res => res.json()).then((r) => {
if (r.data?.user) {
return next();
}
throw new Error("Unauthorized");
} catch {
else {
throw new Error("Unauthorized");
}
}).catch(() => {
return c.json({ error: "Unauthorized" }, 401);
}
});
};
export function registerMergeRoutes(app: Hono) {
app.post('/merge', authMiddleware, async (c) => {
try {
const body = await c.req.json();
const { filename, chunks } = body;
const { filename, chunks, size } = body;
if (!filename || !Array.isArray(chunks) || chunks.length === 0) {
return c.json({ error: 'invalid payload' }, 400);
@@ -41,7 +39,7 @@ export function registerMergeRoutes(app: Hono) {
const hostError = validateChunkUrls(chunks);
if (hostError) return c.json({ error: hostError }, 400);
const manifest = createManifest(filename, chunks);
const manifest = createManifest(filename, chunks, size);
await saveManifest(manifest);
return c.json({
@@ -49,6 +47,7 @@ export function registerMergeRoutes(app: Hono) {
id: manifest.id,
filename: manifest.filename,
total_parts: manifest.total_parts,
size: manifest.size,
});
} catch (e: any) {
return c.json({ error: e?.message ?? String(e) }, 500);

View File

@@ -18,18 +18,18 @@ export function registerSSRRoutes(app: Hono) {
app.get("*", async (c) => {
const nonce = crypto.randomUUID();
const url = new URL(c.req.url);
const { app: vueApp, router, head, pinia, bodyClass, queryCache } = createApp();
vueApp.provide("honoContext", c);
const auth = useAuthStore();
auth.$reset();
await auth.init();
await router.push(url.pathname);
await router.isReady();
const usedStyles = new Set<string>();
Base.setLoadedStyleName = async (name: string) => usedStyles.add(name);
@@ -49,9 +49,9 @@ export function registerSSRRoutes(app: Hono) {
await stream.write(headResult.headTags.replace(/\n/g, ""));
// Fonts & Favicon
await stream.write(`<link rel="preconnect" href="https://fonts.googleapis.com">`);
await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap" rel="stylesheet">`);
await stream.write('<link rel="icon" href="/favicon.ico" />');
await stream.write('<link rel="icon" href="/favicon.ico">');
await stream.write(`<link rel="preconnect" href="https://fonts.googleapis.com" />`);
await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap" rel="stylesheet" />`);
// Bootstrap scripts
await stream.write(buildBootstrapScript());
@@ -60,11 +60,11 @@ export function registerSSRRoutes(app: Hono) {
if (usedStyles.size > 0) {
DEFAULT_STYLE_NAMES.forEach(name => usedStyles.add(name));
}
const activeStyles = styleTags.filter(tag =>
const activeStyles = styleTags.filter(tag =>
usedStyles.has(tag.name.replace(/-(variables|style)$/, ""))
);
for (const tag of activeStyles) {
await stream.write(`<style type="text/css" data-primevue-style-id="${tag.name}">${tag.value}</style>`);
}
@@ -81,9 +81,9 @@ export function registerSSRRoutes(app: Hono) {
delete ctx.modules;
// Inject state
Object.assign(ctx, {
$p: pinia.state.value,
$colada: serializeQueryCache(queryCache)
Object.assign(ctx, {
$p: pinia.state.value,
$colada: serializeQueryCache(queryCache)
});
// App data script