[O] Use header key
This commit is contained in:
+31
-40
@@ -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
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return await file.json()
|
return await file.json()
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Warning: Could not parse 'metadata.json'. Returning empty array.`, e)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,24 +65,33 @@ 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"
|
||||||
@@ -123,32 +131,11 @@ export const app = new Elysia()
|
|||||||
await saveMetadata(metadata)
|
await saveMetadata(metadata)
|
||||||
|
|
||||||
return status(201, { success: true, id: newEntry.id })
|
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." })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// --- 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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user