mirror of
https://github.com/diced/zipline.git
synced 2025-04-04 23:21:17 -05:00
fix: a lot of stuff (#683)
* fix: No more infinite loading button! :D * chore: buhbai version! * chore: update browserlist db * fix: a totp secret that shouldn't be /probably/ shouldn't be revealed * fix: revert range getting for datasource * chore: a line lost! :O * chore: this probably should've been ignored for a long while * fix: Don't compress webm or webp. They go breaky * fix: issue 659, it was the wrong statusCode to look for * fix: I'll just regex it. * fix: let s3 in on the fun with partial uploads * chore&fix: they're files now :3 & unlock video and/or audio files * fix: Maybe prisma plugin needs a return? * fix: super focused regex this time :D * I guess this works? So cool :O * fix: bad id check * fix: Byte me! >:3 * fix: add password bool to file's prop * fix(?): this might fix some people's weard errors. * chore: I discovered more typing * fix: stats logger * fix(?): await the registers * chore: typeer typer * fix: This looks to properly fix issue 659. I dunno how, don't ask * More like uglier >:( * fix: actions don't like dis * fix: ranged requests handled properly * feat: remove supabase datasource --------- Co-authored-by: diced <pranaco2@gmail.com>
This commit is contained in:
parent
41e197ed4a
commit
1febd5aca0
37 changed files with 299 additions and 373 deletions
|
@ -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
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -31,6 +31,7 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ export abstract class Datasource {
|
|||
public abstract delete(file: string): Promise<void>;
|
||||
public abstract clear(): Promise<void>;
|
||||
public abstract size(file: string): Promise<number | null>;
|
||||
public abstract get(file: string, start?: number, end?: number): Readable | Promise<Readable>;
|
||||
public abstract get(file: string): Readable | Promise<Readable>;
|
||||
public abstract fullSize(): Promise<number>;
|
||||
public abstract range(file: string, start: number, end: number): Promise<Readable>;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export class Local extends Datasource {
|
|||
}
|
||||
|
||||
public async save(file: string, data: Buffer): Promise<void> {
|
||||
await writeFile(join(this.path, file), data);
|
||||
await writeFile(join(this.path, file), Uint8Array.from(data));
|
||||
}
|
||||
|
||||
public async delete(file: string): Promise<void> {
|
||||
|
@ -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<ReadStream> {
|
||||
const path = join(this.path, file);
|
||||
const readStream = createReadStream(path, { start, end });
|
||||
|
||||
return readStream;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Readable> {
|
||||
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<Readable> {
|
||||
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<Readable> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
await fetch(`${this.config.url}/storage/v1/object/${this.config.bucket}/${file}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.config.key}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async clear(): Promise<void> {
|
||||
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<Readable> {
|
||||
// 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<number | null> {
|
||||
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<number> {
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
export { Datasource } from './Datasource';
|
||||
export { Local } from './Local';
|
||||
export { S3 } from './S3';
|
||||
export { Supabase } from './Supabase';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -40,16 +40,18 @@ export default function EmbeddedFile({
|
|||
|
||||
const [downloadWPass, setDownloadWPass] = useState(false);
|
||||
|
||||
const mimeMatch = new RegExp(/^((?<img>image)|(?<vid>video)|(?<aud>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({
|
|||
<meta property='theme-color' content={parseString(user.embed.color, { file: file, user })} />
|
||||
)}
|
||||
|
||||
{file.mimetype.startsWith('image') && (
|
||||
{mimeMatch?.groups?.img && (
|
||||
<>
|
||||
<meta property='og:type' content='image' />
|
||||
<meta property='og:image' itemProp='image' content={`${host}/r/${file.name}`} />
|
||||
|
@ -137,7 +145,7 @@ export default function EmbeddedFile({
|
|||
<meta property='twitter:title' content={file.name} />
|
||||
</>
|
||||
)}
|
||||
{file.mimetype.startsWith('video') && (
|
||||
{mimeMatch?.groups?.vid && (
|
||||
<>
|
||||
<meta name='twitter:card' content='player' />
|
||||
<meta name='twitter:player' content={`${host}/r/${file.name}`} />
|
||||
|
@ -160,7 +168,7 @@ export default function EmbeddedFile({
|
|||
<meta property='og:video:type' content={file.mimetype} />
|
||||
</>
|
||||
)}
|
||||
{file.mimetype.startsWith('audio') && (
|
||||
{mimeMatch?.groups?.aud && (
|
||||
<>
|
||||
<meta name='twitter:card' content='player' />
|
||||
<meta name='twitter:player' content={`${host}/r/${file.name}`} />
|
||||
|
@ -177,9 +185,7 @@ export default function EmbeddedFile({
|
|||
<meta property='og:audio:type' content={file.mimetype} />
|
||||
</>
|
||||
)}
|
||||
{!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && (
|
||||
<meta property='og:url' content={`${host}/r/${file.name}`} />
|
||||
)}
|
||||
{!mimeMatch && <meta property='og:url' content={`${host}/r/${file.name}`} />}
|
||||
<title>{file.name}</title>
|
||||
</Head>
|
||||
<Modal
|
||||
|
@ -209,11 +215,9 @@ export default function EmbeddedFile({
|
|||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{file.mimetype.startsWith('image') && (
|
||||
<img src={dataURL('/r')} alt={dataURL('/r')} id='image_content' />
|
||||
)}
|
||||
{mimeMatch?.groups?.img && <img src={dataURL('/r')} alt={dataURL('/r')} id='image_content' />}
|
||||
|
||||
{file.mimetype.startsWith('video') && (
|
||||
{mimeMatch?.groups?.vid && (
|
||||
<video
|
||||
style={{
|
||||
maxHeight: '100vh',
|
||||
|
@ -227,17 +231,13 @@ export default function EmbeddedFile({
|
|||
/>
|
||||
)}
|
||||
|
||||
{file.mimetype.startsWith('audio') && (
|
||||
<audio src={dataURL('/r')} controls autoPlay muted id='audio_content' />
|
||||
)}
|
||||
{mimeMatch?.groups?.aud && <audio src={dataURL('/r')} controls autoPlay muted id='audio_content' />}
|
||||
|
||||
{!file.mimetype.startsWith('video') &&
|
||||
!file.mimetype.startsWith('image') &&
|
||||
!file.mimetype.startsWith('audio') && (
|
||||
<AnchorNext component={Link} href={dataURL('/r', downloadWPass ? password : undefined)}>
|
||||
Can't preview this file. Click here to download it.
|
||||
</AnchorNext>
|
||||
)}
|
||||
{!mimeMatch && (
|
||||
<AnchorNext component={Link} href={dataURL('/r', downloadWPass ? password : undefined)}>
|
||||
Can't preview this file. Click here to download it.
|
||||
</AnchorNext>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -53,8 +53,9 @@ async function main() {
|
|||
// copy files to local storage
|
||||
console.log(`Copying files to ${config.datasource.type} storage..`);
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const file = files[i];
|
||||
await datasource.save(file, await readFile(join(directory, file)), {
|
||||
const file = files[i],
|
||||
fb = await readFile(join(directory, file));
|
||||
await datasource.save(file, fb, {
|
||||
type: data[i]?.mimetype ?? 'application/octet-stream',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import config from 'lib/config';
|
||||
import 'lib/config';
|
||||
import { inspect } from 'util';
|
||||
|
||||
console.log(inspect(config, { depth: Infinity, colors: true }));
|
||||
|
|
|
@ -2,7 +2,7 @@ import { File } from '@prisma/client';
|
|||
import { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import exts from 'lib/exts';
|
||||
import { parseRangeHeader } from 'lib/utils/range';
|
||||
import { parseRange } from 'lib/utils/range';
|
||||
|
||||
function dbFileDecorator(fastify: FastifyInstance, _, done) {
|
||||
fastify.decorateReply('dbFile', dbFile);
|
||||
|
@ -17,28 +17,70 @@ function dbFileDecorator(fastify: FastifyInstance, _, done) {
|
|||
const size = await this.server.datasource.size(file.name);
|
||||
if (size === null) return this.notFound();
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [rangeStart, rangeEnd] = parseRangeHeader(this.request.headers.range);
|
||||
if (rangeStart >= rangeEnd)
|
||||
return this.code(416)
|
||||
.header('Content-Range', `bytes 0/${size - 1}`)
|
||||
.send();
|
||||
if (rangeEnd === Infinity) rangeEnd = size - 1;
|
||||
|
||||
const data = await this.server.datasource.get(file.name, rangeStart, rangeEnd);
|
||||
|
||||
// only send content-range if the client asked for it
|
||||
if (this.request.headers.range) {
|
||||
this.code(206);
|
||||
this.header('Content-Range', `bytes ${rangeStart}-${rangeEnd}/${size}`);
|
||||
const [start, end] = parseRange(this.request.headers.range, size);
|
||||
if (start >= size || end >= size) {
|
||||
const buf = await datasource.get(file.name);
|
||||
if (!buf) return this.server.nextServer.render404(this.request.raw, this.raw);
|
||||
|
||||
return this.type(file.mimetype || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
...(file.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(
|
||||
file.originalName,
|
||||
)}"`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(416)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.range(file.name, start || 0, end);
|
||||
if (!buf) return this.server.nextServer.render404(this.request.raw, this.raw);
|
||||
|
||||
return this.type(file.mimetype || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
...(file.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(
|
||||
file.originalName,
|
||||
)}"`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(206)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
this.header('Content-Length', rangeEnd - rangeStart + 1);
|
||||
this.header('Content-Type', download ? 'application/octet-stream' : file.mimetype);
|
||||
this.header('Content-Disposition', `inline; filename="${encodeURI(file.originalName || file.name)}"`);
|
||||
this.header('Accept-Ranges', 'bytes');
|
||||
const data = await datasource.get(file.name);
|
||||
if (!data) return this.server.nextServer.render404(this.request.raw, this.raw);
|
||||
|
||||
return this.send(data);
|
||||
return this.type(file.mimetype || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
'Accept-Ranges': 'bytes',
|
||||
...(file.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(
|
||||
file.originalName,
|
||||
)}"`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(200)
|
||||
.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import { Url } from '@prisma/client';
|
|||
import { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
function postUrlDecorator(fastify: FastifyInstance, _, done) {
|
||||
fastify.decorateReply('postUrl', postUrl.bind(fastify));
|
||||
function postUrlDecorator(fastify: FastifyInstance, _, done: () => void) {
|
||||
fastify.decorateReply('postUrl', postUrl);
|
||||
done();
|
||||
|
||||
async function postUrl(this: FastifyReply, url: Url) {
|
||||
if (!url) return true;
|
||||
if (!url) return;
|
||||
|
||||
const nUrl = await this.server.prisma.url.update({
|
||||
where: {
|
||||
|
@ -27,6 +27,7 @@ function postUrlDecorator(fastify: FastifyInstance, _, done) {
|
|||
|
||||
this.server.logger.child('url').info(`url deleted due to max views ${JSON.stringify(nUrl)}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import fastifyPlugin from 'fastify-plugin';
|
|||
import { createBrotliCompress, createDeflate, createGzip } from 'zlib';
|
||||
import pump from 'pump';
|
||||
import { Transform } from 'stream';
|
||||
import { parseRangeHeader } from 'lib/utils/range';
|
||||
import { parseRange } from 'lib/utils/range';
|
||||
|
||||
function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
||||
fastify.decorateReply('rawFile', rawFile);
|
||||
|
@ -18,36 +18,63 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
|||
|
||||
const mimetype = await guess(extname(id).slice(1));
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [rangeStart, rangeEnd] = parseRangeHeader(this.request.headers.range);
|
||||
if (rangeStart >= rangeEnd)
|
||||
return this.code(416)
|
||||
.header('Content-Range', `bytes 0/${size - 1}`)
|
||||
.send();
|
||||
if (rangeEnd === Infinity) rangeEnd = size - 1;
|
||||
|
||||
const data = await this.server.datasource.get(id, rangeStart, rangeEnd + 1);
|
||||
|
||||
// only send content-range if the client asked for it
|
||||
if (this.request.headers.range) {
|
||||
this.code(206);
|
||||
this.header('Content-Range', `bytes ${rangeStart}-${rangeEnd}/${size}`);
|
||||
const [start, end] = parseRange(this.request.headers.range, size);
|
||||
if (start >= size || end >= size) {
|
||||
const buf = await datasource.get(id);
|
||||
if (!buf) return this.server.nextServer.render404(this.request.raw, this.raw);
|
||||
|
||||
return this.type(mimetype || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
...(download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(416)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.range(id, start || 0, end);
|
||||
if (!buf) return this.server.nextServer.render404(this.request.raw, this.raw);
|
||||
|
||||
return this.type(mimetype || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
...(download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(206)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
this.header('Content-Length', rangeEnd - rangeStart + 1);
|
||||
this.header('Content-Type', download ? 'application/octet-stream' : mimetype);
|
||||
this.header('Accept-Ranges', 'bytes');
|
||||
const data = await datasource.get(id);
|
||||
if (!data) return this.server.nextServer.render404(this.request.raw, this.raw);
|
||||
|
||||
if (
|
||||
this.server.config.core.compression.enabled &&
|
||||
compress?.match(/^true$/i) &&
|
||||
!this.request.headers['X-Zipline-NoCompress'] &&
|
||||
(compress?.match(/^true$/i) || !this.request.headers['X-Zipline-NoCompress']) &&
|
||||
!!this.request.headers['accept-encoding']
|
||||
)
|
||||
if (size > this.server.config.core.compression.threshold && mimetype.match(/^(image|video|text)/))
|
||||
if (
|
||||
size > this.server.config.core.compression.threshold &&
|
||||
mimetype.match(/^(image(?!\/(webp))|video(?!\/(webm))|text)/)
|
||||
)
|
||||
return this.send(useCompress.call(this, data));
|
||||
|
||||
return this.send(data);
|
||||
return this.type(mimetype || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
'Accept-Ranges': 'bytes',
|
||||
...(download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(200)
|
||||
.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ async function start() {
|
|||
logger.debug('Starting server');
|
||||
|
||||
// plugins
|
||||
server
|
||||
await server
|
||||
.register(loggerPlugin)
|
||||
.register(configPlugin, config)
|
||||
.register(datasourcePlugin, datasource)
|
||||
|
@ -61,7 +61,7 @@ async function start() {
|
|||
.register(allPlugin);
|
||||
|
||||
// decorators
|
||||
server
|
||||
await server
|
||||
.register(notFound)
|
||||
.register(postUrlDecorator)
|
||||
.register(postFileDecorator)
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { migrations } from 'server/util';
|
||||
|
||||
async function prismaPlugin(fastify: FastifyInstance) {
|
||||
process.env.DATABASE_URL = fastify.config.core?.database_url;
|
||||
await migrations();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
fastify.decorate('prisma', prisma);
|
||||
fastify.decorate('prisma', new PrismaClient());
|
||||
return;
|
||||
}
|
||||
|
||||
export default fastifyPlugin(prismaPlugin, {
|
||||
|
|
|
@ -7,21 +7,21 @@ export default async function uploadsRoute(this: FastifyInstance, req: FastifyRe
|
|||
else if (id === 'dashboard' && !this.config.features.headless)
|
||||
return this.nextServer.render(req.raw, reply.raw, '/dashboard');
|
||||
|
||||
const image = await this.prisma.file.findFirst({
|
||||
const file = await this.prisma.file.findFirst({
|
||||
where: {
|
||||
OR: [{ name: id }, { name: decodeURI(id) }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
|
||||
},
|
||||
});
|
||||
if (!image) return reply.rawFile(id);
|
||||
if (!file) return reply.rawFile(id);
|
||||
|
||||
const failed = await reply.preFile(image);
|
||||
const failed = await reply.preFile(file);
|
||||
if (failed) return reply.notFound();
|
||||
|
||||
const ext = image.name.split('.').pop();
|
||||
const ext = file.name.split('.').pop();
|
||||
|
||||
if (image.password || image.embed || image.mimetype.startsWith('text/') || Object.keys(exts).includes(ext))
|
||||
return reply.redirect(`/view/${image.name}`);
|
||||
else return reply.dbFile(image);
|
||||
if (file.password || file.embed || file.mimetype.startsWith('text/') || Object.keys(exts).includes(ext))
|
||||
return reply.redirect(`/view/${file.name}`);
|
||||
else return reply.dbFile(file);
|
||||
}
|
||||
|
||||
export async function uploadsRouteOnResponse(
|
||||
|
|
|
@ -13,9 +13,7 @@ export default async function urlsRoute(this: FastifyInstance, req: FastifyReque
|
|||
});
|
||||
if (!url) return reply.notFound();
|
||||
|
||||
reply.redirect(url.destination);
|
||||
|
||||
reply.postUrl(url);
|
||||
return await reply.redirect(url.destination);
|
||||
}
|
||||
|
||||
export async function urlsRouteOnResponse(
|
||||
|
@ -24,7 +22,7 @@ export async function urlsRouteOnResponse(
|
|||
reply: FastifyReply,
|
||||
done: () => void,
|
||||
) {
|
||||
if (reply.statusCode === 200) {
|
||||
if (reply.statusCode === 302) {
|
||||
const { id } = req.params as { id: string };
|
||||
|
||||
const url = await this.prisma.url.findFirst({
|
||||
|
@ -32,8 +30,7 @@ export async function urlsRouteOnResponse(
|
|||
OR: [{ id }, { vanity: id }, { invisible: { invis: decodeURI(id) } }],
|
||||
},
|
||||
});
|
||||
|
||||
reply.postUrl(url);
|
||||
await reply.postUrl(url);
|
||||
}
|
||||
|
||||
done();
|
||||
|
|
|
@ -83,7 +83,8 @@ export function redirect(res: ServerResponse, url: string) {
|
|||
|
||||
export async function getStats(prisma: PrismaClient, datasource: Datasource, logger: Logger) {
|
||||
const size = await datasource.fullSize();
|
||||
logger.debug(`full size: ${size}`);
|
||||
const llogger = logger.child('stats');
|
||||
llogger.debug(`full size: ${size}`);
|
||||
|
||||
const byUser = await prisma.file.groupBy({
|
||||
by: ['userId'],
|
||||
|
@ -91,15 +92,15 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource, log
|
|||
_all: true,
|
||||
},
|
||||
});
|
||||
logger.debug(`by user: ${JSON.stringify(byUser)}`);
|
||||
llogger.debug(`by user: ${JSON.stringify(byUser)}`);
|
||||
|
||||
const count_users = await prisma.user.count();
|
||||
logger.debug(`count users: ${count_users}`);
|
||||
llogger.debug(`count users: ${count_users}`);
|
||||
|
||||
const count_by_user = [];
|
||||
for (let i = 0, L = byUser.length; i !== L; ++i) {
|
||||
if (!byUser[i].userId) {
|
||||
logger.debug(`skipping user ${byUser[i]}`);
|
||||
llogger.debug(`skipping user ${byUser[i]}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -114,17 +115,17 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource, log
|
|||
count: byUser[i]._count._all,
|
||||
});
|
||||
}
|
||||
logger.debug(`count by user: ${JSON.stringify(count_by_user)}`);
|
||||
llogger.debug(`count by user: ${JSON.stringify(count_by_user)}`);
|
||||
|
||||
const count = await prisma.file.count();
|
||||
logger.debug(`count files: ${JSON.stringify(count)}`);
|
||||
llogger.debug(`count files: ${JSON.stringify(count)}`);
|
||||
|
||||
const views = await prisma.file.aggregate({
|
||||
_sum: {
|
||||
views: true,
|
||||
},
|
||||
});
|
||||
logger.debug(`sum views: ${JSON.stringify(views)}`);
|
||||
llogger.debug(`sum views: ${JSON.stringify(views)}`);
|
||||
|
||||
const typesCount = await prisma.file.groupBy({
|
||||
by: ['mimetype'],
|
||||
|
@ -132,7 +133,7 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource, log
|
|||
mimetype: true,
|
||||
},
|
||||
});
|
||||
logger.debug(`types count: ${JSON.stringify(typesCount)}`);
|
||||
llogger.debug(`types count: ${JSON.stringify(typesCount)}`);
|
||||
const types_count = [];
|
||||
for (let i = 0, L = typesCount.length; i !== L; ++i)
|
||||
types_count.push({
|
||||
|
@ -140,7 +141,7 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource, log
|
|||
count: typesCount[i]._count.mimetype,
|
||||
});
|
||||
|
||||
logger.debug(`types count: ${JSON.stringify(types_count)}`);
|
||||
llogger.debug(`types count: ${JSON.stringify(types_count)}`);
|
||||
|
||||
return {
|
||||
size: bytesToHuman(size),
|
||||
|
|
|
@ -3063,9 +3063,9 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"caniuse-lite@npm:^1.0.30001406":
|
||||
version: 1.0.30001642
|
||||
resolution: "caniuse-lite@npm:1.0.30001642"
|
||||
checksum: 23f823ec115306eaf9299521328bb6ad0c4ce65254c375b14fd497ceda759ee8ee5b8763b7b622cb36b6b5fb53c6cb8569785fba842fe289be7dc3fcf008eb4f
|
||||
version: 1.0.30001696
|
||||
resolution: "caniuse-lite@npm:1.0.30001696"
|
||||
checksum: 079be180f364b63fb85415fa3948d1e9646aa655f8678a827e9b533712e14d727c2983397603ce7107b995226f6590d96bf26ef2032e756ef6ee09898feee5f9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue