[+] Base server
This commit is contained in:
+176
-5
@@ -1,7 +1,178 @@
|
||||
import { Elysia } from "elysia";
|
||||
import { Elysia, t } from "elysia"
|
||||
import ExifReader from "exifreader"
|
||||
import { exists, mkdir } from "fs/promises"
|
||||
|
||||
const app = new Elysia().get("/", () => "Hello Elysia").listen(3000);
|
||||
// --- Configuration ---
|
||||
const FOLDER_PHOTOS = "./photos"
|
||||
const FILE_METADATA = "./metadata.json"
|
||||
|
||||
console.log(
|
||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
||||
);
|
||||
// --- Types ---
|
||||
// Interface for our metadata entries
|
||||
interface PhotoMetadata {
|
||||
id: string
|
||||
owner_key: string // This will be filtered out for public requests
|
||||
upload_time: string
|
||||
original_photo: { filename: string, path: string, exif: any }
|
||||
edited_photo: { filename: string, path: string, exif: any }
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the metadata.json file.
|
||||
* Returns an empty array if the file doesn't exist.
|
||||
*/
|
||||
async function getMetadata(): Promise<PhotoMetadata[]> {
|
||||
const file = Bun.file(FILE_METADATA)
|
||||
if (!(await file.exists())) {
|
||||
return [] // Return empty array if no metadata file yet
|
||||
}
|
||||
try {
|
||||
return await file.json()
|
||||
} catch (e) {
|
||||
console.warn(`Warning: Could not parse 'metadata.json'. Returning empty array.`, e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely saves the metadata array to metadata.json.
|
||||
*/
|
||||
async function saveMetadata(data: PhotoMetadata[]): Promise<void> {
|
||||
try {
|
||||
await Bun.write(FILE_METADATA, JSON.stringify(data, null, 2))
|
||||
} catch (e) {
|
||||
console.error("Error: Could not write to 'metadata.json'.", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts EXIF data from an uploaded file.
|
||||
*/
|
||||
async function getExifData(file: File): Promise<any> {
|
||||
try {
|
||||
const buffer = await file.arrayBuffer()
|
||||
// Set options to not parse maker notes or unknown tags for speed
|
||||
const tags = ExifReader.load(buffer, {
|
||||
expanded: true,
|
||||
includeUnknown: false,
|
||||
})
|
||||
return tags
|
||||
} catch (e) {
|
||||
console.error(`Error parsing EXIF for ${file.name}:`, e)
|
||||
return { error: "Could not parse EXIF data." }
|
||||
}
|
||||
}
|
||||
|
||||
// --- Elysia Server ---
|
||||
|
||||
// Load secrets and ensure directories exist before starting
|
||||
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)
|
||||
|
||||
console.log("Server starting with valid 'secrets.json'.")
|
||||
|
||||
export const app = new Elysia()
|
||||
.post("/upload", async ({ body, status }) => {
|
||||
const { key, id, owner_key, photo, edited_photo } = body
|
||||
|
||||
// 1. Authentication
|
||||
if (key !== INSTANT_KEY) status(401, "Invalid authentication key")
|
||||
|
||||
// 2. Check for existing ID
|
||||
const metadata = await getMetadata()
|
||||
if (metadata.find((m) => m.id === id)) status(409, "This ID already exists")
|
||||
|
||||
try {
|
||||
// 3. Process and save files
|
||||
// We use {id}-1 and {id}-2 as requested by "{id}-{number}"
|
||||
const originalExt = photo.name.split(".").pop() || "jpg"
|
||||
const editedExt = edited_photo.name.split(".").pop() || "jpg"
|
||||
|
||||
// Using "1" for original and "2" for edited
|
||||
const originalPath = `${FOLDER_PHOTOS}/${id}-1.${originalExt}`
|
||||
const editedPath = `${FOLDER_PHOTOS}/${id}-2.${editedExt}`
|
||||
|
||||
// Run file saving and EXIF parsing in parallel
|
||||
const [originalExif, editedExif] = await Promise.all([
|
||||
getExifData(photo),
|
||||
getExifData(edited_photo),
|
||||
Bun.write(originalPath, photo),
|
||||
Bun.write(editedPath, edited_photo),
|
||||
])
|
||||
|
||||
// 4. Create new metadata entry
|
||||
const newEntry: PhotoMetadata = {
|
||||
id: id,
|
||||
owner_key: owner_key,
|
||||
upload_time: new Date().toISOString(),
|
||||
original_photo: {
|
||||
filename: photo.name,
|
||||
path: originalPath,
|
||||
exif: originalExif,
|
||||
},
|
||||
edited_photo: {
|
||||
filename: edited_photo.name,
|
||||
path: editedPath,
|
||||
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." })
|
||||
}
|
||||
},
|
||||
{
|
||||
// --- 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({
|
||||
key: t.String(),
|
||||
id: 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({
|
||||
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).",
|
||||
}),
|
||||
edited_photo: t.File({
|
||||
type: ["image/jpeg", "image/png", "image/webp", "image/avif", "image/heic", "image/heif"],
|
||||
error: "Edited photo must be a valid image (jpg, png, webp, avif, heic, heif).",
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
// ----- 2. GET /photos -----
|
||||
// Returns all metadata entries *without* the 'owner_key'.
|
||||
.get("/photos", async () => {
|
||||
const metadata = await getMetadata()
|
||||
|
||||
// Map over the array and filter out the 'owner_key' from each object
|
||||
const publicMetadata = metadata.map((entry) => {
|
||||
const { owner_key, ...publicEntry } = entry
|
||||
return publicEntry
|
||||
})
|
||||
|
||||
return publicMetadata
|
||||
})
|
||||
|
||||
.listen(3000)
|
||||
|
||||
console.log(`🦊 Elysia server running at http://${app.server?.hostname}:${app.server?.port}`)
|
||||
|
||||
Reference in New Issue
Block a user