[O] Use header key

This commit is contained in:
2025-10-25 21:47:09 +08:00
parent 05c3cb2b3c
commit ea2749e8c9
2 changed files with 68 additions and 80 deletions
+67 -76
View File
@@ -17,21 +17,20 @@ interface PhotoMetadata {
edited_photo: { filename: string, path: string, exif: any } edited_photo: { filename: string, path: string, exif: any }
} }
class HttpError extends Error {
constructor(public status: number, message: string) {
super(message)
}
}
/** /**
* Loads the metadata.json file. * Loads the metadata.json file.
* Returns an empty array if the file doesn't exist. * Returns an empty array if the file doesn't exist.
*/ */
async function getMetadata(): Promise<PhotoMetadata[]> { async function getMetadata(): Promise<PhotoMetadata[]> {
const file = Bun.file(FILE_METADATA) const file = Bun.file(FILE_METADATA)
if (!(await file.exists())) { if (!(await file.exists())) return []
return [] // Return empty array if no metadata file yet return await file.json()
}
try {
return await file.json()
} catch (e) {
console.warn(`Warning: Could not parse 'metadata.json'. Returning empty array.`, e)
return []
}
} }
/** /**
@@ -66,89 +65,77 @@ async function getExifData(file: File): Promise<any> {
// --- Elysia Server --- // --- Elysia Server ---
// Load secrets and ensure directories exist before starting // Load secrets and ensure directories exist before starting
const INSTANT_KEY = process.env.INSTANT_KEY const INSTANT_KEY = process.env.INSTANT_KEY!
if (!INSTANT_KEY) throw new Error("INSTANT_KEY is not defined")
if (!await exists(FOLDER_PHOTOS)) await mkdir(FOLDER_PHOTOS, { recursive: true }) if (!await exists(FOLDER_PHOTOS)) await mkdir(FOLDER_PHOTOS, { recursive: true })
console.log("Server starting with valid 'secrets.json'.") console.log("Server starting with valid 'secrets.json'.")
export const app = new Elysia() function checkHeaderKey(headers: Record<string, string | undefined>, expectList: string[] = [INSTANT_KEY]) {
.post("/upload", async ({ body, status }) => { const key = headers["x-instant-key"]
const { key, id, owner_key, photo, edited_photo } = body if (!expectList.includes(key ?? "")) throw new HttpError(401, "Invalid authentication key")
}
// 1. Authentication export const app = new Elysia()
if (key !== INSTANT_KEY) status(401, "Invalid authentication key") // Error handling: Return status code and message
.onError(({ error, status }) => {
console.error(error)
if (error instanceof HttpError) return status(error.status, { error: error.message })
else return status(500, { error: "Internal Server Error" })
})
.post("/upload", async ({ body, headers, status }) => {
const { id, owner_key, photo, edited_photo } = body
checkHeaderKey(headers)
// 2. Check for existing ID // 2. Check for existing ID
const metadata = await getMetadata() const metadata = await getMetadata()
if (metadata.find((m) => m.id === id)) status(409, "This ID already exists") if (metadata.find((m) => m.id === id)) status(409, "This ID already exists")
try { // 3. Process and save files
// 3. Process and save files // We use {id}-1 and {id}-2 as requested by "{id}-{number}"
// We use {id}-1 and {id}-2 as requested by "{id}-{number}" const originalExt = photo.name.split(".").pop() || "jpg"
const originalExt = photo.name.split(".").pop() || "jpg" const editedExt = edited_photo.name.split(".").pop() || "jpg"
const editedExt = edited_photo.name.split(".").pop() || "jpg"
// Using "1" for original and "2" for edited // Using "1" for original and "2" for edited
const originalPath = `${FOLDER_PHOTOS}/${id}-1.${originalExt}` const originalPath = `${FOLDER_PHOTOS}/${id}-1.${originalExt}`
const editedPath = `${FOLDER_PHOTOS}/${id}-2.${editedExt}` const editedPath = `${FOLDER_PHOTOS}/${id}-2.${editedExt}`
// Run file saving and EXIF parsing in parallel // Run file saving and EXIF parsing in parallel
const [originalExif, editedExif] = await Promise.all([ const [originalExif, editedExif] = await Promise.all([
getExifData(photo), getExifData(photo),
getExifData(edited_photo), getExifData(edited_photo),
Bun.write(originalPath, photo), Bun.write(originalPath, photo),
Bun.write(editedPath, edited_photo), Bun.write(editedPath, edited_photo),
]) ])
// 4. Create new metadata entry // 4. Create new metadata entry
const newEntry: PhotoMetadata = { const newEntry: PhotoMetadata = {
id: id, id: id,
owner_key: owner_key, owner_key: owner_key,
upload_time: new Date().toISOString(), upload_time: new Date().toISOString(),
original_photo: { original_photo: {
filename: photo.name, filename: photo.name,
path: originalPath, path: originalPath,
exif: originalExif, exif: originalExif,
}, },
edited_photo: { edited_photo: {
filename: edited_photo.name, filename: edited_photo.name,
path: editedPath, path: editedPath,
exif: editedExif, exif: editedExif,
}, },
}
// 5. Save updated metadata
metadata.push(newEntry)
await saveMetadata(metadata)
return status(201, { success: true, id: newEntry.id })
} catch (error) {
console.error("File upload processing error:", error)
return status(500, { error: "Failed to process file upload." })
} }
},
{ // 5. Save updated metadata
metadata.push(newEntry)
await saveMetadata(metadata)
return status(201, { success: true, id: newEntry.id })
}, {
// --- Validation Schema --- // --- Validation Schema ---
// We interpret "Part 1: JSON body" as the text fields of
// the multipart form, which is the standard way to send
// metadata alongside files.
body: t.Object({ body: t.Object({
key: t.String(), id: t.String(),
id: t.String({ owner_key: t.String(),
minLength: 3,
maxLength: 20,
// Simple pattern: letters, numbers, hyphens
pattern: "^[a-zA-Z0-9_-]+$",
error:
"ID must be 3-20 alphanumeric characters, hyphens, or underscores.",
}),
owner_key: t.String({
minLength: 4,
maxLength: 4,
pattern: "^[0-9]{4}$", // e.g., "2941"
error: "Owner key must be exactly 4 digits.",
}),
photo: t.File({ photo: t.File({
type: ["image/jpeg", "image/png", "image/webp", "image/avif", "image/heic", "image/heif"], type: ["image/jpeg", "image/png", "image/webp", "image/avif", "image/heic", "image/heif"],
error: "Original photo must be a valid image (jpg, png, webp, avif, heic, heif).", error: "Original photo must be a valid image (jpg, png, webp, avif, heic, heif).",
@@ -174,6 +161,10 @@ export const app = new Elysia()
return publicMetadata return publicMetadata
}) })
// ----- 3. GET /photos/:id -----
// Returns the static file for the *edited* photo.
// Respects the 'hide' flag.
.listen(3000) .listen(3000)
console.log(`🦊 Elysia server running at http://${app.server?.hostname}:${app.server?.port}`) console.log(`🦊 Elysia server running at http://${app.server?.hostname}:${app.server?.port}`)
+1 -4
View File
@@ -36,12 +36,8 @@ async function testUpload() {
// 1. Create the multipart/form-data payload // 1. Create the multipart/form-data payload
const formData = new FormData(); const formData = new FormData();
formData.append("key", API_KEY);
formData.append("id", TEST_ID); formData.append("id", TEST_ID);
formData.append("owner_key", TEST_OWNER_KEY); formData.append("owner_key", TEST_OWNER_KEY);
// Append the files
// We'll use the same dummy image for both "original" and "edited"
formData.append("photo", testFile); formData.append("photo", testFile);
formData.append("edited_photo", testFile); formData.append("edited_photo", testFile);
@@ -50,6 +46,7 @@ async function testUpload() {
const response = await fetch(`${SERVER_URL}/upload`, { const response = await fetch(`${SERVER_URL}/upload`, {
method: "POST", method: "POST",
body: formData, body: formData,
headers: { "x-instant-key": API_KEY }
}); });
// 3. Log the response // 3. Log the response