[+] Base server

This commit is contained in:
2025-10-25 21:25:57 +08:00
parent 7b39975dad
commit 89c529d83d
7 changed files with 362 additions and 8 deletions
+176 -5
View File
@@ -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}`)
+108
View File
@@ -0,0 +1,108 @@
// --- Configuration ---
// Make sure this key matches your secrets.json!
const API_KEY = "meowmeow";
const SERVER_URL = "http://localhost:3000";
// A unique ID for this test upload
const TEST_ID = `test-${Math.floor(Math.random() * 10000)}`;
const TEST_OWNER_KEY = "1234";
const TEST_IMAGE_PATH = "./UWU01721.JPG"; // The dummy image you will create
/**
* Creates a dummy image file if it doesn't exist.
* This is just for testing; in a real client, you'd use a real file.
*/
async function ensureTestImageExists() {
const file = Bun.file(TEST_IMAGE_PATH);
if (!(await file.exists())) {
console.log(`Creating dummy file at: ${TEST_IMAGE_PATH}`);
// Create a tiny, simple text file as a placeholder.
// The server validation only checks MIME type based on extension,
// so for this test, a fake "jpg" is okay.
// A real image would be better for EXIF parsing.
await Bun.write(TEST_IMAGE_PATH, "This is a test image buffer");
}
}
/**
* Test 1: POST /upload
* Attempts to upload the test image with metadata.
*/
async function testUpload() {
console.log(`--- Testing POST /upload with ID: ${TEST_ID} ---`);
await ensureTestImageExists();
const testFile = Bun.file(TEST_IMAGE_PATH);
// 1. Create the multipart/form-data payload
const formData = new FormData();
formData.append("key", API_KEY);
formData.append("id", TEST_ID);
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("edited_photo", testFile);
try {
// 2. Send the request
const response = await fetch(`${SERVER_URL}/upload`, {
method: "POST",
body: formData,
});
// 3. Log the response
const result = await response.json();
console.log(`Status: ${response.status}`);
console.log("Response:", result);
if (!response.ok) {
console.error("Upload test FAILED!");
} else {
console.log("Upload test PASSED!");
}
} catch (e) {
console.error("Error during /upload test:", e);
}
}
/**
* Test 2: GET /photos
* Fetches the public metadata.
*/
async function testGetPhotos() {
console.log("\n--- Testing GET /photos ---");
try {
const response = await fetch(`${SERVER_URL}/photos`);
const metadata = await response.json();
console.log(`Status: ${response.status}`);
console.log(`Found ${metadata.length} entries.`);
// Find our test upload
const ourEntry = metadata.find((e: any) => e.id === TEST_ID);
if (ourEntry) {
console.log("Found our test entry:", ourEntry);
if (ourEntry.owner_key) {
console.error("GET /photos test FAILED: 'owner_key' was exposed!");
} else {
console.log(
"GET /photos test PASSED: 'owner_key' was correctly removed.",
);
}
} else {
console.warn("Could not find our test entry in /photos response.");
}
} catch (e) {
console.error("Error during /photos test:", e);
}
}
// --- Run Tests ---
async function runTests() {
await testUpload();
await testGetPhotos();
}
runTests();