feat: Implement a global upload dialog and refactor the upload UI/UX, including manifest size tracking.
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user