diff --git a/.env.local.example b/.env.local.example index 7c2a44a4..4ed889f0 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,7 +1,7 @@ # every field in here is optional except, CORE_SECRET and CORE_DATABASE_URL. # if CORE_SECRET is still "changethis" then zipline will exit and tell you to change it. -# if using s3/supabase make sure to uncomment or comment out the correct lines needed. +# if using s3 make sure to uncomment or comment out the correct lines needed. CORE_RETURN_HTTPS=true CORE_SECRET="changethis" @@ -27,13 +27,6 @@ DATASOURCE_LOCAL_DIRECTORY=./uploads # DATASOURCE_S3_FORCE_S3_PATH=false # DATASOURCE_S3_USE_SSL=false -# or supabase -# DATASOURCE_TYPE=supabase -# DATASOURCE_SUPABASE_KEY=xxx -# remember: no leading slash -# DATASOURCE_SUPABASE_URL=https://something.supabase.co -# DATASOURCE_SUPABASE_BUCKET=zipline - UPLOADER_DEFAULT_FORMAT=RANDOM UPLOADER_ROUTE=/u UPLOADER_LENGTH=6 diff --git a/.gitignore b/.gitignore index 2b54b9fa..cff390b4 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ yarn-debug.log* yarn-error.log* # local env files +.env .env.local .env.development.local .env.test.local diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3d158e2c..4fb71802 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,4 +1,3 @@ -version: '3' services: postgres: image: postgres:15 diff --git a/docker-compose.yml b/docker-compose.yml index d42369c6..c3b358fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: postgres: image: postgres:15 diff --git a/src/components/pages/Users/UserFiles.tsx b/src/components/pages/Users/UserFiles.tsx index 0b4cc77a..d5d63848 100644 --- a/src/components/pages/Users/UserFiles.tsx +++ b/src/components/pages/Users/UserFiles.tsx @@ -1,5 +1,5 @@ import { ActionIcon, Button, Center, Group, SimpleGrid, Title } from '@mantine/core'; -import { File } from '@prisma/client'; +import type { File } from '@prisma/client'; import { IconArrowLeft, IconFile } from '@tabler/icons-react'; import FileComponent from 'components/File'; import MutedText from 'components/MutedText'; diff --git a/src/lib/config/Config.ts b/src/lib/config/Config.ts index 915962ac..bb055e04 100644 --- a/src/lib/config/Config.ts +++ b/src/lib/config/Config.ts @@ -20,10 +20,9 @@ export interface ConfigCompression { } export interface ConfigDatasource { - type: 'local' | 's3' | 'supabase'; + type: 'local' | 's3'; local: ConfigLocalDatasource; s3?: ConfigS3Datasource; - supabase?: ConfigSupabaseDatasource; } export interface ConfigLocalDatasource { @@ -41,12 +40,6 @@ export interface ConfigS3Datasource { region?: string; } -export interface ConfigSupabaseDatasource { - url: string; - key: string; - bucket: string; -} - export interface ConfigUploader { default_format: string; route: string; diff --git a/src/lib/config/readConfig.ts b/src/lib/config/readConfig.ts index cc0f42a5..89aa3e22 100644 --- a/src/lib/config/readConfig.ts +++ b/src/lib/config/readConfig.ts @@ -85,10 +85,6 @@ export default function readConfig() { map('DATASOURCE_S3_REGION', 'string', 'datasource.s3.region'), map('DATASOURCE_S3_USE_SSL', 'boolean', 'datasource.s3.use_ssl'), - map('DATASOURCE_SUPABASE_URL', 'string', 'datasource.supabase.url'), - map('DATASOURCE_SUPABASE_KEY', 'string', 'datasource.supabase.key'), - map('DATASOURCE_SUPABASE_BUCKET', 'string', 'datasource.supabase.bucket'), - map('UPLOADER_DEFAULT_FORMAT', 'string', 'uploader.default_format'), map('UPLOADER_ROUTE', 'string', 'uploader.route'), map('UPLOADER_LENGTH', 'number', 'uploader.length'), diff --git a/src/lib/config/validateConfig.ts b/src/lib/config/validateConfig.ts index 9d7f05a6..e4fb35ba 100644 --- a/src/lib/config/validateConfig.ts +++ b/src/lib/config/validateConfig.ts @@ -51,7 +51,7 @@ const validator = s.object({ }), datasource: s .object({ - type: s.enum('local', 's3', 'supabase').default('local'), + type: s.enum('local', 's3').default('local'), local: s .object({ directory: s.string.default(resolve('./uploads')).transform((v) => resolve(v)), @@ -69,11 +69,6 @@ const validator = s.object({ region: s.string.default('us-east-1'), use_ssl: s.boolean.default(false), }).optional, - supabase: s.object({ - url: s.string, - key: s.string, - bucket: s.string, - }).optional, }) .default({ type: 'local', @@ -253,43 +248,29 @@ export default function validate(config): Config { logger.debug(`Attemping to validate ${JSON.stringify(config)}`); const validated = validator.parse(config); logger.debug(`Recieved config: ${JSON.stringify(validated)}`); - switch (validated.datasource.type) { - case 's3': { - const errors = []; - if (!validated.datasource.s3.access_key_id) - errors.push('datasource.s3.access_key_id is a required field'); - if (!validated.datasource.s3.secret_access_key) - errors.push('datasource.s3.secret_access_key is a required field'); - if (!validated.datasource.s3.bucket) errors.push('datasource.s3.bucket is a required field'); - if (!validated.datasource.s3.endpoint) errors.push('datasource.s3.endpoint is a required field'); - if (errors.length) throw { errors }; - break; - } - case 'supabase': { - const errors = []; - if (!validated.datasource.supabase.key) errors.push('datasource.supabase.key is a required field'); - if (!validated.datasource.supabase.url) errors.push('datasource.supabase.url is a required field'); - if (!validated.datasource.supabase.bucket) - errors.push('datasource.supabase.bucket is a required field'); - if (errors.length) throw { errors }; - - break; - } + if (validated.datasource.type === 's3') { + const errors = []; + if (!validated.datasource.s3.access_key_id) + errors.push('datasource.s3.access_key_id is a required field'); + if (!validated.datasource.s3.secret_access_key) + errors.push('datasource.s3.secret_access_key is a required field'); + if (!validated.datasource.s3.bucket) errors.push('datasource.s3.bucket is a required field'); + if (!validated.datasource.s3.endpoint) errors.push('datasource.s3.endpoint is a required field'); + if (errors.length) throw { errors }; } - const reserved = ['/view', '/dashboard', '/code', '/folder', '/api', '/auth', '/r']; - if (reserved.some((r) => validated.uploader.route.startsWith(r))) { + const reserved = new RegExp(/^\/(view|code|folder|auth|r)(\/\S*)?$|^\/(api|dashboard)(\/\S*)*/); + if (reserved.exec(validated.uploader.route)) throw { errors: [`The uploader route cannot be ${validated.uploader.route}, this is a reserved route.`], show: true, }; - } else if (reserved.some((r) => validated.urls.route.startsWith(r))) { + if (reserved.exec(validated.urls.route)) throw { errors: [`The urls route cannot be ${validated.urls.route}, this is a reserved route.`], show: true, }; - } return validated as unknown as Config; } catch (e) { diff --git a/src/lib/datasource.ts b/src/lib/datasource.ts index 8f621a2e..55d917e3 100644 --- a/src/lib/datasource.ts +++ b/src/lib/datasource.ts @@ -1,5 +1,5 @@ import config from './config'; -import { Datasource, Local, S3, Supabase } from './datasources'; +import { Datasource, Local, S3 } from './datasources'; import Logger from './logger'; const logger = Logger.get('datasource'); @@ -14,10 +14,6 @@ if (!global.datasource) { global.datasource = new Local(config.datasource.local.directory); logger.info(`using Local(${config.datasource.local.directory}) datasource`); break; - case 'supabase': - global.datasource = new Supabase(config.datasource.supabase); - logger.info(`using Supabase(${config.datasource.supabase.bucket}) datasource`); - break; default: throw new Error('Invalid datasource type'); } diff --git a/src/lib/datasources/Datasource.ts b/src/lib/datasources/Datasource.ts index 594f3b1a..51c27d0e 100644 --- a/src/lib/datasources/Datasource.ts +++ b/src/lib/datasources/Datasource.ts @@ -7,6 +7,7 @@ export abstract class Datasource { public abstract delete(file: string): Promise; public abstract clear(): Promise; public abstract size(file: string): Promise; - public abstract get(file: string, start?: number, end?: number): Readable | Promise; + public abstract get(file: string): Readable | Promise; public abstract fullSize(): Promise; + public abstract range(file: string, start: number, end: number): Promise; } diff --git a/src/lib/datasources/Local.ts b/src/lib/datasources/Local.ts index 15b2e4c1..4f3506a0 100644 --- a/src/lib/datasources/Local.ts +++ b/src/lib/datasources/Local.ts @@ -11,7 +11,7 @@ export class Local extends Datasource { } public async save(file: string, data: Buffer): Promise { - await writeFile(join(this.path, file), data); + await writeFile(join(this.path, file), Uint8Array.from(data)); } public async delete(file: string): Promise { @@ -26,12 +26,12 @@ export class Local extends Datasource { } } - public get(file: string, start: number = 0, end: number = Infinity): ReadStream { + public get(file: string): ReadStream { const full = join(this.path, file); if (!existsSync(full)) return null; try { - return createReadStream(full, { start, end }); + return createReadStream(full); } catch (e) { return null; } @@ -56,4 +56,11 @@ export class Local extends Datasource { return size; } + + public async range(file: string, start: number, end: number): Promise { + const path = join(this.path, file); + const readStream = createReadStream(path, { start, end }); + + return readStream; + } } diff --git a/src/lib/datasources/S3.ts b/src/lib/datasources/S3.ts index 21a998fc..dfc441db 100644 --- a/src/lib/datasources/S3.ts +++ b/src/lib/datasources/S3.ts @@ -1,5 +1,5 @@ import { Datasource } from '.'; -import { Readable } from 'stream'; +import { PassThrough, Readable } from 'stream'; import { ConfigS3Datasource } from 'lib/config/Config'; import { BucketItemStat, Client } from 'minio'; @@ -24,7 +24,8 @@ export class S3 extends Datasource { await this.s3.putObject( this.config.bucket, file, - data, + new PassThrough().end(data), + data.byteLength, options ? { 'Content-Type': options.type } : undefined, ); } @@ -45,28 +46,12 @@ export class S3 extends Datasource { }); } - public get(file: string, start: number = 0, end: number = Infinity): Promise { - if (start === 0 && end === Infinity) { - return new Promise((res) => { - this.s3.getObject(this.config.bucket, file, (err, stream) => { - if (err) res(null); - else res(stream); - }); - }); - } - + public get(file: string): Promise { return new Promise((res) => { - this.s3.getPartialObject( - this.config.bucket, - file, - start, - // undefined means to read the rest of the file from the start (offset) - end === Infinity ? undefined : end, - (err, stream) => { - if (err) res(null); - else res(stream); - }, - ); + this.s3.getObject(this.config.bucket, file, (err, stream) => { + if (err) res(null); + else res(stream); + }); }); } @@ -96,4 +81,15 @@ export class S3 extends Datasource { }); }); } + + public async range(file: string, start: number, end: number): Promise { + return new Promise((res) => { + this.s3.getPartialObject(this.config.bucket, file, start, end, (err, stream) => { + if (err) { + console.log(err); + res(null); + } else res(stream); + }); + }); + } } diff --git a/src/lib/datasources/Supabase.ts b/src/lib/datasources/Supabase.ts deleted file mode 100644 index 3f9ec762..00000000 --- a/src/lib/datasources/Supabase.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Datasource } from '.'; -import { ConfigSupabaseDatasource } from 'lib/config/Config'; -import { guess } from 'lib/mimes'; -import Logger from 'lib/logger'; -import { Readable } from 'stream'; - -export class Supabase extends Datasource { - public name = 'Supabase'; - public logger: Logger = Logger.get('datasource::supabase'); - - public constructor(public config: ConfigSupabaseDatasource) { - super(); - } - - public async save(file: string, data: Buffer): Promise { - const mimetype = await guess(file.split('.').pop()); - - const r = await fetch(`${this.config.url}/storage/v1/object/${this.config.bucket}/${file}`, { - method: 'POST', - headers: { - Authorization: `Bearer ${this.config.key}`, - 'Content-Type': mimetype, - }, - body: data, - }); - - const j = await r.json(); - if (j.error) this.logger.error(`${j.error}: ${j.message}`); - } - - public async delete(file: string): Promise { - await fetch(`${this.config.url}/storage/v1/object/${this.config.bucket}/${file}`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${this.config.key}`, - }, - }); - } - - public async clear(): Promise { - try { - const resp = await fetch(`${this.config.url}/storage/v1/object/list/${this.config.bucket}`, { - method: 'POST', - headers: { - Authorization: `Bearer ${this.config.key}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - prefix: '', - }), - }); - const objs = await resp.json(); - if (objs.error) throw new Error(`${objs.error}: ${objs.message}`); - - const res = await fetch(`${this.config.url}/storage/v1/object/${this.config.bucket}`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${this.config.key}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - prefixes: objs.map((x: { name: string }) => x.name), - }), - }); - - const j = await res.json(); - if (j.error) throw new Error(`${j.error}: ${j.message}`); - - return; - } catch (e) { - this.logger.error(e); - } - } - - public async get(file: string, start: number = 0, end: number = Infinity): Promise { - // get a readable stream from the request - const r = await fetch(`${this.config.url}/storage/v1/object/${this.config.bucket}/${file}`, { - method: 'GET', - headers: { - Authorization: `Bearer ${this.config.key}`, - Range: `bytes=${start}-${end === Infinity ? '' : end}`, - }, - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return Readable.fromWeb(r.body as any); - } - - public size(file: string): Promise { - return new Promise(async (res) => { - fetch(`${this.config.url}/storage/v1/object/list/${this.config.bucket}`, { - method: 'POST', - headers: { - Authorization: `Bearer ${this.config.key}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - prefix: '', - search: file, - }), - }) - .then((r) => r.json()) - .then((j) => { - if (j.error) { - this.logger.error(`${j.error}: ${j.message}`); - res(null); - } - - if (j.length === 0) { - res(null); - } else { - res(j[0].metadata.size); - } - }); - }); - } - - public async fullSize(): Promise { - return new Promise((res) => { - fetch(`${this.config.url}/storage/v1/object/list/${this.config.bucket}`, { - method: 'POST', - headers: { - Authorization: `Bearer ${this.config.key}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - prefix: '', - }), - }) - .then((r) => r.json()) - .then((j) => { - if (j.error) { - this.logger.error(`${j.error}: ${j.message}`); - res(0); - } - - res(j.reduce((a, b) => a + b.metadata.size, 0)); - }); - }); - } -} diff --git a/src/lib/datasources/index.ts b/src/lib/datasources/index.ts index 07582f39..0e9f033c 100644 --- a/src/lib/datasources/index.ts +++ b/src/lib/datasources/index.ts @@ -1,4 +1,3 @@ export { Datasource } from './Datasource'; export { Local } from './Local'; export { S3 } from './S3'; -export { Supabase } from './Supabase'; diff --git a/src/lib/discord.ts b/src/lib/discord.ts index c2b71e67..c8ce6e2f 100644 --- a/src/lib/discord.ts +++ b/src/lib/discord.ts @@ -1,4 +1,4 @@ -import { File, Url, User } from '@prisma/client'; +import type { File, Url, User } from '@prisma/client'; import config from 'lib/config'; import { ConfigDiscordContent } from 'config/Config'; import Logger from 'lib/logger'; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index ed13334a..97f46304 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,7 +1,11 @@ import { PrismaClient } from '@prisma/client'; +import 'lib/config'; if (!global.prisma) { - if (!process.env.ZIPLINE_DOCKER_BUILD) global.prisma = new PrismaClient(); + if (!process.env.ZIPLINE_DOCKER_BUILD) { + process.env.DATABASE_URL = config.core.database_url; + global.prisma = new PrismaClient(); + } } export default global.prisma as PrismaClient; diff --git a/src/lib/util.ts b/src/lib/util.ts index 3fcaddd7..a6d9f13f 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,4 +1,4 @@ -import { InvisibleFile, InvisibleUrl } from '@prisma/client'; +import type { InvisibleFile, InvisibleUrl } from '@prisma/client'; import { hash, verify } from 'argon2'; import { randomBytes } from 'crypto'; import { readdir, stat } from 'fs/promises'; diff --git a/src/lib/utils/exif.ts b/src/lib/utils/exif.ts index 8509dea7..f1c9f803 100644 --- a/src/lib/utils/exif.ts +++ b/src/lib/utils/exif.ts @@ -1,4 +1,4 @@ -import { File } from '@prisma/client'; +import type { File } from '@prisma/client'; import { ExifTool, Tags } from 'exiftool-vendored'; import { createWriteStream } from 'fs'; import { readFile, rm } from 'fs/promises'; diff --git a/src/lib/utils/range.ts b/src/lib/utils/range.ts index f90daaf1..baf5f4a2 100644 --- a/src/lib/utils/range.ts +++ b/src/lib/utils/range.ts @@ -1,9 +1,20 @@ -export function parseRangeHeader(header?: string): [number, number] { - if (!header || !header.startsWith('bytes=')) return [0, Infinity]; +export function parseRange(header: string, length: number): [number, number] { + const range = header.trim().substring(6); - const range = header.replace('bytes=', '').split('-'); - const start = Number(range[0]) || 0; - const end = Number(range[1]) || Infinity; + let start, end; + + if (range.startsWith('-')) { + end = length - 1; + start = length - 1 - Number(range.substring(1)); + } else { + const [s, e] = range.split('-').map(Number); + start = s; + end = e || length - 1; + } + + if (end > length - 1) { + end = length - 1; + } return [start, end]; } diff --git a/src/pages/api/auth/image.ts b/src/pages/api/auth/file.ts similarity index 86% rename from src/pages/api/auth/image.ts rename to src/pages/api/auth/file.ts index 5db9ac72..d1375b9d 100644 --- a/src/pages/api/auth/image.ts +++ b/src/pages/api/auth/file.ts @@ -15,7 +15,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, }); - if (!file) return res.notFound('image not found'); + if (!file) return res.notFound('file not found'); if (!password) return res.badRequest('no password provided'); const decoded = decodeURIComponent(password as string); @@ -24,7 +24,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { if (!valid) return res.badRequest('wrong password'); const data = await datasource.get(file.name); - if (!data) return res.notFound('image not found'); + if (!data) return res.notFound('file not found'); const size = await datasource.size(file.name); @@ -33,7 +33,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { res.setHeader('Content-Length', size); data.pipe(res); - data.on('error', () => res.notFound('image not found')); + data.on('error', () => res.notFound('file not found')); data.on('end', () => res.end()); } diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts index f4bf555f..8430a702 100644 --- a/src/pages/api/auth/login.ts +++ b/src/pages/api/auth/login.ts @@ -14,8 +14,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { code?: string; }; - const users = await prisma.user.count(); - if (users === 0) { + if ((await prisma.user.count()) === 0) { logger.debug('no users found... creating default user...'); await prisma.user.create({ data: { diff --git a/src/pages/api/user/[id].ts b/src/pages/api/user/[id].ts index 6e9a1caa..e21e2343 100644 --- a/src/pages/api/user/[id].ts +++ b/src/pages/api/user/[id].ts @@ -12,9 +12,11 @@ const logger = Logger.get('user'); async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { const { id } = req.query as { id: string }; + if (!id || isNaN(parseInt(id))) return res.notFound('no user provided'); + const target = await prisma.user.findFirst({ where: { - id: Number(id), + id: parseInt(id), }, include: { files: { @@ -187,6 +189,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { return res.json(newUser); } else { delete target.password; + delete target.totpSecret; if (user.superAdmin && target.superAdmin) { delete target.files; diff --git a/src/pages/api/user/files.ts b/src/pages/api/user/files.ts index 9c5c2295..52bd9d1f 100644 --- a/src/pages/api/user/files.ts +++ b/src/pages/api/user/files.ts @@ -142,6 +142,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { size: bigint; originalName: string; thumbnail?: { name: string }; + password: string | boolean; }[] = await prisma.file.findMany({ where: { userId: user.id, @@ -163,11 +164,13 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { size: true, originalName: true, thumbnail: true, + password: true, }, }); for (let i = 0; i !== files.length; ++i) { (files[i] as unknown as { url: string }).url = formatRootUrl(config.uploader.route, files[i].name); + files[i].password = !!files[i].password; if (files[i].thumbnail) { (files[i].thumbnail as unknown as string) = formatRootUrl('/r', files[i].thumbnail.name); diff --git a/src/pages/api/user/recent.ts b/src/pages/api/user/recent.ts index 9873be13..e482af21 100644 --- a/src/pages/api/user/recent.ts +++ b/src/pages/api/user/recent.ts @@ -8,7 +8,20 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { if (take >= 50) return res.badRequest("take can't be more than 50"); - let files = await prisma.file.findMany({ + let files: { + favorite: boolean; + createdAt: Date; + id: number; + name: string; + mimetype: string; + expiresAt: Date; + maxViews: number; + views: number; + folderId: number; + size: bigint; + password: string | boolean; + thumbnail?: { name: string }; + }[] = await prisma.file.findMany({ take, where: { userId: user.id, @@ -28,14 +41,16 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { size: true, favorite: true, thumbnail: true, + password: true, }, }); for (let i = 0; i !== files.length; ++i) { (files[i] as unknown as { url: string }).url = formatRootUrl(config.uploader.route, files[i].name); - if (files[i].thumbnail) { + files[i].password = !!files[i].password; + + if (files[i].thumbnail) (files[i].thumbnail as unknown as string) = formatRootUrl('/r', files[i].thumbnail.name); - } } if (req.query.filter && req.query.filter === 'media') diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index 64258e11..0da9565a 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -67,7 +67,11 @@ export default function Login({ const username = values.username.trim(); const password = values.password.trim(); - if (username === '') return form.setFieldError('username', "Username can't be nothing"); + if (username === '') { + setLoading(false); + setDisabled(false); + return form.setFieldError('username', "Username can't be nothing"); + } const res = await useFetch('/api/auth/login', 'POST', { username, diff --git a/src/pages/view/[id].tsx b/src/pages/view/[id].tsx index 1ed10913..afcc0088 100644 --- a/src/pages/view/[id].tsx +++ b/src/pages/view/[id].tsx @@ -40,16 +40,18 @@ export default function EmbeddedFile({ const [downloadWPass, setDownloadWPass] = useState(false); + const mimeMatch = new RegExp(/^((?image)|(?video)|(?audio))/).exec(file.mimetype); + // reapply date from workaround file.createdAt = new Date(file ? file.createdAt : 0); const check = async () => { - const res = await fetch(`/api/auth/image?id=${file.id}&password=${encodeURIComponent(password)}`); + const res = await fetch(`/api/auth/file?id=${file.id}&password=${encodeURIComponent(password)}`); if (res.ok) { setError(''); if (prismRender) return router.push(`/code/${file.name}?password=${password}`); - updateImage(`/api/auth/image?id=${file.id}&password=${password}`); + updateFile(`/api/auth/file?id=${file.id}&password=${password}`); setOpened(false); setDownloadWPass(true); } else { @@ -57,34 +59,40 @@ export default function EmbeddedFile({ } }; - const updateImage = async (url?: string) => { - if (!file.mimetype.startsWith('image')) return; + const updateFile = async (url?: string) => { + if (!mimeMatch) return; - const imageEl = document.getElementById('image_content') as HTMLImageElement; + const imageEl = document.getElementById('image_content') as HTMLImageElement, + videoEl = document.getElementById('video_content') as HTMLVideoElement, + audioEl = document.getElementById('audio_content') as HTMLAudioElement; - const img = new Image(); - img.addEventListener('load', function () { - // my best attempt of recreating - // firefox: https://searchfox.org/mozilla-central/source/dom/html/ImageDocument.cpp#271-276 - // chromium-based: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/image_document.cc + if (mimeMatch?.groups?.img) { + const img = new Image(); + img.addEventListener('load', function () { + // my best attempt of recreating + // firefox: https://searchfox.org/mozilla-central/source/dom/html/ImageDocument.cpp#271-276 + // chromium-based: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/image_document.cc - // keeps image original if smaller than screen - if (this.width <= window.innerWidth && this.height <= window.innerHeight) return; + // keeps image original if smaller than screen + if (this.width <= window.innerWidth && this.height <= window.innerHeight) return; - // resizes to fit screen - const ratio = Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth); - const newWidth = Math.max(1, Math.floor(ratio * this.naturalWidth)); - const newHeight = Math.max(1, Math.floor(ratio * this.naturalHeight)); + // resizes to fit screen + const ratio = Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth); + const newWidth = Math.max(1, Math.floor(ratio * this.naturalWidth)); + const newHeight = Math.max(1, Math.floor(ratio * this.naturalHeight)); - imageEl.width = newWidth; - imageEl.height = newHeight; - }); + imageEl.width = newWidth; + imageEl.height = newHeight; + }); - img.src = url || dataURL('/r'); - if (url) { - imageEl.src = url; + img.src = url || dataURL('/r'); + file.imageProps = img; + } + if (url) { + if (mimeMatch?.groups?.img) imageEl.src = url; + if (mimeMatch?.groups?.vid) videoEl.src = url; + if (mimeMatch?.groups?.aud) audioEl.src = url; } - file.imageProps = img; }; useEffect(() => { @@ -94,12 +102,12 @@ export default function EmbeddedFile({ }, []); useEffect(() => { - if (!file?.mimetype?.startsWith('image')) return; + if (!mimeMatch) return; - updateImage(); - window.addEventListener('resize', () => updateImage()); + updateFile(); + window.addEventListener('resize', () => updateFile()); return () => { - window.removeEventListener('resize', () => updateImage()); + window.removeEventListener('resize', () => updateFile()); }; }, []); @@ -125,7 +133,7 @@ export default function EmbeddedFile({ )} - {file.mimetype.startsWith('image') && ( + {mimeMatch?.groups?.img && ( <> @@ -137,7 +145,7 @@ export default function EmbeddedFile({ )} - {file.mimetype.startsWith('video') && ( + {mimeMatch?.groups?.vid && ( <> @@ -160,7 +168,7 @@ export default function EmbeddedFile({ )} - {file.mimetype.startsWith('audio') && ( + {mimeMatch?.groups?.aud && ( <> @@ -177,9 +185,7 @@ export default function EmbeddedFile({ )} - {!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && ( - - )} + {!mimeMatch && } {file.name} - {file.mimetype.startsWith('image') && ( - {dataURL('/r')} - )} + {mimeMatch?.groups?.img && {dataURL('/r')}} - {file.mimetype.startsWith('video') && ( + {mimeMatch?.groups?.vid && (