diff --git a/src/lib/components/Gallery.svelte b/src/lib/components/Gallery.svelte
index befb6d9..7d4d032 100644
--- a/src/lib/components/Gallery.svelte
+++ b/src/lib/components/Gallery.svelte
@@ -25,7 +25,7 @@
Title: {$strf(item.title)}
Description: {#if item.description}{$strf(item.description)}{:else}no description{/if}
-
+
{/each}
diff --git a/src/lib/data/language.js b/src/lib/data/language.js
index ed42688..ac28627 100644
--- a/src/lib/data/language.js
+++ b/src/lib/data/language.js
@@ -60,6 +60,7 @@ export const strf = derived(language, $language => {
* @param {...any} args
*/
function translate(translations, ...args) {
+ if (translations === undefined) return undefined;
if (typeof translations === 'string') return translations;
const str = translations[$language];
if (str === undefined) return translations.de;
diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts
index d3fc40b..9e3f51a 100644
--- a/src/lib/types.d.ts
+++ b/src/lib/types.d.ts
@@ -35,9 +35,12 @@ type Item = ItemMetadata;
type Album = AlbumMetadata & {
slug: string;
uriTimestamp?: string;
+ allowDownload?: boolean;
items: Item[];
};
type ApiError = {
error: string;
};
+
+type GetFileFunction = (zipName: string, entryName: string, keepOpen: T) => Promise;
diff --git a/src/lib/util/album.js b/src/lib/util/album.js
new file mode 100644
index 0000000..08a2eb9
--- /dev/null
+++ b/src/lib/util/album.js
@@ -0,0 +1,96 @@
+import { error } from "@sveltejs/kit";
+import StreamZip from "node-stream-zip";
+
+/** @param {string} zipName */
+async function _getZip(zipName) {
+ return new StreamZip.async({ file: `./zip/${zipName}` });
+}
+/** @param {import('node-stream-zip').StreamZipAsync} zip @param {string} entryName */
+async function _getFile(zip, entryName) {
+ return await zip.entryData(entryName);
+}
+/** @param {import('node-stream-zip').StreamZipAsync} zip */
+async function _getMetadata(zip) {
+ const metadataContent = await _getFile(zip, 'album.json');
+ return JSON.parse(metadataContent.toString());
+}
+/** @param {import('node-stream-zip').StreamZipAsync} zip @param {boolean} keepOpen */
+async function _close(zip, keepOpen) {
+ if (!keepOpen)
+ await zip.close();
+}
+
+/**
+ *
+ * @param {string} zipName
+ * @param {boolean} keepOpen
+ * @returns {Promise<{album: Album, zip: import('node-stream-zip').StreamZipAsync}>}
+ */
+export async function getMetadataOpen(zipName, keepOpen = true) {
+ const zip = await _getZip(zipName);
+ const album = await _getMetadata(zip);
+ await _close(zip, keepOpen);
+ return { album, zip };
+}
+
+/**
+ * @param {string} zipName
+ * @returns {Promise}
+ */
+export async function getMetadata(zipName) {
+ return (await getMetadataOpen(zipName, false)).album;
+}
+
+/**
+ * @param {string} zipName
+ * @param {string} entryName
+ * @param {boolean} keepOpen
+ * @returns {Promise<{album: Album, content: Buffer, zip: import('node-stream-zip').StreamZipAsync}>}
+ */
+export async function getMetadataAndFileOpen(zipName, entryName, keepOpen = true) {
+ const zip = await _getZip(zipName);
+ const album = await _getMetadata(zip);
+ const content = await _getFile(zip, entryName);
+ await _close(zip, keepOpen);
+ return { album, content, zip };
+}
+
+/**
+ *
+ * @param {string} zipName
+ * @param {string} entryName
+ * @returns {Promise<{album: Album, content: Buffer}>}
+ */
+export async function getMetadataAndFile(zipName, entryName) {
+ const { album, content } = await getMetadataAndFileOpen(zipName, entryName, false);
+ return { album, content };
+}
+
+/**
+ * @param {string} zipName
+ * @param {string} entryName
+ * @param {boolean} keepOpen
+ * @returns {Promise<{content: Buffer, zip: import('node-stream-zip').StreamZipAsync}>}
+ */
+export async function getFileOpen(zipName, entryName, keepOpen = true) {
+ const zip = await _getZip(zipName);
+ let content = null;
+ try {
+ console.log(`Getting ${entryName} from ${zipName}`);
+ content = await zip.entryData(entryName);
+ } catch (err) {
+ console.error(`${err.stack}`.replaceAll('/home/sveltekit', '.'));
+ throw error(404, `File ${entryName} not found.`);
+ }
+ await _close(zip, keepOpen);
+ return { content, zip };
+}
+
+/**
+ * @param {string} zipName
+ * @param {string} entryName
+ * @returns {Promise}
+ */
+export async function getFile(zipName, entryName) {
+ return (await getFileOpen(zipName, entryName, false)).content;
+}
diff --git a/src/lib/util/links.js b/src/lib/util/links.js
new file mode 100644
index 0000000..48982a3
--- /dev/null
+++ b/src/lib/util/links.js
@@ -0,0 +1,42 @@
+/**
+ * @param {string} str
+ * @returns {string}
+ */
+export function safe(str) {
+ return str.replace(/[^\w.-]/gi, '');
+}
+
+/**
+ * @param {string} str
+ * @returns {string}
+ */
+export function getFileName(str) {
+ return safe(str.split('/').pop() || '');
+}
+
+/**
+ * @param {string} str
+ * @returns {string}
+ */
+export function getFilePath(str) {
+ console.log(`getFilePath(${str})`);
+ return str.split('/').map(safe).join('/');
+}
+
+/**
+ * @param {any} params
+ * @returns {string}
+ */
+export function getZipName(params) {
+ const { slug, timestamp } = params;
+ return `${safe(slug)}${timestamp ? '-' + safe(timestamp) :''}.zip`;
+}
+
+/**
+ * @param {any} params
+ * @returns {string}
+ */
+export function getAlbumUri(params) {
+ const { slug, timestamp } = params;
+ return `/g/${safe(slug)}${timestamp ? '/' + safe(timestamp) :''}`;
+}
diff --git a/src/routes/g/[slug]/[[timestamp]]/+page.js b/src/routes/g/[slug]/[[timestamp]]/+page.js
index 5778046..4313885 100644
--- a/src/routes/g/[slug]/[[timestamp]]/+page.js
+++ b/src/routes/g/[slug]/[[timestamp]]/+page.js
@@ -1,26 +1,14 @@
-import { error } from '@sveltejs/kit';
-import StreamZip from 'node-stream-zip';
+import { getMetadataOpen } from '$lib/util/album';
+import { getAlbumUri, getZipName } from '$lib/util/links';
/** @type {import('./$types').PageLoad} */
export async function load({ params }) {
- const { slug, timestamp } = params;
- const cslug = slug.replace(/[^\w-]/gi, '');
- const ctimestamp = timestamp?.replace(/[^\w-]/gi, '');
-
- const zipFile = `./zip/${cslug}${ctimestamp ? '-' + ctimestamp :''}.zip`;
+ const {zip, album} = await getMetadataOpen(getZipName(params));
+ const entries = await zip.entries();
+ const base = getAlbumUri(params);
+ await zip.close();
- let entries = null;
- try {
- const zip = new StreamZip.async({ file: zipFile });
- entries = await zip.entries();
- await zip.close();
- } catch (err) {
- console.error(err);
- throw error(404, 'Not found');
- }
return {
- slug: params.slug,
- timestamp: params.timestamp,
- entries
+ album, entries, base
};
}
diff --git a/src/routes/g/[slug]/[[timestamp]]/+page.svelte b/src/routes/g/[slug]/[[timestamp]]/+page.svelte
index 57765ba..fb2c313 100644
--- a/src/routes/g/[slug]/[[timestamp]]/+page.svelte
+++ b/src/routes/g/[slug]/[[timestamp]]/+page.svelte
@@ -7,8 +7,8 @@
/** @type {import('./$types').PageData} */
export let data;
- const uriBase = `/s/apitest.php?slug=${data.slug}` + (data.timestamp ? `×tamp=${data.timestamp}` : '');
+ //const uriBase = `/s/apitest.php?slug=${data.slug}` + (data.timestamp ? `×tamp=${data.timestamp}` : '');
-
-
+
+
diff --git a/src/routes/g/[slug]/[[timestamp]]/--layout.js b/src/routes/g/[slug]/[[timestamp]]/--layout.js
new file mode 100644
index 0000000..bbfef8f
--- /dev/null
+++ b/src/routes/g/[slug]/[[timestamp]]/--layout.js
@@ -0,0 +1,29 @@
+import { error } from '@sveltejs/kit';
+import StreamZip from 'node-stream-zip';
+
+/** @type {import('./$types').LayoutLoad} */
+export async function load({ params }) {
+ const { slug, timestamp } = params;
+ const cslug = slug.replace(/[^\w-]/gi, '');
+ const ctimestamp = timestamp?.replace(/[^\w-]/gi, '');
+
+ const file = `./zip/${cslug}${ctimestamp ? '-' + ctimestamp :''}.zip`;
+ let entries = null;
+ try {
+ const zip = new StreamZip.async({ file });
+ entries = await zip.entries();
+ await zip.close();
+ } catch (err) {
+ console.error(err);
+ throw error(404, 'Not found');
+ }
+
+ console.log(`REQ: ${cslug}/${ctimestamp} @ ${file} with ${entries.length} entries:\n ${JSON.stringify(entries)}`);
+
+ return {
+ file,
+ cslug,
+ ctimestamp,
+ entries
+ };
+}
diff --git a/src/routes/g/[slug]/[[timestamp]]/d/+server.js b/src/routes/g/[slug]/[[timestamp]]/d/+server.js
deleted file mode 100644
index 56529e6..0000000
--- a/src/routes/g/[slug]/[[timestamp]]/d/+server.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { error } from '@sveltejs/kit';
-
-/** @type {import('./$types').RequestHandler} */
-export function GET({ url }) {
- const min = Number(url.searchParams.get('min') ?? '0');
- const max = Number(url.searchParams.get('max') ?? '1');
-
- const d = max - min;
-
- if (isNaN(d) || d < 0) {
- throw error(400, 'min and max must be numbers, and min must be less than max');
- }
-
- const random = min + Math.random() * d;
-
- return new Response(String(random));
-}
diff --git a/src/routes/g/[slug]/[[timestamp]]/d/.download b/src/routes/g/[slug]/[[timestamp]]/d/.download
deleted file mode 100644
index e69de29..0000000
diff --git a/src/routes/g/[slug]/[[timestamp]]/download/+server.js b/src/routes/g/[slug]/[[timestamp]]/download/+server.js
new file mode 100644
index 0000000..2547d86
--- /dev/null
+++ b/src/routes/g/[slug]/[[timestamp]]/download/+server.js
@@ -0,0 +1,34 @@
+import { getMetadata } from '$lib/util/album';
+import { getZipName } from '$lib/util/links';
+import { error } from '@sveltejs/kit';
+import fs from 'node:fs/promises';
+
+/** @type {import('./$types').RequestHandler} */
+export async function GET({ params, url }) {
+ try {
+ const zipName = getZipName(params);
+ const album = await getMetadata(zipName);
+ const allowDownload = album.allowDownload === false ? false : true;
+ if (!allowDownload) {
+ throw error(403, 'Forbidden');
+ }
+ if (url.searchParams.get('type') === 'json') {
+ return new Response(JSON.stringify(album), {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Content-Disposition': 'inline'
+ }
+ });
+ } else {
+ const content = await fs.readFile(`./zip/${zipName}`);
+ return new Response(content, {
+ headers: {
+ 'Content-Type': 'application/zip',
+ 'Content-Disposition': `attachment; filename="${zipName}"`
+ }
+ });
+ }
+ } catch (err) {
+ throw error(404, 'Not found');
+ }
+}
diff --git a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/+page.js b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/+page.js
index e69de29..e49ccc5 100644
--- a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/+page.js
+++ b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/+page.js
@@ -0,0 +1,8 @@
+import { getFileName } from '$lib/util/links.js';
+
+/** @type {import('./$types').PageLoad} */
+export async function load({ params }) {
+ return {
+ filename: getFileName(params.item)
+ }
+};
diff --git a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/+page.svelte b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/+page.svelte
index e69de29..1656d36 100644
--- a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/+page.svelte
+++ b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/+page.svelte
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/d/+server.js b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/d/+server.js
deleted file mode 100644
index 88aad42..0000000
--- a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/d/+server.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { error } from '@sveltejs/kit';
-
-/** @type {import('./$types').RequestHandler} */
-export function GET({ url }) {
- const noAttachment = url.searchParams.has('r');
-
-
-
- const min = Number(url.searchParams.get('min') ?? '0');
- const max = Number(url.searchParams.get('max') ?? '1');
-
- const d = max - min;
-
- if (isNaN(d) || d < 0) {
- throw error(400, 'min and max must be numbers, and min must be less than max');
- }
-
- const random = min + Math.random() * d;
-
- return new Response(String(random), {
- headers: {
- 'Content-Type': 'text/plain',
- 'Content-Disposition': noAttachment ? 'inline' : 'attachment'
- }
- });
-}
diff --git a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/d/.download b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/d/.download
deleted file mode 100644
index e69de29..0000000
diff --git a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/download/+server.js b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/download/+server.js
new file mode 100644
index 0000000..d5c2dc9
--- /dev/null
+++ b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/download/+server.js
@@ -0,0 +1,34 @@
+import { getMetadataAndFile } from '$lib/util/album';
+import { getZipName, getFileName, getFilePath } from '$lib/util/links';
+import { error } from '@sveltejs/kit';
+
+/** @type {import('./$types').RequestHandler} */
+export async function GET({ params }) {
+ try {
+ const {album, content} = await getMetadataAndFile(getZipName(params), getFilePath(params.item));
+ const allowDownload = album.allowDownload === false ? false : true;
+ if (!allowDownload) {
+ throw error(403, 'Forbidden');
+ }
+ const filename = getFileName(params.item);
+ const ext = filename.split('.').pop() || 'any';
+ const mimes = new Map([
+ ['jpg', 'image/jpeg'],
+ ['png', 'image/png'],
+ ['webp', 'image/webp'],
+ ['avif', 'image/avif'],
+ ['mp4', 'video/mp4'],
+ ['any', 'application/octet-stream']
+ ]);
+ const mime = mimes.get(ext) || 'application/octet-stream';
+ return new Response(content, {
+ headers: {
+ 'Content-Type': mime,
+ 'Content-Disposition': `attachment; filename="${filename}"`
+ }
+ });
+ } catch (err) {
+ console.error(err);
+ throw error(404, 'Not found');
+ }
+}
diff --git a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/+server.js b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/+server.js
deleted file mode 100644
index 56529e6..0000000
--- a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/+server.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { error } from '@sveltejs/kit';
-
-/** @type {import('./$types').RequestHandler} */
-export function GET({ url }) {
- const min = Number(url.searchParams.get('min') ?? '0');
- const max = Number(url.searchParams.get('max') ?? '1');
-
- const d = max - min;
-
- if (isNaN(d) || d < 0) {
- throw error(400, 'min and max must be numbers, and min must be less than max');
- }
-
- const random = min + Math.random() * d;
-
- return new Response(String(random));
-}
diff --git a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/[[width]]/+server.js b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/[[width]]/+server.js
new file mode 100644
index 0000000..1e3b537
--- /dev/null
+++ b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/[[width]]/+server.js
@@ -0,0 +1,35 @@
+import { getFile } from '$lib/util/album';
+import { getFilePath, getZipName } from '$lib/util/links';
+import sharp from 'sharp';
+
+/** @type {import('./$types').RequestHandler} */
+export async function GET({ params }) {
+ let thumbnail = null;
+ console.log(`Getting thumbnail for ${params}`);
+ try {
+ const content = await getFile(getZipName(params), getFilePath(params.item));
+ let width = 400;
+ if (params.width) {
+ if (params.width === 'full') {
+ thumbnail = await sharp(content).webp({ quality: 90 }).toBuffer();
+ } else if (params.width === 's') {
+ width = 400;
+ } else if (params.width === 'm') {
+ width = 800;
+ } else if (params.width === 'l') {
+ width = 1200;
+ } else if (!Number.isNaN(Number(params.width))) {
+ width = Number(width);
+ }
+ }
+ thumbnail = thumbnail || await sharp(content).resize(width).webp({ quality: 90 }).toBuffer();
+ } catch (err) {
+ console.error(`${err.stack}`.replaceAll('/home/sveltekit', '.'));
+ }
+ return new Response(thumbnail, {
+ headers: {
+ 'Content-Type': 'image/webp',
+ 'Content-Disposition': 'inline'
+ }
+ });
+}
diff --git a/src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/.thumbnail b/src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/[[width]]/.thumbnail
similarity index 100%
rename from src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/.thumbnail
rename to src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/[[width]]/.thumbnail