src/routes/admin/+page.server.ts 5.9 K raw
1
import type { Actions, PageServerLoad } from "./$types";
2
import { fail, redirect, isRedirect } from "@sveltejs/kit";
3
4
const R2_BASE_URL = "https://r2.steve.photo";
5
6
type PhotoRow = {
7
	id: number;
8
	slug: string;
9
	title: string;
10
	date: string;
11
	image_key: string;
12
	thumb_key: string;
13
};
14
15
export const load: PageServerLoad = async ({ platform }) => {
16
	const db = platform?.env?.DB;
17
	if (!db) {
18
		return { photos: [] };
19
	}
20
21
	const result = await db
22
		.prepare(
23
			"SELECT id, slug, title, date, image_key, thumb_key FROM photos ORDER BY date DESC",
24
		)
25
		.all<PhotoRow>();
26
27
	const photos = result.results.map((row: PhotoRow) => ({
28
		id: row.id,
29
		slug: row.slug,
30
		title: row.title,
31
		date: row.date,
32
		thumb: `${R2_BASE_URL}/${row.thumb_key}`,
33
	}));
34
35
	return { photos };
36
};
37
38
export const actions: Actions = {
39
	upload: async ({ request, platform, locals }) => {
40
		if (!locals.user?.authenticated) {
41
			return fail(401, { error: "Unauthorized" });
42
		}
43
44
		const db = platform?.env?.DB;
45
		const bucket = platform?.env?.PHOTOS;
46
47
		if (!db || !bucket) {
48
			return fail(500, { error: "Server configuration error" });
49
		}
50
51
		const formData = await request.formData();
52
		const file = formData.get("file") as File | null;
53
		const thumbnail = formData.get("thumbnail") as File | null;
54
		const title = formData.get("title") as string;
55
		const date = formData.get("date") as string;
56
		const camera = formData.get("camera") as string;
57
		const lens = formData.get("lens") as string;
58
		const aperture = formData.get("aperture") as string;
59
		const exposure = formData.get("exposure") as string;
60
		const focalLength = formData.get("focalLength") as string;
61
		const iso = formData.get("iso") as string;
62
		const make = formData.get("make") as string;
63
		const blurData = formData.get("blur_data") as string;
64
65
		if (!file || !title || !date) {
66
			return fail(400, { error: "File, title, and date are required" });
67
		}
68
69
		// Generate slug from title
70
		const slug = title
71
			.toLowerCase()
72
			.replace(/[^a-z0-9]+/g, "-")
73
			.replace(/(^-|-$)/g, "");
74
75
		// Check if slug already exists
76
		const existing = await db
77
			.prepare("SELECT id FROM photos WHERE slug = ?")
78
			.bind(slug)
79
			.first();
80
		if (existing) {
81
			return fail(400, { error: "A photo with this title already exists" });
82
		}
83
84
		// Get file extension
85
		const ext = file.name.split(".").pop()?.toLowerCase() || "jpg";
86
		const imageKey = `${slug}.${ext}`;
87
		const thumbKey = `${slug}-thumb.jpg`;
88
89
		try {
90
			// Upload original image to R2
91
			const fileBuffer = await file.arrayBuffer();
92
			await bucket.put(imageKey, fileBuffer, {
93
				httpMetadata: {
94
					contentType: file.type,
95
				},
96
			});
97
98
			// Upload thumbnail to R2
99
			if (thumbnail) {
100
				const thumbBuffer = await thumbnail.arrayBuffer();
101
				await bucket.put(thumbKey, thumbBuffer, {
102
					httpMetadata: {
103
						contentType: "image/jpeg",
104
					},
105
				});
106
			} else {
107
				// If no thumbnail provided, use original as thumbnail
108
				await bucket.put(thumbKey, fileBuffer, {
109
					httpMetadata: {
110
						contentType: file.type,
111
					},
112
				});
113
			}
114
115
			// Insert into database
116
			await db
117
				.prepare(
118
					`INSERT INTO photos (slug, title, date, image_key, thumb_key, camera, lens, aperture, exposure, focal_length, iso, make, blur_data)
119
					 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
120
				)
121
				.bind(
122
					slug,
123
					title,
124
					date,
125
					imageKey,
126
					thumbKey,
127
					camera || null,
128
					lens || null,
129
					aperture || null,
130
					exposure || null,
131
					focalLength || null,
132
					iso || null,
133
					make || null,
134
					blurData || null,
135
				)
136
				.run();
137
138
			return { success: true };
139
		} catch (err) {
140
			if (isRedirect(err)) {
141
				throw err; // Re-throw redirects
142
			}
143
			const errorMessage = err instanceof Error ? err.message : String(err);
144
			console.error("Upload error:", errorMessage, err);
145
			return fail(500, { error: `Failed to upload photo: ${errorMessage}` });
146
		}
147
	},
148
149
	edit: async ({ request, platform, locals }) => {
150
		if (!locals.user?.authenticated) {
151
			return fail(401, { error: "Unauthorized" });
152
		}
153
154
		const db = platform?.env?.DB;
155
		if (!db) {
156
			return fail(500, { error: "Server configuration error" });
157
		}
158
159
		const formData = await request.formData();
160
		const id = formData.get("id") as string;
161
		const title = formData.get("title") as string;
162
163
		if (!id || !title) {
164
			return fail(400, { error: "ID and title are required" });
165
		}
166
167
		try {
168
			await db
169
				.prepare("UPDATE photos SET title = ? WHERE id = ?")
170
				.bind(title, parseInt(id, 10))
171
				.run();
172
173
			return { success: true };
174
		} catch (err) {
175
			const errorMessage = err instanceof Error ? err.message : String(err);
176
			console.error("Edit error:", errorMessage);
177
			return fail(500, { error: `Failed to update photo: ${errorMessage}` });
178
		}
179
	},
180
181
	delete: async ({ request, platform, locals }) => {
182
		if (!locals.user?.authenticated) {
183
			return fail(401, { error: "Unauthorized" });
184
		}
185
186
		const db = platform?.env?.DB;
187
		const bucket = platform?.env?.PHOTOS;
188
189
		if (!db || !bucket) {
190
			return fail(500, { error: "Server configuration error" });
191
		}
192
193
		const formData = await request.formData();
194
		const id = formData.get("id") as string;
195
196
		if (!id) {
197
			return fail(400, { error: "ID is required" });
198
		}
199
200
		try {
201
			// Get photo details first to delete from R2
202
			const photo = await db
203
				.prepare("SELECT image_key, thumb_key FROM photos WHERE id = ?")
204
				.bind(parseInt(id, 10))
205
				.first<{ image_key: string; thumb_key: string }>();
206
207
			if (!photo) {
208
				return fail(404, { error: "Photo not found" });
209
			}
210
211
			// Delete from R2
212
			await bucket.delete(photo.image_key);
213
			await bucket.delete(photo.thumb_key);
214
215
			// Delete from database
216
			await db
217
				.prepare("DELETE FROM photos WHERE id = ?")
218
				.bind(parseInt(id, 10))
219
				.run();
220
221
			return { success: true };
222
		} catch (err) {
223
			const errorMessage = err instanceof Error ? err.message : String(err);
224
			console.error("Delete error:", errorMessage);
225
			return fail(500, { error: `Failed to delete photo: ${errorMessage}` });
226
		}
227
	},
228
};