diff --git a/src/database.ts b/src/database.ts index 0aa50dd..3632922 100644 --- a/src/database.ts +++ b/src/database.ts @@ -16,9 +16,9 @@ export async function migrateTables(db: D1Database) { export interface UploadRecord { id: number; uploader: string; - ctime: Date; + ctime: number; size: number; - files: string; // JSON string of [{ name, size, path }] + files: RecordFileItem[]; message: string; } @@ -29,6 +29,44 @@ export interface RecordFileItem { thumbnail?: string; } +function fromDB(data: any): UploadRecord { + let files: RecordFileItem[] = []; + try { + files = JSON.parse(data.files); + } catch {} + + return { + id: data.id, + uploader: data.uploader, + ctime: +new Date(data.ctime), + size: data.size, + files, + message: data.message, + }; +} + +function toDB(record: UploadRecord): any { + return { + id: record.id, + uploader: record.uploader, + ctime: new Date(+record.ctime), + size: record.size, + files: JSON.stringify( + Array.from( + record.files || [], + (item) => + item && { + name: String(item.name), + size: +item.size, + path: String(item.path || ""), + thumbnail: String(item.thumbnail || ""), + } + ).filter(Boolean) + ), + message: record.message, + }; +} + /** * get latest 20 records. in descending order (newest first) * @@ -48,18 +86,20 @@ export async function getUploadRecords( const rows = await db.prepare(sql).all(); for (const row of rows.results as any[]) { - records.push({ - id: row.id, - uploader: row.uploader, - ctime: new Date(row.ctime), - size: row.size, - files: row.files, - message: row.message, - }); + records.push(fromDB(row)); } return records; } +export async function getSingleUploadRecord(db: D1Database, id: number) { + const record = await db + .prepare(`SELECT * FROM upload_record WHERE id = ?`) + .bind(id) + .first(); + if (!record) return null; + return fromDB(record); +} + export async function createUploadRecord( db: D1Database, record: Omit @@ -68,16 +108,11 @@ export async function createUploadRecord( .prepare( "INSERT INTO upload_record (uploader, size, files, message) VALUES (?, ?, ?, ?)" ) - .bind( - record.uploader, - record.size, - record.files, - record.message - ) + .bind(record.uploader, record.size, record.files, record.message) .run(); const id = res.meta.last_row_id; const inserted: UploadRecord = { - ...record, + ...toDB(record as UploadRecord), ctime: new Date(), id, }; @@ -98,15 +133,9 @@ export async function purgeRecordsBeforeId( // 2. delete files for (const row of rows.results) { - if (!row.files) continue; - try { - const files = JSON.parse(row.files) as { - name: string; - path: string; - size: number; - }[]; - await deleteFiles(files.map((file) => file.path)); - } catch {} + const files = fromDB(row).files; + if (!files.length) continue; + await deleteFiles(files.map((file) => file.path)); } // 3. delete records @@ -121,20 +150,12 @@ export async function deleteRecord( deleteFiles: (path: string[]) => Promise ) { // 1. get record - const record = await db - .prepare(`SELECT * FROM upload_record WHERE id = ?`) - .bind(id) - .first(); + const record = await getSingleUploadRecord(db, id); if (!record) return; // 2. delete files - if (!record.files) return; try { - const files = JSON.parse(record.files) as { - name: string; - path: string; - size: number; - }[]; + const files = record.files; await deleteFiles(files.map((file) => file.path)); } catch {} diff --git a/src/index.ts b/src/index.ts index 8e021db..ded68f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { Hono } from "hono"; import { createUploadRecord, deleteRecord, + getSingleUploadRecord, getUploadRecords, migrateTables, purgeRecordsBeforeId, @@ -65,17 +66,28 @@ app.post("/api/upload", async (c) => { const record = await createUploadRecord(c.env.DB, { uploader, size: uploadedFiles.reduce((acc, file) => acc + file.size, 0), - files: JSON.stringify(uploadedFiles), + files: uploadedFiles, message, }); return c.json({ record }); }); -app.get("/api/download", async (c) => { - const filePath = c.req.query("path"); +app.get("/api/download/:id/:index", async (c) => { + const id = +c.req.param("id"); + const index = c.req.param("index"); + const record = await getSingleUploadRecord(c.env.DB, id); + if (!record) { + return c.status(404); + } + + if (index === "message") { + return c.text(record.message); + } + + const filePath = record.files[+index]?.path; if (!filePath) { - return c.json({ error: "No file path" }); + return c.status(404); } const r = await c.env.MY_BUCKET.get(filePath, { @@ -87,8 +99,16 @@ app.get("/api/download", async (c) => { return c.json({ error: "File not found" }); } + const basename = filePath.split("/").pop()!.replace(/\?.*/, ""); const headers = new Headers(); headers.set("accept-ranges", "bytes"); + + if ( + !/\.(jpg|png|gif|avif|mp4|mov|txt|html|js|css|json|ya?ml)/.test(basename) + ) { + headers.set("content-disposition", `attachment; filename="${basename}"`); + } + r.writeHttpMetadata(headers); return new Response(r.body, { headers }); }); diff --git a/web/src/App.scss b/web/src/App.scss index a7dd11d..641fec4 100644 --- a/web/src/App.scss +++ b/web/src/App.scss @@ -42,3 +42,23 @@ input[type='text'] { font-size: inherit; font-family: Menlo, Consolas, Roboto Mono, 'Source Code Pro', 'Courier New', Courier, monospace; } + +.popover-container { + position: relative; + + .popover-content { + display: none; + position: absolute; + top: 100%; + left: 0; + width: max-content; + } + + &:hover, + &:focus-within { + .popover-content { + display: flex; + flex-direction: column; + } + } +} diff --git a/web/src/components/UploadRecords.tsx b/web/src/components/UploadRecords.tsx index 03c6462..380116d 100644 --- a/web/src/components/UploadRecords.tsx +++ b/web/src/components/UploadRecords.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useMemo } from 'react'; import useSWRInfinite from 'swr/infinite'; -import type { RecordFileItem, UploadRecord } from '../../../src/database'; +import type { UploadRecord } from '../../../src/database'; interface Props {} @@ -38,10 +38,9 @@ export const UploadRecords = memo((props) => { }); const UploadRecordItem = memo((props: { record: UploadRecord }) => { - const files = useMemo(() => { - if (!props.record.files) return []; - return JSON.parse(props.record.files) as RecordFileItem[]; - }, [props.record.files]); + const files = useMemo(() => props.record.files || [], [props.record.files]); + + const actionLink = 'cursor-pointer hover:text-black text-inherit decoration-none'; return (
@@ -66,13 +65,27 @@ const UploadRecordItem = memo((props: { record: UploadRecord }) => {
{!!props.record.message && ( - copyToClipboard(props.record.message)}> - - Copy Message - +
+
copyToClipboard(props.record.message)}> + + Copy Message +
+ + +
)} - deleteRecord(props.record.id)}> + deleteRecord(props.record.id)}> Delete @@ -82,8 +95,8 @@ const UploadRecordItem = memo((props: { record: UploadRecord }) => { {files.length > 0 && (
- {files.map((file) => { - const link = `/api/download?path=${encodeURIComponent(file.path)}`; + {files.map((file, index) => { + const link = `/api/download/${props.record.id}/${index}`; return (