mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
103 lines
2.7 KiB
TypeScript
103 lines
2.7 KiB
TypeScript
import { HTTPException } from '@hono/hono/http-exception';
|
|
import { logi } from '@soapbox/logi';
|
|
import { crypto } from '@std/crypto';
|
|
import { encodeHex } from '@std/encoding/hex';
|
|
import { encode } from 'blurhash';
|
|
import sharp from 'sharp';
|
|
|
|
import { AppContext } from '@/app.ts';
|
|
import { DittoUpload, dittoUploads } from '@/DittoUploads.ts';
|
|
import { errorJson } from '@/utils/log.ts';
|
|
|
|
interface FileMeta {
|
|
pubkey: string;
|
|
description?: string;
|
|
}
|
|
|
|
/** Upload a file, track it in the database, and return the resulting media object. */
|
|
export async function uploadFile(
|
|
c: AppContext,
|
|
file: File,
|
|
meta: FileMeta,
|
|
signal?: AbortSignal,
|
|
): Promise<DittoUpload> {
|
|
const { conf, uploader } = c.var;
|
|
|
|
if (!uploader) {
|
|
throw new HTTPException(500, {
|
|
res: c.json({ error: 'No uploader configured.' }),
|
|
});
|
|
}
|
|
|
|
const { pubkey, description } = meta;
|
|
|
|
if (file.size > conf.maxUploadSize) {
|
|
throw new Error('File size is too large.');
|
|
}
|
|
|
|
const tags = await uploader.upload(file, { signal });
|
|
const url = tags[0][1];
|
|
|
|
if (description) {
|
|
tags.push(['alt', description]);
|
|
}
|
|
|
|
const x = tags.find(([key]) => key === 'x')?.[1];
|
|
const m = tags.find(([key]) => key === 'm')?.[1];
|
|
const dim = tags.find(([key]) => key === 'dim')?.[1];
|
|
const size = tags.find(([key]) => key === 'size')?.[1];
|
|
const blurhash = tags.find(([key]) => key === 'blurhash')?.[1];
|
|
|
|
if (!x) {
|
|
const sha256 = encodeHex(await crypto.subtle.digest('SHA-256', file.stream()));
|
|
tags.push(['x', sha256]);
|
|
}
|
|
|
|
if (!m) {
|
|
tags.push(['m', file.type]);
|
|
}
|
|
|
|
if (!size) {
|
|
tags.push(['size', file.size.toString()]);
|
|
}
|
|
|
|
// If the uploader didn't already, try to get a blurhash and media dimensions.
|
|
// This requires `MEDIA_ANALYZE=true` to be configured because it comes with security tradeoffs.
|
|
if (conf.mediaAnalyze && (!blurhash || !dim)) {
|
|
try {
|
|
const bytes = await new Response(file.stream()).bytes();
|
|
const img = sharp(bytes);
|
|
|
|
const { width, height } = await img.metadata();
|
|
|
|
if (!dim && (width && height)) {
|
|
tags.push(['dim', `${width}x${height}`]);
|
|
}
|
|
|
|
if (!blurhash && (width && height)) {
|
|
const pixels = await img
|
|
.raw()
|
|
.ensureAlpha()
|
|
.toBuffer({ resolveWithObject: false })
|
|
.then((buffer) => new Uint8ClampedArray(buffer));
|
|
|
|
const blurhash = encode(pixels, width, height, 4, 4);
|
|
tags.push(['blurhash', blurhash]);
|
|
}
|
|
} catch (e) {
|
|
logi({ level: 'error', ns: 'ditto.upload.analyze', error: errorJson(e) });
|
|
}
|
|
}
|
|
|
|
const upload = {
|
|
id: crypto.randomUUID(),
|
|
url,
|
|
tags,
|
|
pubkey,
|
|
uploadedAt: new Date(),
|
|
};
|
|
|
|
dittoUploads.set(upload.id, upload);
|
|
|
|
return upload;
|
|
}
|