mirror of
https://github.com/diced/zipline.git
synced 2025-05-11 10:26:05 +02: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.
|
# 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 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_RETURN_HTTPS=true
|
||||||
CORE_SECRET="changethis"
|
CORE_SECRET="changethis"
|
||||||
|
@ -27,13 +27,6 @@ DATASOURCE_LOCAL_DIRECTORY=./uploads
|
||||||
# DATASOURCE_S3_FORCE_S3_PATH=false
|
# DATASOURCE_S3_FORCE_S3_PATH=false
|
||||||
# DATASOURCE_S3_USE_SSL=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_DEFAULT_FORMAT=RANDOM
|
||||||
UPLOADER_ROUTE=/u
|
UPLOADER_ROUTE=/u
|
||||||
UPLOADER_LENGTH=6
|
UPLOADER_LENGTH=6
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -31,6 +31,7 @@ yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ActionIcon, Button, Center, Group, SimpleGrid, Title } from '@mantine/core';
|
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 { IconArrowLeft, IconFile } from '@tabler/icons-react';
|
||||||
import FileComponent from 'components/File';
|
import FileComponent from 'components/File';
|
||||||
import MutedText from 'components/MutedText';
|
import MutedText from 'components/MutedText';
|
||||||
|
|
|
@ -20,10 +20,9 @@ export interface ConfigCompression {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigDatasource {
|
export interface ConfigDatasource {
|
||||||
type: 'local' | 's3' | 'supabase';
|
type: 'local' | 's3';
|
||||||
local: ConfigLocalDatasource;
|
local: ConfigLocalDatasource;
|
||||||
s3?: ConfigS3Datasource;
|
s3?: ConfigS3Datasource;
|
||||||
supabase?: ConfigSupabaseDatasource;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigLocalDatasource {
|
export interface ConfigLocalDatasource {
|
||||||
|
@ -41,12 +40,6 @@ export interface ConfigS3Datasource {
|
||||||
region?: string;
|
region?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigSupabaseDatasource {
|
|
||||||
url: string;
|
|
||||||
key: string;
|
|
||||||
bucket: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigUploader {
|
export interface ConfigUploader {
|
||||||
default_format: string;
|
default_format: string;
|
||||||
route: string;
|
route: string;
|
||||||
|
|
|
@ -85,10 +85,6 @@ export default function readConfig() {
|
||||||
map('DATASOURCE_S3_REGION', 'string', 'datasource.s3.region'),
|
map('DATASOURCE_S3_REGION', 'string', 'datasource.s3.region'),
|
||||||
map('DATASOURCE_S3_USE_SSL', 'boolean', 'datasource.s3.use_ssl'),
|
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_DEFAULT_FORMAT', 'string', 'uploader.default_format'),
|
||||||
map('UPLOADER_ROUTE', 'string', 'uploader.route'),
|
map('UPLOADER_ROUTE', 'string', 'uploader.route'),
|
||||||
map('UPLOADER_LENGTH', 'number', 'uploader.length'),
|
map('UPLOADER_LENGTH', 'number', 'uploader.length'),
|
||||||
|
|
|
@ -51,7 +51,7 @@ const validator = s.object({
|
||||||
}),
|
}),
|
||||||
datasource: s
|
datasource: s
|
||||||
.object({
|
.object({
|
||||||
type: s.enum('local', 's3', 'supabase').default('local'),
|
type: s.enum('local', 's3').default('local'),
|
||||||
local: s
|
local: s
|
||||||
.object({
|
.object({
|
||||||
directory: s.string.default(resolve('./uploads')).transform((v) => resolve(v)),
|
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'),
|
region: s.string.default('us-east-1'),
|
||||||
use_ssl: s.boolean.default(false),
|
use_ssl: s.boolean.default(false),
|
||||||
}).optional,
|
}).optional,
|
||||||
supabase: s.object({
|
|
||||||
url: s.string,
|
|
||||||
key: s.string,
|
|
||||||
bucket: s.string,
|
|
||||||
}).optional,
|
|
||||||
})
|
})
|
||||||
.default({
|
.default({
|
||||||
type: 'local',
|
type: 'local',
|
||||||
|
@ -253,43 +248,29 @@ export default function validate(config): Config {
|
||||||
logger.debug(`Attemping to validate ${JSON.stringify(config)}`);
|
logger.debug(`Attemping to validate ${JSON.stringify(config)}`);
|
||||||
const validated = validator.parse(config);
|
const validated = validator.parse(config);
|
||||||
logger.debug(`Recieved config: ${JSON.stringify(validated)}`);
|
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.type === 's3') {
|
||||||
if (!validated.datasource.supabase.url) errors.push('datasource.supabase.url is a required field');
|
const errors = [];
|
||||||
if (!validated.datasource.supabase.bucket)
|
if (!validated.datasource.s3.access_key_id)
|
||||||
errors.push('datasource.supabase.bucket is a required field');
|
errors.push('datasource.s3.access_key_id is a required field');
|
||||||
if (errors.length) throw { errors };
|
if (!validated.datasource.s3.secret_access_key)
|
||||||
|
errors.push('datasource.s3.secret_access_key is a required field');
|
||||||
break;
|
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'];
|
const reserved = new RegExp(/^\/(view|code|folder|auth|r)(\/\S*)?$|^\/(api|dashboard)(\/\S*)*/);
|
||||||
if (reserved.some((r) => validated.uploader.route.startsWith(r))) {
|
if (reserved.exec(validated.uploader.route))
|
||||||
throw {
|
throw {
|
||||||
errors: [`The uploader route cannot be ${validated.uploader.route}, this is a reserved route.`],
|
errors: [`The uploader route cannot be ${validated.uploader.route}, this is a reserved route.`],
|
||||||
show: true,
|
show: true,
|
||||||
};
|
};
|
||||||
} else if (reserved.some((r) => validated.urls.route.startsWith(r))) {
|
if (reserved.exec(validated.urls.route))
|
||||||
throw {
|
throw {
|
||||||
errors: [`The urls route cannot be ${validated.urls.route}, this is a reserved route.`],
|
errors: [`The urls route cannot be ${validated.urls.route}, this is a reserved route.`],
|
||||||
show: true,
|
show: true,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return validated as unknown as Config;
|
return validated as unknown as Config;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import { Datasource, Local, S3, Supabase } from './datasources';
|
import { Datasource, Local, S3 } from './datasources';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
|
|
||||||
const logger = Logger.get('datasource');
|
const logger = Logger.get('datasource');
|
||||||
|
@ -14,10 +14,6 @@ if (!global.datasource) {
|
||||||
global.datasource = new Local(config.datasource.local.directory);
|
global.datasource = new Local(config.datasource.local.directory);
|
||||||
logger.info(`using Local(${config.datasource.local.directory}) datasource`);
|
logger.info(`using Local(${config.datasource.local.directory}) datasource`);
|
||||||
break;
|
break;
|
||||||
case 'supabase':
|
|
||||||
global.datasource = new Supabase(config.datasource.supabase);
|
|
||||||
logger.info(`using Supabase(${config.datasource.supabase.bucket}) datasource`);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw new Error('Invalid datasource type');
|
throw new Error('Invalid datasource type');
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ export abstract class Datasource {
|
||||||
public abstract delete(file: string): Promise<void>;
|
public abstract delete(file: string): Promise<void>;
|
||||||
public abstract clear(): Promise<void>;
|
public abstract clear(): Promise<void>;
|
||||||
public abstract size(file: string): Promise<number | null>;
|
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 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> {
|
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> {
|
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);
|
const full = join(this.path, file);
|
||||||
if (!existsSync(full)) return null;
|
if (!existsSync(full)) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return createReadStream(full, { start, end });
|
return createReadStream(full);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -56,4 +56,11 @@ export class Local extends Datasource {
|
||||||
|
|
||||||
return size;
|
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 { Datasource } from '.';
|
||||||
import { Readable } from 'stream';
|
import { PassThrough, Readable } from 'stream';
|
||||||
import { ConfigS3Datasource } from 'lib/config/Config';
|
import { ConfigS3Datasource } from 'lib/config/Config';
|
||||||
import { BucketItemStat, Client } from 'minio';
|
import { BucketItemStat, Client } from 'minio';
|
||||||
|
|
||||||
|
@ -24,7 +24,8 @@ export class S3 extends Datasource {
|
||||||
await this.s3.putObject(
|
await this.s3.putObject(
|
||||||
this.config.bucket,
|
this.config.bucket,
|
||||||
file,
|
file,
|
||||||
data,
|
new PassThrough().end(data),
|
||||||
|
data.byteLength,
|
||||||
options ? { 'Content-Type': options.type } : undefined,
|
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> {
|
public get(file: string): 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((res) => {
|
return new Promise((res) => {
|
||||||
this.s3.getPartialObject(
|
this.s3.getObject(this.config.bucket, file, (err, stream) => {
|
||||||
this.config.bucket,
|
if (err) res(null);
|
||||||
file,
|
else res(stream);
|
||||||
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);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 { Datasource } from './Datasource';
|
||||||
export { Local } from './Local';
|
export { Local } from './Local';
|
||||||
export { S3 } from './S3';
|
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 config from 'lib/config';
|
||||||
import { ConfigDiscordContent } from 'config/Config';
|
import { ConfigDiscordContent } from 'config/Config';
|
||||||
import Logger from 'lib/logger';
|
import Logger from 'lib/logger';
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import 'lib/config';
|
||||||
|
|
||||||
if (!global.prisma) {
|
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;
|
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 { hash, verify } from 'argon2';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { readdir, stat } from 'fs/promises';
|
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 { ExifTool, Tags } from 'exiftool-vendored';
|
||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from 'fs';
|
||||||
import { readFile, rm } from 'fs/promises';
|
import { readFile, rm } from 'fs/promises';
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
export function parseRangeHeader(header?: string): [number, number] {
|
export function parseRange(header: string, length: number): [number, number] {
|
||||||
if (!header || !header.startsWith('bytes=')) return [0, Infinity];
|
const range = header.trim().substring(6);
|
||||||
|
|
||||||
const range = header.replace('bytes=', '').split('-');
|
let start, end;
|
||||||
const start = Number(range[0]) || 0;
|
|
||||||
const end = Number(range[1]) || Infinity;
|
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];
|
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');
|
if (!password) return res.badRequest('no password provided');
|
||||||
|
|
||||||
const decoded = decodeURIComponent(password as string);
|
const decoded = decodeURIComponent(password as string);
|
||||||
|
@ -24,7 +24,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (!valid) return res.badRequest('wrong password');
|
if (!valid) return res.badRequest('wrong password');
|
||||||
|
|
||||||
const data = await datasource.get(file.name);
|
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);
|
const size = await datasource.size(file.name);
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
res.setHeader('Content-Length', size);
|
res.setHeader('Content-Length', size);
|
||||||
|
|
||||||
data.pipe(res);
|
data.pipe(res);
|
||||||
data.on('error', () => res.notFound('image not found'));
|
data.on('error', () => res.notFound('file not found'));
|
||||||
data.on('end', () => res.end());
|
data.on('end', () => res.end());
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
code?: string;
|
code?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const users = await prisma.user.count();
|
if ((await prisma.user.count()) === 0) {
|
||||||
if (users === 0) {
|
|
||||||
logger.debug('no users found... creating default user...');
|
logger.debug('no users found... creating default user...');
|
||||||
await prisma.user.create({
|
await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -12,9 +12,11 @@ const logger = Logger.get('user');
|
||||||
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||||
const { id } = req.query as { id: string };
|
const { id } = req.query as { id: string };
|
||||||
|
|
||||||
|
if (!id || isNaN(parseInt(id))) return res.notFound('no user provided');
|
||||||
|
|
||||||
const target = await prisma.user.findFirst({
|
const target = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: Number(id),
|
id: parseInt(id),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
files: {
|
files: {
|
||||||
|
@ -187,6 +189,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||||
return res.json(newUser);
|
return res.json(newUser);
|
||||||
} else {
|
} else {
|
||||||
delete target.password;
|
delete target.password;
|
||||||
|
delete target.totpSecret;
|
||||||
|
|
||||||
if (user.superAdmin && target.superAdmin) {
|
if (user.superAdmin && target.superAdmin) {
|
||||||
delete target.files;
|
delete target.files;
|
||||||
|
|
|
@ -142,6 +142,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||||
size: bigint;
|
size: bigint;
|
||||||
originalName: string;
|
originalName: string;
|
||||||
thumbnail?: { name: string };
|
thumbnail?: { name: string };
|
||||||
|
password: string | boolean;
|
||||||
}[] = await prisma.file.findMany({
|
}[] = await prisma.file.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -163,11 +164,13 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||||
size: true,
|
size: true,
|
||||||
originalName: true,
|
originalName: true,
|
||||||
thumbnail: true,
|
thumbnail: true,
|
||||||
|
password: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i !== files.length; ++i) {
|
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] as unknown as { url: string }).url = formatRootUrl(config.uploader.route, files[i].name);
|
||||||
|
files[i].password = !!files[i].password;
|
||||||
|
|
||||||
if (files[i].thumbnail) {
|
if (files[i].thumbnail) {
|
||||||
(files[i].thumbnail as unknown as string) = formatRootUrl('/r', files[i].thumbnail.name);
|
(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");
|
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,
|
take,
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -28,14 +41,16 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||||
size: true,
|
size: true,
|
||||||
favorite: true,
|
favorite: true,
|
||||||
thumbnail: true,
|
thumbnail: true,
|
||||||
|
password: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i !== files.length; ++i) {
|
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] 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);
|
(files[i].thumbnail as unknown as string) = formatRootUrl('/r', files[i].thumbnail.name);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.query.filter && req.query.filter === 'media')
|
if (req.query.filter && req.query.filter === 'media')
|
||||||
|
|
|
@ -67,7 +67,11 @@ export default function Login({
|
||||||
const username = values.username.trim();
|
const username = values.username.trim();
|
||||||
const password = values.password.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', {
|
const res = await useFetch('/api/auth/login', 'POST', {
|
||||||
username,
|
username,
|
||||||
|
|
|
@ -40,16 +40,18 @@ export default function EmbeddedFile({
|
||||||
|
|
||||||
const [downloadWPass, setDownloadWPass] = useState(false);
|
const [downloadWPass, setDownloadWPass] = useState(false);
|
||||||
|
|
||||||
|
const mimeMatch = new RegExp(/^((?<img>image)|(?<vid>video)|(?<aud>audio))/).exec(file.mimetype);
|
||||||
|
|
||||||
// reapply date from workaround
|
// reapply date from workaround
|
||||||
file.createdAt = new Date(file ? file.createdAt : 0);
|
file.createdAt = new Date(file ? file.createdAt : 0);
|
||||||
|
|
||||||
const check = async () => {
|
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) {
|
if (res.ok) {
|
||||||
setError('');
|
setError('');
|
||||||
if (prismRender) return router.push(`/code/${file.name}?password=${password}`);
|
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);
|
setOpened(false);
|
||||||
setDownloadWPass(true);
|
setDownloadWPass(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -57,34 +59,40 @@ export default function EmbeddedFile({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateImage = async (url?: string) => {
|
const updateFile = async (url?: string) => {
|
||||||
if (!file.mimetype.startsWith('image')) return;
|
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();
|
if (mimeMatch?.groups?.img) {
|
||||||
img.addEventListener('load', function () {
|
const img = new Image();
|
||||||
// my best attempt of recreating
|
img.addEventListener('load', function () {
|
||||||
// firefox: https://searchfox.org/mozilla-central/source/dom/html/ImageDocument.cpp#271-276
|
// my best attempt of recreating
|
||||||
// chromium-based: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/image_document.cc
|
// 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
|
// keeps image original if smaller than screen
|
||||||
if (this.width <= window.innerWidth && this.height <= window.innerHeight) return;
|
if (this.width <= window.innerWidth && this.height <= window.innerHeight) return;
|
||||||
|
|
||||||
// resizes to fit screen
|
// resizes to fit screen
|
||||||
const ratio = Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth);
|
const ratio = Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth);
|
||||||
const newWidth = Math.max(1, Math.floor(ratio * this.naturalWidth));
|
const newWidth = Math.max(1, Math.floor(ratio * this.naturalWidth));
|
||||||
const newHeight = Math.max(1, Math.floor(ratio * this.naturalHeight));
|
const newHeight = Math.max(1, Math.floor(ratio * this.naturalHeight));
|
||||||
|
|
||||||
imageEl.width = newWidth;
|
imageEl.width = newWidth;
|
||||||
imageEl.height = newHeight;
|
imageEl.height = newHeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
img.src = url || dataURL('/r');
|
img.src = url || dataURL('/r');
|
||||||
if (url) {
|
file.imageProps = img;
|
||||||
imageEl.src = url;
|
}
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
|
@ -94,12 +102,12 @@ export default function EmbeddedFile({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!file?.mimetype?.startsWith('image')) return;
|
if (!mimeMatch) return;
|
||||||
|
|
||||||
updateImage();
|
updateFile();
|
||||||
window.addEventListener('resize', () => updateImage());
|
window.addEventListener('resize', () => updateFile());
|
||||||
return () => {
|
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 })} />
|
<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:type' content='image' />
|
||||||
<meta property='og:image' itemProp='image' content={`${host}/r/${file.name}`} />
|
<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} />
|
<meta property='twitter:title' content={file.name} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{file.mimetype.startsWith('video') && (
|
{mimeMatch?.groups?.vid && (
|
||||||
<>
|
<>
|
||||||
<meta name='twitter:card' content='player' />
|
<meta name='twitter:card' content='player' />
|
||||||
<meta name='twitter:player' content={`${host}/r/${file.name}`} />
|
<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} />
|
<meta property='og:video:type' content={file.mimetype} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{file.mimetype.startsWith('audio') && (
|
{mimeMatch?.groups?.aud && (
|
||||||
<>
|
<>
|
||||||
<meta name='twitter:card' content='player' />
|
<meta name='twitter:card' content='player' />
|
||||||
<meta name='twitter:player' content={`${host}/r/${file.name}`} />
|
<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} />
|
<meta property='og:audio:type' content={file.mimetype} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && (
|
{!mimeMatch && <meta property='og:url' content={`${host}/r/${file.name}`} />}
|
||||||
<meta property='og:url' content={`${host}/r/${file.name}`} />
|
|
||||||
)}
|
|
||||||
<title>{file.name}</title>
|
<title>{file.name}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -209,11 +215,9 @@ export default function EmbeddedFile({
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{file.mimetype.startsWith('image') && (
|
{mimeMatch?.groups?.img && <img src={dataURL('/r')} alt={dataURL('/r')} id='image_content' />}
|
||||||
<img src={dataURL('/r')} alt={dataURL('/r')} id='image_content' />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{file.mimetype.startsWith('video') && (
|
{mimeMatch?.groups?.vid && (
|
||||||
<video
|
<video
|
||||||
style={{
|
style={{
|
||||||
maxHeight: '100vh',
|
maxHeight: '100vh',
|
||||||
|
@ -227,17 +231,13 @@ export default function EmbeddedFile({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{file.mimetype.startsWith('audio') && (
|
{mimeMatch?.groups?.aud && <audio src={dataURL('/r')} controls autoPlay muted id='audio_content' />}
|
||||||
<audio src={dataURL('/r')} controls autoPlay muted id='audio_content' />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!file.mimetype.startsWith('video') &&
|
{!mimeMatch && (
|
||||||
!file.mimetype.startsWith('image') &&
|
<AnchorNext component={Link} href={dataURL('/r', downloadWPass ? password : undefined)}>
|
||||||
!file.mimetype.startsWith('audio') && (
|
Can't preview this file. Click here to download it.
|
||||||
<AnchorNext component={Link} href={dataURL('/r', downloadWPass ? password : undefined)}>
|
</AnchorNext>
|
||||||
Can't preview this file. Click here to download it.
|
)}
|
||||||
</AnchorNext>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -53,8 +53,9 @@ async function main() {
|
||||||
// copy files to local storage
|
// copy files to local storage
|
||||||
console.log(`Copying files to ${config.datasource.type} storage..`);
|
console.log(`Copying files to ${config.datasource.type} storage..`);
|
||||||
for (let i = 0; i !== files.length; ++i) {
|
for (let i = 0; i !== files.length; ++i) {
|
||||||
const file = files[i];
|
const file = files[i],
|
||||||
await datasource.save(file, await readFile(join(directory, file)), {
|
fb = await readFile(join(directory, file));
|
||||||
|
await datasource.save(file, fb, {
|
||||||
type: data[i]?.mimetype ?? 'application/octet-stream',
|
type: data[i]?.mimetype ?? 'application/octet-stream',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import config from 'lib/config';
|
import 'lib/config';
|
||||||
import { inspect } from 'util';
|
import { inspect } from 'util';
|
||||||
|
|
||||||
console.log(inspect(config, { depth: Infinity, colors: true }));
|
console.log(inspect(config, { depth: Infinity, colors: true }));
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { File } from '@prisma/client';
|
||||||
import { FastifyInstance, FastifyReply } from 'fastify';
|
import { FastifyInstance, FastifyReply } from 'fastify';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
import exts from 'lib/exts';
|
import exts from 'lib/exts';
|
||||||
import { parseRangeHeader } from 'lib/utils/range';
|
import { parseRange } from 'lib/utils/range';
|
||||||
|
|
||||||
function dbFileDecorator(fastify: FastifyInstance, _, done) {
|
function dbFileDecorator(fastify: FastifyInstance, _, done) {
|
||||||
fastify.decorateReply('dbFile', dbFile);
|
fastify.decorateReply('dbFile', dbFile);
|
||||||
|
@ -17,28 +17,70 @@ function dbFileDecorator(fastify: FastifyInstance, _, done) {
|
||||||
const size = await this.server.datasource.size(file.name);
|
const size = await this.server.datasource.size(file.name);
|
||||||
if (size === null) return this.notFound();
|
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) {
|
if (this.request.headers.range) {
|
||||||
this.code(206);
|
const [start, end] = parseRange(this.request.headers.range, size);
|
||||||
this.header('Content-Range', `bytes ${rangeStart}-${rangeEnd}/${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);
|
const data = await datasource.get(file.name);
|
||||||
this.header('Content-Type', download ? 'application/octet-stream' : file.mimetype);
|
if (!data) return this.server.nextServer.render404(this.request.raw, this.raw);
|
||||||
this.header('Content-Disposition', `inline; filename="${encodeURI(file.originalName || file.name)}"`);
|
|
||||||
this.header('Accept-Ranges', 'bytes');
|
|
||||||
|
|
||||||
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 { FastifyInstance, FastifyReply } from 'fastify';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
|
||||||
function postUrlDecorator(fastify: FastifyInstance, _, done) {
|
function postUrlDecorator(fastify: FastifyInstance, _, done: () => void) {
|
||||||
fastify.decorateReply('postUrl', postUrl.bind(fastify));
|
fastify.decorateReply('postUrl', postUrl);
|
||||||
done();
|
done();
|
||||||
|
|
||||||
async function postUrl(this: FastifyReply, url: Url) {
|
async function postUrl(this: FastifyReply, url: Url) {
|
||||||
if (!url) return true;
|
if (!url) return;
|
||||||
|
|
||||||
const nUrl = await this.server.prisma.url.update({
|
const nUrl = await this.server.prisma.url.update({
|
||||||
where: {
|
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)}`);
|
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 { createBrotliCompress, createDeflate, createGzip } from 'zlib';
|
||||||
import pump from 'pump';
|
import pump from 'pump';
|
||||||
import { Transform } from 'stream';
|
import { Transform } from 'stream';
|
||||||
import { parseRangeHeader } from 'lib/utils/range';
|
import { parseRange } from 'lib/utils/range';
|
||||||
|
|
||||||
function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
||||||
fastify.decorateReply('rawFile', rawFile);
|
fastify.decorateReply('rawFile', rawFile);
|
||||||
|
@ -18,36 +18,63 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
||||||
|
|
||||||
const mimetype = await guess(extname(id).slice(1));
|
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) {
|
if (this.request.headers.range) {
|
||||||
this.code(206);
|
const [start, end] = parseRange(this.request.headers.range, size);
|
||||||
this.header('Content-Range', `bytes ${rangeStart}-${rangeEnd}/${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);
|
const data = await datasource.get(id);
|
||||||
this.header('Content-Type', download ? 'application/octet-stream' : mimetype);
|
if (!data) return this.server.nextServer.render404(this.request.raw, this.raw);
|
||||||
this.header('Accept-Ranges', 'bytes');
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.server.config.core.compression.enabled &&
|
this.server.config.core.compression.enabled &&
|
||||||
compress?.match(/^true$/i) &&
|
(compress?.match(/^true$/i) || !this.request.headers['X-Zipline-NoCompress']) &&
|
||||||
!this.request.headers['X-Zipline-NoCompress'] &&
|
|
||||||
!!this.request.headers['accept-encoding']
|
!!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(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');
|
logger.debug('Starting server');
|
||||||
|
|
||||||
// plugins
|
// plugins
|
||||||
server
|
await server
|
||||||
.register(loggerPlugin)
|
.register(loggerPlugin)
|
||||||
.register(configPlugin, config)
|
.register(configPlugin, config)
|
||||||
.register(datasourcePlugin, datasource)
|
.register(datasourcePlugin, datasource)
|
||||||
|
@ -61,7 +61,7 @@ async function start() {
|
||||||
.register(allPlugin);
|
.register(allPlugin);
|
||||||
|
|
||||||
// decorators
|
// decorators
|
||||||
server
|
await server
|
||||||
.register(notFound)
|
.register(notFound)
|
||||||
.register(postUrlDecorator)
|
.register(postUrlDecorator)
|
||||||
.register(postFileDecorator)
|
.register(postFileDecorator)
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
import { migrations } from 'server/util';
|
import { migrations } from 'server/util';
|
||||||
|
|
||||||
async function prismaPlugin(fastify: FastifyInstance) {
|
async function prismaPlugin(fastify: FastifyInstance) {
|
||||||
process.env.DATABASE_URL = fastify.config.core?.database_url;
|
process.env.DATABASE_URL = fastify.config.core?.database_url;
|
||||||
await migrations();
|
await migrations();
|
||||||
|
fastify.decorate('prisma', new PrismaClient());
|
||||||
const prisma = new PrismaClient();
|
return;
|
||||||
|
|
||||||
fastify.decorate('prisma', prisma);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fastifyPlugin(prismaPlugin, {
|
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)
|
else if (id === 'dashboard' && !this.config.features.headless)
|
||||||
return this.nextServer.render(req.raw, reply.raw, '/dashboard');
|
return this.nextServer.render(req.raw, reply.raw, '/dashboard');
|
||||||
|
|
||||||
const image = await this.prisma.file.findFirst({
|
const file = await this.prisma.file.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ name: id }, { name: decodeURI(id) }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
|
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();
|
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))
|
if (file.password || file.embed || file.mimetype.startsWith('text/') || Object.keys(exts).includes(ext))
|
||||||
return reply.redirect(`/view/${image.name}`);
|
return reply.redirect(`/view/${file.name}`);
|
||||||
else return reply.dbFile(image);
|
else return reply.dbFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadsRouteOnResponse(
|
export async function uploadsRouteOnResponse(
|
||||||
|
|
|
@ -13,9 +13,7 @@ export default async function urlsRoute(this: FastifyInstance, req: FastifyReque
|
||||||
});
|
});
|
||||||
if (!url) return reply.notFound();
|
if (!url) return reply.notFound();
|
||||||
|
|
||||||
reply.redirect(url.destination);
|
return await reply.redirect(url.destination);
|
||||||
|
|
||||||
reply.postUrl(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function urlsRouteOnResponse(
|
export async function urlsRouteOnResponse(
|
||||||
|
@ -24,7 +22,7 @@ export async function urlsRouteOnResponse(
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
done: () => void,
|
done: () => void,
|
||||||
) {
|
) {
|
||||||
if (reply.statusCode === 200) {
|
if (reply.statusCode === 302) {
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
|
|
||||||
const url = await this.prisma.url.findFirst({
|
const url = await this.prisma.url.findFirst({
|
||||||
|
@ -32,8 +30,7 @@ export async function urlsRouteOnResponse(
|
||||||
OR: [{ id }, { vanity: id }, { invisible: { invis: decodeURI(id) } }],
|
OR: [{ id }, { vanity: id }, { invisible: { invis: decodeURI(id) } }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await reply.postUrl(url);
|
||||||
reply.postUrl(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
|
|
@ -83,7 +83,8 @@ export function redirect(res: ServerResponse, url: string) {
|
||||||
|
|
||||||
export async function getStats(prisma: PrismaClient, datasource: Datasource, logger: Logger) {
|
export async function getStats(prisma: PrismaClient, datasource: Datasource, logger: Logger) {
|
||||||
const size = await datasource.fullSize();
|
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({
|
const byUser = await prisma.file.groupBy({
|
||||||
by: ['userId'],
|
by: ['userId'],
|
||||||
|
@ -91,15 +92,15 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource, log
|
||||||
_all: true,
|
_all: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
logger.debug(`by user: ${JSON.stringify(byUser)}`);
|
llogger.debug(`by user: ${JSON.stringify(byUser)}`);
|
||||||
|
|
||||||
const count_users = await prisma.user.count();
|
const count_users = await prisma.user.count();
|
||||||
logger.debug(`count users: ${count_users}`);
|
llogger.debug(`count users: ${count_users}`);
|
||||||
|
|
||||||
const count_by_user = [];
|
const count_by_user = [];
|
||||||
for (let i = 0, L = byUser.length; i !== L; ++i) {
|
for (let i = 0, L = byUser.length; i !== L; ++i) {
|
||||||
if (!byUser[i].userId) {
|
if (!byUser[i].userId) {
|
||||||
logger.debug(`skipping user ${byUser[i]}`);
|
llogger.debug(`skipping user ${byUser[i]}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,17 +115,17 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource, log
|
||||||
count: byUser[i]._count._all,
|
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();
|
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({
|
const views = await prisma.file.aggregate({
|
||||||
_sum: {
|
_sum: {
|
||||||
views: true,
|
views: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
logger.debug(`sum views: ${JSON.stringify(views)}`);
|
llogger.debug(`sum views: ${JSON.stringify(views)}`);
|
||||||
|
|
||||||
const typesCount = await prisma.file.groupBy({
|
const typesCount = await prisma.file.groupBy({
|
||||||
by: ['mimetype'],
|
by: ['mimetype'],
|
||||||
|
@ -132,7 +133,7 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource, log
|
||||||
mimetype: true,
|
mimetype: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
logger.debug(`types count: ${JSON.stringify(typesCount)}`);
|
llogger.debug(`types count: ${JSON.stringify(typesCount)}`);
|
||||||
const types_count = [];
|
const types_count = [];
|
||||||
for (let i = 0, L = typesCount.length; i !== L; ++i)
|
for (let i = 0, L = typesCount.length; i !== L; ++i)
|
||||||
types_count.push({
|
types_count.push({
|
||||||
|
@ -140,7 +141,7 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource, log
|
||||||
count: typesCount[i]._count.mimetype,
|
count: typesCount[i]._count.mimetype,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(`types count: ${JSON.stringify(types_count)}`);
|
llogger.debug(`types count: ${JSON.stringify(types_count)}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
size: bytesToHuman(size),
|
size: bytesToHuman(size),
|
||||||
|
|
|
@ -3063,9 +3063,9 @@ __metadata:
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"caniuse-lite@npm:^1.0.30001406":
|
"caniuse-lite@npm:^1.0.30001406":
|
||||||
version: 1.0.30001642
|
version: 1.0.30001696
|
||||||
resolution: "caniuse-lite@npm:1.0.30001642"
|
resolution: "caniuse-lite@npm:1.0.30001696"
|
||||||
checksum: 23f823ec115306eaf9299521328bb6ad0c4ce65254c375b14fd497ceda759ee8ee5b8763b7b622cb36b6b5fb53c6cb8569785fba842fe289be7dc3fcf008eb4f
|
checksum: 079be180f364b63fb85415fa3948d1e9646aa655f8678a827e9b533712e14d727c2983397603ce7107b995226f6590d96bf26ef2032e756ef6ee09898feee5f9
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue