From d12462222fa8cc6118e1506435c1eedc71b855c6 Mon Sep 17 00:00:00 2001 From: Luca Bosin Date: Thu, 17 Aug 2023 22:33:01 +0200 Subject: [PATCH] Add image loading --- src/lib/components/Gallery.svelte | 2 +- src/lib/data/language.js | 1 + src/lib/types.d.ts | 3 + src/lib/util/album.js | 96 +++++++++++++++++++ src/lib/util/links.js | 42 ++++++++ src/routes/g/[slug]/[[timestamp]]/+page.js | 26 ++--- .../g/[slug]/[[timestamp]]/+page.svelte | 6 +- src/routes/g/[slug]/[[timestamp]]/--layout.js | 29 ++++++ .../g/[slug]/[[timestamp]]/d/+server.js | 17 ---- src/routes/g/[slug]/[[timestamp]]/d/.download | 0 .../[slug]/[[timestamp]]/download/+server.js | 34 +++++++ .../[slug]/[[timestamp]]/i/[...item]/+page.js | 8 ++ .../[[timestamp]]/i/[...item]/+page.svelte | 6 ++ .../[[timestamp]]/i/[...item]/d/+server.js | 26 ----- .../[[timestamp]]/i/[...item]/d/.download | 0 .../i/[...item]/download/+server.js | 34 +++++++ .../[[timestamp]]/i/[...item]/t/+server.js | 17 ---- .../i/[...item]/t/[[width]]/+server.js | 35 +++++++ .../i/[...item]/t/{ => [[width]]}/.thumbnail | 0 19 files changed, 299 insertions(+), 83 deletions(-) create mode 100644 src/lib/util/album.js create mode 100644 src/lib/util/links.js create mode 100644 src/routes/g/[slug]/[[timestamp]]/--layout.js delete mode 100644 src/routes/g/[slug]/[[timestamp]]/d/+server.js delete mode 100644 src/routes/g/[slug]/[[timestamp]]/d/.download create mode 100644 src/routes/g/[slug]/[[timestamp]]/download/+server.js delete mode 100644 src/routes/g/[slug]/[[timestamp]]/i/[...item]/d/+server.js delete mode 100644 src/routes/g/[slug]/[[timestamp]]/i/[...item]/d/.download create mode 100644 src/routes/g/[slug]/[[timestamp]]/i/[...item]/download/+server.js delete mode 100644 src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/+server.js create mode 100644 src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/[[width]]/+server.js rename src/routes/g/[slug]/[[timestamp]]/i/[...item]/t/{ => [[width]]}/.thumbnail (100%) 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}

- {$strf(item.title)} + {$strf(item.title) {/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