mirror of https://github.com/sylv/micro.git
chore: convert api to esm (#30)
This commit is contained in:
parent
f43a8e0cb5
commit
89ff23edfd
|
@ -28,7 +28,6 @@
|
|||
// const selectedClass = 'value'
|
||||
["const [a-zA-Z]+s = ['\"`]([^\"`'`]*)"]
|
||||
],
|
||||
"cSpell.words": ["xbytes"],
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/github-workflow.json": "file:///home/ryan/projects/micro/.github/workflows/build.yaml"
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"private": true,
|
||||
"packageManager": "pnpm@7.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 <17",
|
||||
"node": ">=16",
|
||||
"pnpm": ">=7"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -17,6 +17,6 @@
|
|||
"clean": "rm -rf ./packages/*/{tsconfig.tsbuildinfo,lib,dist,yarn-error.log,.next}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "1.3.2-canary.0"
|
||||
"turbo": "1.5.5"
|
||||
}
|
||||
}
|
|
@ -5,85 +5,83 @@
|
|||
"author": "Ryan <ryan@sylver.me>",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engine": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "rm -rf ./dist && tsup src/main.ts src/migrations/* --watch --onSuccess \"node dist/main.js\" --target node16",
|
||||
"watch": "tsup src/main.ts src/migrations/* --watch --clean --format esm --sourcemap --onSuccess \"node dist/main.js --inspect --inspect-brk\"",
|
||||
"build": "rm -rf ./dist && ncc build src/main.ts -o dist --minify --transpile-only --v8-cache --no-source-map-register",
|
||||
"lint": "eslint src --fix --cache",
|
||||
"test": "jest"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^6.0.0",
|
||||
"@fastify/helmet": "^8.0.0",
|
||||
"@fastify/multipart": "^6.0.0",
|
||||
"@fastify/cookie": "^8.3.0",
|
||||
"@fastify/helmet": "^10.0.1",
|
||||
"@fastify/multipart": "^7.2.0",
|
||||
"@jenyus-org/nestjs-graphql-utils": "^1.6.4",
|
||||
"@mikro-orm/core": "^5.2.2",
|
||||
"@mikro-orm/migrations": "^5.2.2",
|
||||
"@mikro-orm/nestjs": "^5.0.2",
|
||||
"@mikro-orm/postgresql": "^5.2.2",
|
||||
"@nestjs/common": "^8.4.4",
|
||||
"@nestjs/core": "^8.4.4",
|
||||
"@mikro-orm/core": "^5.4.2",
|
||||
"@mikro-orm/migrations": "^5.4.2",
|
||||
"@mikro-orm/nestjs": "^5.1.2",
|
||||
"@mikro-orm/postgresql": "^5.4.2",
|
||||
"@nestjs/common": "^9.1.4",
|
||||
"@nestjs/core": "^9.1.4",
|
||||
"@nestjs/graphql": "^10.0.16",
|
||||
"@nestjs/jwt": "^8.0.0",
|
||||
"@nestjs/jwt": "^9.0.0",
|
||||
"@nestjs/mercurius": "^10.0.16",
|
||||
"@nestjs/passport": "^8.2.1",
|
||||
"@nestjs/platform-fastify": "^8.4.4",
|
||||
"@nestjs/schedule": "^1.1.0",
|
||||
"@ryanke/thumbnail-generator": "workspace:^0.0.1",
|
||||
"@ryanke/venera": "^0.0.2",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-fastify": "^9.1.4",
|
||||
"@nestjs/schedule": "^2.1.0",
|
||||
"@ryanke/venera": "^1.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bytes": "^3.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"content-range": "^2.0.2",
|
||||
"dedent": "^0.7.0",
|
||||
"escape-string-regexp": "^4",
|
||||
"fastify": "^3.29.0",
|
||||
"file-type": "^16",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"fastify": "^4.7.0",
|
||||
"file-type": "^18.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"get-stream": "^6.0.1",
|
||||
"graphql": "^16.5.0",
|
||||
"graphql": "^16.6.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"istextorbinary": "^6.0.0",
|
||||
"luxon": "^2.3.2",
|
||||
"mercurius": "^9",
|
||||
"luxon": "^3.0.4",
|
||||
"mercurius": "^11.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"ms": "^3.0.0-canary.1",
|
||||
"nanoid": "^3.3.4",
|
||||
"nodemailer": "^6.7.6",
|
||||
"normalize-url": "^6",
|
||||
"nanoid": "^4.0.0",
|
||||
"nodemailer": "^6.8.0",
|
||||
"normalize-url": "^7.2.0",
|
||||
"otplib": "^12.0.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.5.5",
|
||||
"sharp": "^0.30.4",
|
||||
"stream-size": "^0.0.6",
|
||||
"xbytes": "^1.7.0"
|
||||
"rxjs": "^7.5.7",
|
||||
"sharp": "^0.31.1",
|
||||
"stream-size": "^0.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mikro-orm/cli": "^5.2.2",
|
||||
"@swc/core": "^1.2.208",
|
||||
"@sylo-digital/scripts": "^1.0.2",
|
||||
"@mikro-orm/cli": "^5.4.2",
|
||||
"@swc/core": "^1.3.5",
|
||||
"@sylo-digital/scripts": "^1.0.12",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/bytes": "^3.1.1",
|
||||
"@types/dedent": "^0.7.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.20",
|
||||
"@types/jest": "^28.1.4",
|
||||
"@types/luxon": "^2.3.2",
|
||||
"@types/luxon": "^3.0.1",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "16",
|
||||
"@types/passport-jwt": "^3.0.6",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
"@types/sharp": "^0.30.2",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/passport-jwt": "^3.0.7",
|
||||
"@types/sharp": "^0.31.0",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"jest": "^28.1.2",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"ts-node": "^10.8.2",
|
||||
"tsup": "^6.1.3",
|
||||
"typescript": "^4.7.4"
|
||||
"ts-node": "^10.9.1",
|
||||
"tsup": "^6.2.3",
|
||||
"typescript": "^4.8.4",
|
||||
"vitest": "^0.24.0"
|
||||
},
|
||||
"mikro-orm": {
|
||||
"useTsNode": true,
|
||||
|
@ -91,8 +89,5 @@
|
|||
"./src/orm.ts",
|
||||
"./dist/orm.js"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"preset": "@sylo-digital/scripts/jest/node"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import bytes from 'bytes';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
|
@ -13,12 +14,11 @@ import {
|
|||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import path from 'path';
|
||||
import xbytes from 'xbytes';
|
||||
import { expandMime } from '../helpers/expand-mime';
|
||||
import { MicroConversion } from './MicroConversion';
|
||||
import { MicroEmail } from './MicroEmail';
|
||||
import { MicroHost } from './MicroHost';
|
||||
import { MicroPurge } from './MicroPurge';
|
||||
import { expandMime } from '../helpers/expand-mime.js';
|
||||
import { MicroConversion } from './MicroConversion.js';
|
||||
import { MicroEmail } from './MicroEmail.js';
|
||||
import { MicroHost } from './MicroHost.js';
|
||||
import { MicroPurge } from './MicroPurge.js';
|
||||
|
||||
export class MicroConfig {
|
||||
@IsUrl({ require_tld: false, require_protocol: true, protocols: ['postgresql', 'postgres'] })
|
||||
|
@ -32,8 +32,8 @@ export class MicroConfig {
|
|||
inquiries: string;
|
||||
|
||||
@IsNumber()
|
||||
@Transform(({ value }) => xbytes.parseSize(value))
|
||||
uploadLimit = xbytes.parseSize('50MB');
|
||||
@Transform(({ value }) => bytes.parse(value))
|
||||
uploadLimit = bytes.parse('50MB');
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
|
@ -77,6 +77,6 @@ export class MicroConfig {
|
|||
hosts: MicroHost[];
|
||||
|
||||
get rootHost() {
|
||||
return this.hosts[0];
|
||||
return this.hosts[0]!;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import bytes from 'bytes';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsMimeType, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
import xbytes from 'xbytes';
|
||||
import { expandMime } from '../helpers/expand-mime';
|
||||
import { expandMime } from '../helpers/expand-mime.js';
|
||||
|
||||
export class MicroConversion {
|
||||
@IsString({ each: true })
|
||||
|
@ -16,6 +16,6 @@ export class MicroConversion {
|
|||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => xbytes.parseSize(value))
|
||||
@Transform(({ value }) => bytes.parse(value))
|
||||
minSize?: number;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IsOptional, IsString, IsUrl, Matches } from 'class-validator';
|
||||
import escapeString from 'escape-string-regexp';
|
||||
import { HostService } from '../modules/host/host.service';
|
||||
import { HostService } from '../modules/host/host.service.js';
|
||||
|
||||
export class MicroHost {
|
||||
constructor(url: string, tags?: string[], redirect?: string) {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import bytes from 'bytes';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsNumber } from 'class-validator';
|
||||
import ms from 'ms';
|
||||
import xbytes from 'xbytes';
|
||||
|
||||
export class MicroPurge {
|
||||
@IsNumber()
|
||||
@Transform(({ value }) => xbytes.parseSize(value))
|
||||
@Transform(({ value }) => bytes.parse(value))
|
||||
overLimit: number;
|
||||
|
||||
@IsNumber()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { loadConfig } from '@ryanke/venera';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { validateSync } from 'class-validator';
|
||||
import { MicroConfig } from './classes/MicroConfig';
|
||||
import { MicroConfig } from './classes/MicroConfig.js';
|
||||
|
||||
const data = loadConfig('micro');
|
||||
const config = plainToClass(MicroConfig, data, { exposeDefaultValues: true });
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { MicroHost } from '../classes/MicroHost';
|
||||
import type { User } from './user/user.entity';
|
||||
import type { MicroHost } from '../classes/MicroHost.js';
|
||||
import type { User } from './user/user.entity.js';
|
||||
import 'fastify';
|
||||
|
||||
declare module 'fastify' {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { expandMime } from './expand-mime';
|
||||
import { expect, it } from 'vitest';
|
||||
import { expandMime } from './expand-mime.js';
|
||||
|
||||
it('should expand mime types', () => {
|
||||
expect(expandMime('video')).toMatchSnapshot();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { FastifyRequest } from 'fastify';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
|
||||
export function getHostFromRequest(request: FastifyRequest): string {
|
||||
if (request.headers['x-forwarded-host']) {
|
||||
if (Array.isArray(request.headers['x-forwarded-host'])) {
|
||||
return request.headers['x-forwarded-host'][0];
|
||||
return request.headers['x-forwarded-host'][0]!;
|
||||
}
|
||||
|
||||
return request.headers['x-forwarded-host'];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as fileType from 'file-type';
|
||||
import { fileTypeFromBuffer } from 'file-type';
|
||||
import { isBinary } from 'istextorbinary';
|
||||
import * as mimeType from 'mime-types';
|
||||
import path from 'path';
|
||||
|
@ -29,7 +29,7 @@ export async function getStreamType(fileName: string, stream: PassThrough): Prom
|
|||
const firstBytes = await readFirstBytes(stream);
|
||||
const binary = isBinary(fileName, firstBytes);
|
||||
if (binary) {
|
||||
const result = await fileType.fromBuffer(firstBytes);
|
||||
const result = await fileTypeFromBuffer(firstBytes);
|
||||
return result?.mime ?? DEFAULT_TYPE;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import type { Edge } from '../types/edge.type';
|
||||
import type { Paginated } from '../types/paginated.type';
|
||||
import type { Edge } from '../types/edge.type.js';
|
||||
import type { Paginated } from '../types/paginated.type.js';
|
||||
|
||||
export function createCursor(offset: number) {
|
||||
return Buffer.from(`${offset}`, 'utf8').toString('base64');
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import type { IdentifiedReference } from '@mikro-orm/core';
|
||||
import { BeforeCreate, Entity, EventArgs, Property } from '@mikro-orm/core';
|
||||
import { BeforeCreate, Entity, type EventArgs, Property } from '@mikro-orm/core';
|
||||
import { ObjectType } from '@nestjs/graphql';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { config } from '../config';
|
||||
import type { ResourceLocations } from '../types/resource-locations.type';
|
||||
import type { User } from '../modules/user/user.entity';
|
||||
import { getHostFromRequest } from './get-host-from-request';
|
||||
import { config } from '../config.js';
|
||||
import type { ResourceLocations } from '../types/resource-locations.type.js';
|
||||
import type { User } from '../modules/user/user.entity.js';
|
||||
import { getHostFromRequest } from './get-host-from-request.js';
|
||||
|
||||
@Entity({ abstract: true })
|
||||
@ObjectType({ isAbstract: true })
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import nodemailer from 'nodemailer';
|
||||
import { config } from '../config';
|
||||
import { config } from '../config.js';
|
||||
|
||||
const transport = config.email && nodemailer.createTransport(config.email.smtp);
|
||||
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
||||
import type { GqlContextType } from '@nestjs/graphql';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
export class SerializerInterceptor implements NestInterceptor {
|
||||
async intercept(context: ExecutionContext, next: CallHandler) {
|
||||
if (context.getType<GqlContextType>() === 'graphql') return next.handle();
|
||||
const response = context.switchToHttp().getResponse<FastifyReply>();
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
if (data === null || data === undefined) return data;
|
||||
if (typeof data === 'string') return data;
|
||||
if (typeof data === 'object') {
|
||||
void response.header('Content-Type', 'application/json');
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
throw new Error(`Do not know how to serialize "${data}"`);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,19 +1,45 @@
|
|||
import cookie from '@fastify/cookie';
|
||||
import helmet from '@fastify/helmet';
|
||||
import type { FastifyMultipartOptions } from '@fastify/multipart';
|
||||
import multipart from '@fastify/multipart';
|
||||
import fastifyCookie from '@fastify/cookie';
|
||||
import fastifyHelmet from '@fastify/helmet';
|
||||
import fastifyMultipart from '@fastify/multipart';
|
||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { FastifyAdapter } from '@nestjs/platform-fastify';
|
||||
import createApp from 'fastify';
|
||||
import { config } from './config';
|
||||
import { migrate } from './migrate';
|
||||
import { AppModule } from './modules/app.module';
|
||||
import { HostGuard } from './modules/host/host.guard';
|
||||
import { SerializerInterceptor } from './helpers/serializer.interceptor';
|
||||
import { fastify } from 'fastify';
|
||||
import { config } from './config.js';
|
||||
import { migrate } from './migrate.js';
|
||||
import { AppModule } from './modules/app.module.js';
|
||||
import { HostGuard } from './modules/host/host.guard.js';
|
||||
|
||||
const limits: FastifyMultipartOptions = {
|
||||
await migrate();
|
||||
|
||||
const logger = new Logger('bootstrap');
|
||||
const server = fastify({
|
||||
trustProxy: process.env.TRUST_PROXY === 'true',
|
||||
maxParamLength: 500,
|
||||
bodyLimit: config.uploadLimit,
|
||||
logger: {
|
||||
level: 'trace',
|
||||
},
|
||||
});
|
||||
|
||||
const adapter = new FastifyAdapter(server as any);
|
||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, adapter);
|
||||
app.useGlobalGuards(new HostGuard());
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await app.register(fastifyCookie);
|
||||
await app.register(fastifyHelmet.default);
|
||||
await app.register(fastifyMultipart.default, {
|
||||
limits: {
|
||||
fieldNameSize: 100,
|
||||
fieldSize: 100,
|
||||
|
@ -21,45 +47,9 @@ const limits: FastifyMultipartOptions = {
|
|||
files: 1,
|
||||
headerPairs: 20,
|
||||
},
|
||||
};
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
const logger = new Logger('bootstrap');
|
||||
logger.debug(`Checking for and running migrations`);
|
||||
await migrate();
|
||||
logger.debug(`Migrations check complete`);
|
||||
const fastify = createApp({
|
||||
trustProxy: process.env.TRUST_PROXY === 'true',
|
||||
maxParamLength: 500,
|
||||
bodyLimit: config.uploadLimit,
|
||||
});
|
||||
|
||||
const adapter = new FastifyAdapter(fastify as any);
|
||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, adapter);
|
||||
app.useGlobalInterceptors(new SerializerInterceptor());
|
||||
app.useGlobalGuards(new HostGuard());
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await app.register(cookie as any);
|
||||
await app.register(helmet);
|
||||
|
||||
await app.register(multipart as any, limits);
|
||||
await app.listen(8080, '0.0.0.0', (error, address) => {
|
||||
if (error) throw error;
|
||||
logger.log(`Listening at ${address}`);
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
await app.listen(8080, '0.0.0.0', (error, address) => {
|
||||
if (error) throw error;
|
||||
logger.log(`Listening at ${address}`);
|
||||
});
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import type { Options } from '@mikro-orm/core';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
import type { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { checkForOldDatabase, migrateOldDatabase } from './helpers/migrate-old-database';
|
||||
import mikroOrmConfig, { migrationsTableName, ormLogger } from './orm';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { checkForOldDatabase, migrateOldDatabase } from './helpers/migrate-old-database.js';
|
||||
import mikroOrmConfig, { migrationsTableName, ormLogger } from './orm.js';
|
||||
|
||||
const logger = new Logger('migrate');
|
||||
|
||||
export const migrate = async (
|
||||
config: Options = mikroOrmConfig,
|
||||
skipLock = process.env.SKIP_MIGRATION_LOCK === 'true'
|
||||
) => {
|
||||
logger.debug(`Checking for and running migrations`);
|
||||
const orm = await MikroORM.init(config);
|
||||
const em = orm.em.fork({ clear: true }) as EntityManager;
|
||||
const connection = em.getConnection();
|
||||
|
@ -32,4 +36,5 @@ export const migrate = async (
|
|||
});
|
||||
|
||||
await orm.close();
|
||||
logger.debug(`Migrations check complete`);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { config } from '../config';
|
||||
import { UserId } from './auth/auth.decorators';
|
||||
import { OptionalJWTAuthGuard } from './auth/guards/optional-jwt.guard';
|
||||
import { UserService } from './user/user.service';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { config } from '../config.js';
|
||||
import { UserId } from './auth/auth.decorators.js';
|
||||
import { OptionalJWTAuthGuard } from './auth/guards/optional-jwt.guard.js';
|
||||
import { UserService } from './user/user.service.js';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import MikroOrmOptions from '../orm.js';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
|
@ -5,16 +6,15 @@ import type { MercuriusDriverConfig } from '@nestjs/mercurius';
|
|||
import { MercuriusDriver } from '@nestjs/mercurius';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import MikroOrmOptions from '../orm';
|
||||
import { AppResolver } from './app.resolver';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { FileModule } from './file/file.module';
|
||||
import { HostModule } from './host/host.module';
|
||||
import { InviteModule } from './invite/invite.module';
|
||||
import { PasteModule } from './paste/paste.module';
|
||||
import { StorageModule } from './storage/storage.module';
|
||||
import { ThumbnailModule } from './thumbnail/thumbnail.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { AppResolver } from './app.resolver.js';
|
||||
import { AuthModule } from './auth/auth.module.js';
|
||||
import { FileModule } from './file/file.module.js';
|
||||
import { HostModule } from './host/host.module.js';
|
||||
import { InviteModule } from './invite/invite.module.js';
|
||||
import { PasteModule } from './paste/paste.module.js';
|
||||
import { StorageModule } from './storage/storage.module.js';
|
||||
import { ThumbnailModule } from './thumbnail/thumbnail.module.js';
|
||||
import { UserModule } from './user/user.module.js';
|
||||
|
||||
@Module({
|
||||
providers: [AppResolver],
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { UseGuards } from '@nestjs/common';
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
import { MicroHost } from '../classes/MicroHost';
|
||||
import { config } from '../config';
|
||||
import type { ConfigHost } from '../types/config.type';
|
||||
import { Config } from '../types/config.type';
|
||||
import { CurrentHost, UserId } from './auth/auth.decorators';
|
||||
import { OptionalJWTAuthGuard } from './auth/guards/optional-jwt.guard';
|
||||
import { UserService } from './user/user.service';
|
||||
import { MicroHost } from '../classes/MicroHost.js';
|
||||
import { config } from '../config.js';
|
||||
import type { ConfigHost } from '../types/config.type.js';
|
||||
import { Config } from '../types/config.type.js';
|
||||
import { CurrentHost, UserId } from './auth/auth.decorators.js';
|
||||
import { OptionalJWTAuthGuard } from './auth/guards/optional-jwt.guard.js';
|
||||
import { UserService } from './user/user.service.js';
|
||||
|
||||
@Resolver(() => Config)
|
||||
export class AppResolver {
|
||||
|
@ -28,6 +28,8 @@ export class AppResolver {
|
|||
uploadLimit: config.uploadLimit,
|
||||
allowTypes: config.allowTypes ? [...config.allowTypes?.values()] : [],
|
||||
requireEmails: !!config.email,
|
||||
rootHost: this.filterHost(config.rootHost),
|
||||
currentHost: this.filterHost(currentHost),
|
||||
hosts: config.hosts
|
||||
.filter((host) => {
|
||||
if (!host.tags || !host.tags[0]) return true;
|
||||
|
@ -38,8 +40,6 @@ export class AppResolver {
|
|||
normalised: host.normalised,
|
||||
redirect: host.redirect,
|
||||
})),
|
||||
rootHost: this.filterHost(config.rootHost),
|
||||
currentHost: this.filterHost(currentHost),
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { applyDecorators, createParamDecorator, SetMetadata, UseGuards } from '@nestjs/common';
|
||||
import type { Permission } from '../../constants';
|
||||
import { getRequest } from '../../helpers/get-request';
|
||||
import { JWTAuthGuard } from './guards/jwt.guard';
|
||||
import { PermissionGuard } from './guards/permission.guard';
|
||||
import type { Permission } from '../../constants.js';
|
||||
import { getRequest } from '../../helpers/get-request.js';
|
||||
import { JWTAuthGuard } from './guards/jwt.guard.js';
|
||||
import { PermissionGuard } from './guards/permission.guard.js';
|
||||
|
||||
export const RequirePermissions = (...permissions: Permission[]) => {
|
||||
let aggregate = 0;
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { config } from '../../config';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JWTStrategy } from './strategies/jwt.strategy';
|
||||
import { User } from '../user/user.entity';
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
import { config } from '../../config.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
import { AuthResolver } from './auth.resolver.js';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { JWTStrategy } from './strategies/jwt.strategy.js';
|
||||
|
||||
@Module({
|
||||
controllers: [],
|
||||
providers: [AuthResolver, AuthService, JWTStrategy],
|
||||
exports: [AuthService],
|
||||
imports: [
|
||||
|
|
|
@ -4,13 +4,13 @@ import { UseGuards } from '@nestjs/common';
|
|||
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import ms from 'ms';
|
||||
import { config } from '../../config';
|
||||
import { User } from '../user/user.entity';
|
||||
import { UserId } from './auth.decorators';
|
||||
import { AuthService, TokenType } from './auth.service';
|
||||
import { OTPEnabledDto } from './dto/otp-enabled.dto';
|
||||
import { JWTAuthGuard } from './guards/jwt.guard';
|
||||
import type { JWTPayloadUser } from './strategies/jwt.strategy';
|
||||
import { config } from '../../config.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
import { UserId } from './auth.decorators.js';
|
||||
import { AuthService, TokenType } from './auth.service.js';
|
||||
import { OTPEnabledDto } from './dto/otp-enabled.dto.js';
|
||||
import { JWTAuthGuard } from './guards/jwt.guard.js';
|
||||
import type { JWTPayloadUser } from './strategies/jwt.strategy.js';
|
||||
|
||||
@Resolver(() => User)
|
||||
export class AuthResolver {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { ExecutionContext } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { getRequest } from '../../../helpers/get-request';
|
||||
import { getRequest } from '../../../helpers/get-request.js';
|
||||
|
||||
@Injectable()
|
||||
export class JWTAuthGuard extends AuthGuard('jwt') {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { ExecutionContext } from '@nestjs/common';
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import { JWTAuthGuard } from './jwt.guard';
|
||||
import { JWTAuthGuard } from './jwt.guard.js';
|
||||
|
||||
export class OptionalJWTAuthGuard extends JWTAuthGuard {
|
||||
async canActivate(context: ExecutionContext) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Permission } from '../../../constants';
|
||||
import { getRequest } from '../../../helpers/get-request';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { Permission } from '../../../constants.js';
|
||||
import { getRequest } from '../../../helpers/get-request.js';
|
||||
import { UserService } from '../../user/user.service.js';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionGuard implements CanActivate {
|
||||
|
|
|
@ -4,9 +4,9 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { Strategy } from 'passport-jwt';
|
||||
import { config } from '../../../config';
|
||||
import { User } from '../../user/user.entity';
|
||||
import { TokenType } from '../auth.service';
|
||||
import { config } from '../../../config.js';
|
||||
import { User } from '../../user/user.entity.js';
|
||||
import { TokenType } from '../auth.service.js';
|
||||
|
||||
export interface JWTPayloadUser {
|
||||
id: string;
|
||||
|
|
|
@ -14,15 +14,15 @@ import {
|
|||
Res,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { config } from '../../config';
|
||||
import { UserId } from '../auth/auth.decorators';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard';
|
||||
import { HostService } from '../host/host.service';
|
||||
import { LinkService } from '../link/link.service';
|
||||
import { Paste } from '../paste/paste.entity';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { FileService } from './file.service';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { config } from '../../config.js';
|
||||
import { UserId } from '../auth/auth.decorators.js';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard.js';
|
||||
import { HostService } from '../host/host.service.js';
|
||||
import { LinkService } from '../link/link.service.js';
|
||||
import { Paste } from '../paste/paste.entity.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { FileService } from './file.service.js';
|
||||
|
||||
@Controller()
|
||||
export class FileController {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
Embedded,
|
||||
Entity,
|
||||
IdentifiedReference,
|
||||
Index,
|
||||
LoadStrategy,
|
||||
ManyToOne,
|
||||
|
@ -9,17 +8,18 @@ import {
|
|||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
type IdentifiedReference,
|
||||
} from '@mikro-orm/core';
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { checkThumbnailSupport } from '@ryanke/thumbnail-generator';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import mimeType from 'mime-types';
|
||||
import { generateDeleteKey } from '../../helpers/generate-delete-key.helper';
|
||||
import { Resource } from '../../helpers/resource.entity-base';
|
||||
import { Paginated } from '../../types/paginated.type';
|
||||
import { Thumbnail } from '../thumbnail/thumbnail.entity';
|
||||
import { User } from '../user/user.entity';
|
||||
import { FileMetadata } from './file-metadata.embeddable';
|
||||
import { generateDeleteKey } from '../../helpers/generate-delete-key.helper.js';
|
||||
import { Resource } from '../../helpers/resource.entity-base.js';
|
||||
import { Paginated } from '../../types/paginated.type.js';
|
||||
import { Thumbnail } from '../thumbnail/thumbnail.entity.js';
|
||||
import { ThumbnailService } from '../thumbnail/thumbnail.service.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
import { FileMetadata } from './file-metadata.embeddable.js';
|
||||
|
||||
@Entity({ tableName: 'files' })
|
||||
@ObjectType()
|
||||
|
@ -78,7 +78,7 @@ export class File extends Resource {
|
|||
const prefix = this.type.startsWith('video') ? '/v' : this.type.startsWith('image') ? '/i' : '/f';
|
||||
const viewPath = `${prefix}/${this.id}`;
|
||||
const directPath = `${prefix}/${this.id}.${extension}`;
|
||||
const thumbnailUrl = checkThumbnailSupport(this.type) ? `/t/${this.id}` : undefined;
|
||||
const thumbnailUrl = ThumbnailService.checkSupport(this.type) ? `/t/${this.id}` : undefined;
|
||||
const deletePath = this.deleteKey ? `${prefix}/${this.id}?deleteKey=${this.deleteKey}` : undefined;
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HostModule } from '../host/host.module';
|
||||
import { LinkModule } from '../link/link.module';
|
||||
import { Paste } from '../paste/paste.entity';
|
||||
import { StorageModule } from '../storage/storage.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { FileController } from './file.controller';
|
||||
import { File } from './file.entity';
|
||||
import { FileResolver } from './file.resolver';
|
||||
import { FileService } from './file.service';
|
||||
import { HostModule } from '../host/host.module.js';
|
||||
import { LinkModule } from '../link/link.module.js';
|
||||
import { Paste } from '../paste/paste.entity.js';
|
||||
import { StorageModule } from '../storage/storage.module.js';
|
||||
import { UserModule } from '../user/user.module.js';
|
||||
import { FileController } from './file.controller.js';
|
||||
import { File } from './file.entity.js';
|
||||
import { FileResolver } from './file.resolver.js';
|
||||
import { FileService } from './file.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [StorageModule, HostModule, UserModule, LinkModule, MikroOrmModule.forFeature([File, Paste])],
|
||||
|
|
|
@ -4,10 +4,10 @@ import { EntityRepository } from '@mikro-orm/postgresql';
|
|||
import { ForbiddenException, UseGuards } from '@nestjs/common';
|
||||
import { Args, ID, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { ResourceLocations } from '../../types/resource-locations.type';
|
||||
import { UserId } from '../auth/auth.decorators';
|
||||
import { OptionalJWTAuthGuard } from '../auth/guards/optional-jwt.guard';
|
||||
import { File } from './file.entity';
|
||||
import { ResourceLocations } from '../../types/resource-locations.type.js';
|
||||
import { UserId } from '../auth/auth.decorators.js';
|
||||
import { OptionalJWTAuthGuard } from '../auth/guards/optional-jwt.guard.js';
|
||||
import { File } from './file.entity.js';
|
||||
|
||||
@Resolver(() => File)
|
||||
export class FileResolver {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import type { Multipart } from '@fastify/multipart';
|
||||
import type { MultipartFile } from '@fastify/multipart';
|
||||
import { EntityRepository, MikroORM, UseRequestContext } from '@mikro-orm/core';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import type { OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException, PayloadTooLargeException } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import bytes from 'bytes';
|
||||
import contentRange from 'content-range';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
|
@ -12,24 +13,23 @@ import { DateTime } from 'luxon';
|
|||
import mime from 'mime-types';
|
||||
import sharp from 'sharp';
|
||||
import { PassThrough } from 'stream';
|
||||
import xbytes from 'xbytes';
|
||||
import type { MicroHost } from '../../classes/MicroHost';
|
||||
import { config } from '../../config';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper';
|
||||
import { getStreamType } from '../../helpers/get-stream-type.helper';
|
||||
import { HostService } from '../host/host.service';
|
||||
import { StorageService } from '../storage/storage.service';
|
||||
import type { User } from '../user/user.entity';
|
||||
import { File } from './file.entity';
|
||||
import type { MicroHost } from '../../classes/MicroHost.js';
|
||||
import { config } from '../../config.js';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper.js';
|
||||
import { getStreamType } from '../../helpers/get-stream-type.helper.js';
|
||||
import { HostService } from '../host/host.service.js';
|
||||
import { StorageService } from '../storage/storage.service.js';
|
||||
import type { User } from '../user/user.entity.js';
|
||||
import type { File } from './file.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class FileService implements OnApplicationBootstrap {
|
||||
private readonly logger = new Logger(FileService.name);
|
||||
constructor(
|
||||
@InjectRepository(File) private readonly fileRepo: EntityRepository<File>,
|
||||
@InjectRepository('File') private readonly fileRepo: EntityRepository<File>,
|
||||
private readonly storageService: StorageService,
|
||||
private readonly hostService: HostService,
|
||||
private readonly orm: MikroORM
|
||||
protected readonly orm: MikroORM
|
||||
) {}
|
||||
|
||||
async getFile(id: string, request: FastifyRequest) {
|
||||
|
@ -42,7 +42,7 @@ export class FileService implements OnApplicationBootstrap {
|
|||
}
|
||||
|
||||
async createFile(
|
||||
multipart: Multipart,
|
||||
multipart: MultipartFile,
|
||||
request: FastifyRequest,
|
||||
owner: User,
|
||||
host: MicroHost | undefined
|
||||
|
@ -51,7 +51,7 @@ export class FileService implements OnApplicationBootstrap {
|
|||
if (!request.headers['content-length']) throw new BadRequestException('Missing "Content-Length" header.');
|
||||
const contentLength = Number(request.headers['content-length']);
|
||||
if (Number.isNaN(contentLength) || contentLength >= config.uploadLimit) {
|
||||
const size = xbytes(Number(request.headers['content-length']));
|
||||
const size = bytes.parse(Number(request.headers['content-length']));
|
||||
this.logger.warn(
|
||||
`User ${owner.id} tried uploading a ${size} file, which is over the configured upload size limit.`
|
||||
);
|
||||
|
@ -69,7 +69,7 @@ export class FileService implements OnApplicationBootstrap {
|
|||
|
||||
const conversion = config.conversions?.find((conversion) => {
|
||||
if (!conversion.from.has(fileType)) return false;
|
||||
if (conversion.minSize && contentLength < conversion.minSize) return false;
|
||||
if (conversion.minSize && contentLength < conversion.minSize) return false;
|
||||
if (conversion.to === fileType) return false; // dont convert to the same type
|
||||
return true;
|
||||
});
|
||||
|
@ -175,7 +175,6 @@ export class FileService implements OnApplicationBootstrap {
|
|||
async purgeFiles() {
|
||||
if (!config.purge) return;
|
||||
const createdBefore = new Date(Date.now() - config.purge.afterTime);
|
||||
|
||||
const files = await this.fileRepo.find({
|
||||
size: {
|
||||
$gte: config.purge.overLimit,
|
||||
|
@ -186,7 +185,7 @@ export class FileService implements OnApplicationBootstrap {
|
|||
});
|
||||
|
||||
for (const file of files) {
|
||||
const size = xbytes(file.size, { space: false });
|
||||
const size = bytes.format(file.size);
|
||||
const age = DateTime.fromJSDate(file.createdAt).toRelative();
|
||||
await this.fileRepo.removeAndFlush(file);
|
||||
this.logger.log(`Purging ${file.id} (${size}, ${age})`);
|
||||
|
@ -199,7 +198,7 @@ export class FileService implements OnApplicationBootstrap {
|
|||
|
||||
onApplicationBootstrap() {
|
||||
if (config.purge) {
|
||||
const size = xbytes(config.purge.overLimit, { space: false });
|
||||
const size = bytes.format(config.purge.overLimit);
|
||||
// todo: swap out luxon for dayjs
|
||||
const age = DateTime.local().minus(config.purge.afterTime).toRelative();
|
||||
this.logger.warn(`Purging files is enabled for files over ${size} uploaded more than ${age}.`);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { EventArgs, EventSubscriber } from '@mikro-orm/core';
|
||||
import { Subscriber } from '@mikro-orm/core';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { StorageService } from '../storage/storage.service';
|
||||
import { File } from './file.entity';
|
||||
import { StorageService } from '../storage/storage.service.js';
|
||||
import { File } from './file.entity.js';
|
||||
|
||||
@Injectable()
|
||||
@Subscriber()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { config } from '../../config';
|
||||
import { getRequest } from '../../helpers/get-request';
|
||||
import { config } from '../../config.js';
|
||||
import { getRequest } from '../../helpers/get-request.js';
|
||||
|
||||
@Injectable()
|
||||
export class HostGuard implements CanActivate {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { HostService } from './host.service';
|
||||
import { UserModule } from '../user/user.module.js';
|
||||
import { HostService } from './host.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule],
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import normalizeUrl from 'normalize-url';
|
||||
import type { MicroHost } from '../../classes/MicroHost';
|
||||
import { config } from '../../config';
|
||||
import { randomItem } from '../../helpers/random-item.helper';
|
||||
import type { User } from '../user/user.entity';
|
||||
import type { MicroHost } from '../../classes/MicroHost.js';
|
||||
import { config } from '../../config.js';
|
||||
import { randomItem } from '../../helpers/random-item.helper.js';
|
||||
import type { User } from '../user/user.entity.js';
|
||||
|
||||
export class HostService {
|
||||
formatHostUrl(url: string, username: string, path?: string | null) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { Permission } from '../../constants';
|
||||
import { RequirePermissions, UserId } from '../auth/auth.decorators';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard';
|
||||
import { InviteService } from './invite.service';
|
||||
import { Permission } from '../../constants.js';
|
||||
import { RequirePermissions, UserId } from '../auth/auth.decorators.js';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard.js';
|
||||
import { InviteService } from './invite.service.js';
|
||||
|
||||
@Controller()
|
||||
export class InviteController {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Entity, ManyToOne, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { config } from '../../config';
|
||||
import { generateDeleteKey } from '../../helpers/generate-delete-key.helper';
|
||||
import { User } from '../user/user.entity';
|
||||
import { config } from '../../config.js';
|
||||
import { generateDeleteKey } from '../../helpers/generate-delete-key.helper.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
|
||||
@Entity({ tableName: 'invites' })
|
||||
@ObjectType()
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { User } from '../user/user.entity';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { InviteController } from './invite.controller';
|
||||
import { Invite } from './invite.entity';
|
||||
import { InviteResolver } from './invite.resolver';
|
||||
import { InviteService } from './invite.service';
|
||||
import { AuthModule } from '../auth/auth.module.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
import { UserModule } from '../user/user.module.js';
|
||||
import { InviteController } from './invite.controller.js';
|
||||
import { Invite } from './invite.entity.js';
|
||||
import { InviteResolver } from './invite.resolver.js';
|
||||
import { InviteService } from './invite.service.js';
|
||||
|
||||
@Module({
|
||||
controllers: [InviteController],
|
||||
|
|
|
@ -2,10 +2,10 @@ import { InjectRepository } from '@mikro-orm/nestjs';
|
|||
import { EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, ID, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { Permission } from '../../constants';
|
||||
import { RequirePermissions, UserId } from '../auth/auth.decorators';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard';
|
||||
import { Invite } from './invite.entity';
|
||||
import { Permission } from '../../constants.js';
|
||||
import { RequirePermissions, UserId } from '../auth/auth.decorators.js';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard.js';
|
||||
import { Invite } from './invite.entity.js';
|
||||
|
||||
@Resolver(() => Invite)
|
||||
export class InviteResolver {
|
||||
|
|
|
@ -2,9 +2,9 @@ import { EntityRepository, MikroORM, UseRequestContext } from '@mikro-orm/core';
|
|||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import type { OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Permission } from '../../constants';
|
||||
import { User } from '../user/user.entity';
|
||||
import { Invite } from './invite.entity';
|
||||
import { Permission } from '../../constants.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
import { Invite } from './invite.entity.js';
|
||||
|
||||
export interface JWTPayloadInvite {
|
||||
id: string;
|
||||
|
@ -23,8 +23,8 @@ export class InviteService implements OnApplicationBootstrap {
|
|||
|
||||
async create(inviterId: string | null, permissions: Permission | null) {
|
||||
const invite = this.inviteRepo.create({
|
||||
inviter: inviterId,
|
||||
permissions: permissions,
|
||||
inviter: inviterId || undefined,
|
||||
permissions: permissions || undefined,
|
||||
});
|
||||
|
||||
await this.inviteRepo.persistAndFlush(invite);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Controller, Get, Param, Request, Res } from '@nestjs/common';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { Link } from './link.entity';
|
||||
import { LinkService } from './link.service';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { Link } from './link.entity.js';
|
||||
import { LinkService } from './link.service.js';
|
||||
|
||||
@Controller()
|
||||
export class LinkController {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Entity, IdentifiedReference, ManyToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Entity, type IdentifiedReference, ManyToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper';
|
||||
import { User } from '../user/user.entity';
|
||||
import { Resource } from '../../helpers/resource.entity-base';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
import { Resource } from '../../helpers/resource.entity-base.js';
|
||||
|
||||
@Entity({ tableName: 'links' })
|
||||
@ObjectType()
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HostModule } from '../host/host.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { LinkController } from './link.controller';
|
||||
import { Link } from './link.entity';
|
||||
import { LinkResolver } from './link.resolver';
|
||||
import { LinkService } from './link.service';
|
||||
import { HostModule } from '../host/host.module.js';
|
||||
import { UserModule } from '../user/user.module.js';
|
||||
import { LinkController } from './link.controller.js';
|
||||
import { Link } from './link.entity.js';
|
||||
import { LinkResolver } from './link.resolver.js';
|
||||
import { LinkService } from './link.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [LinkModule, UserModule, HostModule, MikroOrmModule.forFeature([Link])],
|
||||
|
|
|
@ -2,12 +2,12 @@ import { InjectRepository } from '@mikro-orm/nestjs';
|
|||
import { EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, ID, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { ResourceLocations } from '../../types/resource-locations.type';
|
||||
import { UserId } from '../auth/auth.decorators';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard';
|
||||
import { HostService } from '../host/host.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { Link } from './link.entity';
|
||||
import { ResourceLocations } from '../../types/resource-locations.type.js';
|
||||
import { UserId } from '../auth/auth.decorators.js';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard.js';
|
||||
import { HostService } from '../host/host.service.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { Link } from './link.entity.js';
|
||||
|
||||
@Resolver(() => Link)
|
||||
export class LinkResolver {
|
||||
|
|
|
@ -2,8 +2,8 @@ import { InjectRepository } from '@mikro-orm/nestjs';
|
|||
import { EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import type { MicroHost } from '../../classes/MicroHost';
|
||||
import { Link } from './link.entity';
|
||||
import type { MicroHost } from '../../classes/MicroHost.js';
|
||||
import { Link } from './link.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class LinkService {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Controller, Get, Param, Req } from '@nestjs/common';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { PasteService } from './paste.service';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { PasteService } from './paste.service.js';
|
||||
|
||||
@Controller('paste')
|
||||
export class PasteController {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Entity, IdentifiedReference, ManyToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Entity, type IdentifiedReference, ManyToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Field, ID, InputType, ObjectType } from '@nestjs/graphql';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { IsBoolean, IsNumber, IsOptional, IsString, Length } from 'class-validator';
|
||||
import mime from 'mime-types';
|
||||
import { config } from '../../config';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper';
|
||||
import { Resource } from '../../helpers/resource.entity-base';
|
||||
import { Paginated } from '../../types/paginated.type';
|
||||
import { User } from '../user/user.entity';
|
||||
import { config } from '../../config.js';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper.js';
|
||||
import { Resource } from '../../helpers/resource.entity-base.js';
|
||||
import { Paginated } from '../../types/paginated.type.js';
|
||||
import { User } from '../user/user.entity.js';
|
||||
|
||||
@Entity({ tableName: 'pastes' })
|
||||
@ObjectType({ isAbstract: true })
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HostModule } from '../host/host.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { PasteController } from './paste.controller';
|
||||
import { Paste } from './paste.entity';
|
||||
import { PasteResolver } from './paste.resolver';
|
||||
import { PasteService } from './paste.service';
|
||||
import { HostModule } from '../host/host.module.js';
|
||||
import { UserModule } from '../user/user.module.js';
|
||||
import { PasteController } from './paste.controller.js';
|
||||
import { Paste } from './paste.entity.js';
|
||||
import { PasteResolver } from './paste.resolver.js';
|
||||
import { PasteService } from './paste.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([Paste]), HostModule, UserModule],
|
||||
|
|
|
@ -3,14 +3,14 @@ import { InjectRepository } from '@mikro-orm/nestjs';
|
|||
import { EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { BadRequestException, UseGuards } from '@nestjs/common';
|
||||
import { Args, ID, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { generateContentId, generateParanoidId } from '../../helpers/generate-content-id.helper';
|
||||
import { ResourceLocations } from '../../types/resource-locations.type';
|
||||
import { UserId } from '../auth/auth.decorators';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard';
|
||||
import { OptionalJWTAuthGuard } from '../auth/guards/optional-jwt.guard';
|
||||
import { HostService } from '../host/host.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { CreatePasteDto, Paste } from './paste.entity';
|
||||
import { generateContentId, generateParanoidId } from '../../helpers/generate-content-id.helper.js';
|
||||
import { ResourceLocations } from '../../types/resource-locations.type.js';
|
||||
import { UserId } from '../auth/auth.decorators.js';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard.js';
|
||||
import { OptionalJWTAuthGuard } from '../auth/guards/optional-jwt.guard.js';
|
||||
import { HostService } from '../host/host.service.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { CreatePasteDto, Paste } from './paste.entity.js';
|
||||
|
||||
@Resolver(() => Paste)
|
||||
export class PasteResolver {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { InjectRepository } from '@mikro-orm/nestjs';
|
|||
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { Paste } from './paste.entity';
|
||||
import { Paste } from './paste.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class PasteService {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { StorageService } from './storage.service';
|
||||
import { StorageService } from './storage.service.js';
|
||||
|
||||
@Module({
|
||||
providers: [StorageService],
|
||||
|
|
|
@ -3,13 +3,10 @@ import crypto from 'crypto';
|
|||
import fs from 'fs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'path';
|
||||
import stream from 'stream';
|
||||
import getSizeTransform from 'stream-size';
|
||||
import { promisify } from 'util';
|
||||
import { ExifTransformer } from '../../classes/ExifTransformer';
|
||||
import { config } from '../../config';
|
||||
|
||||
const pipeline = promisify(stream.pipeline);
|
||||
import { ExifTransformer } from 'src/classes/ExifTransformer.js';
|
||||
import { default as getSizeTransform } from 'stream-size';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { config } from '../../config.js';
|
||||
|
||||
@Injectable()
|
||||
export class StorageService {
|
||||
|
@ -28,7 +25,7 @@ export class StorageService {
|
|||
try {
|
||||
const hashStream = crypto.createHash('sha256');
|
||||
const exifTransform = new ExifTransformer();
|
||||
const sizeTransform = getSizeTransform(config.uploadLimit);
|
||||
const sizeTransform = getSizeTransform.default(config.uploadLimit);
|
||||
const writeStream = fs.createWriteStream(uploadPath);
|
||||
await Promise.all([
|
||||
// prettier-ignore
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Controller, Get, Param, Req, Res } from '@nestjs/common';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { ThumbnailService } from './thumbnail.service';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { ThumbnailService } from './thumbnail.service.js';
|
||||
|
||||
@Controller()
|
||||
export class ThumbnailController {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { BlobType, Entity, OneToOne, OptionalProps, PrimaryKeyType, Property } from '@mikro-orm/core';
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { File } from '../file/file.entity';
|
||||
import { File } from '../file/file.entity.js';
|
||||
|
||||
@Entity({ tableName: 'thumbnails' })
|
||||
@ObjectType()
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { File } from '../file/file.entity';
|
||||
import { FileModule } from '../file/file.module';
|
||||
import { StorageModule } from '../storage/storage.module';
|
||||
import { ThumbnailController } from './thumbnail.controller';
|
||||
import { Thumbnail } from './thumbnail.entity';
|
||||
import { ThumbnailService } from './thumbnail.service';
|
||||
import { File } from '../file/file.entity.js';
|
||||
import { FileModule } from '../file/file.module.js';
|
||||
import { StorageModule } from '../storage/storage.module.js';
|
||||
import { ThumbnailController } from './thumbnail.controller.js';
|
||||
import { Thumbnail } from './thumbnail.entity.js';
|
||||
import { ThumbnailService } from './thumbnail.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [StorageModule, FileModule, MikroOrmModule.forFeature([Thumbnail, File])],
|
||||
|
|
|
@ -1,23 +1,46 @@
|
|||
import { EntityRepository } from '@mikro-orm/core';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { checkThumbnailSupport, generateThumbnailToStream } from '@ryanke/thumbnail-generator';
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { once } from 'events';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import getStream from 'get-stream';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { readdir, readFile, rm, stat } from 'fs/promises';
|
||||
import { DateTime } from 'luxon';
|
||||
import mime from 'mime-types';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { File } from '../file/file.entity';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { StorageService } from '../storage/storage.service';
|
||||
import { Thumbnail } from './thumbnail.entity';
|
||||
import type { File } from '../file/file.entity.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { StorageService } from '../storage/storage.service.js';
|
||||
import type { Thumbnail } from './thumbnail.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class ThumbnailService {
|
||||
private static readonly THUMBNAIL_SIZE = 200;
|
||||
private static readonly THUMBNAIL_TYPE = 'image/webp';
|
||||
private static readonly IMAGE_TYPES = new Set(
|
||||
Object.keys(sharp.format)
|
||||
.map((key) => mime.lookup(key))
|
||||
.filter((key) => key && key.startsWith('image'))
|
||||
);
|
||||
|
||||
private static readonly VIDEO_TYPES = new Set([
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/x-matroska',
|
||||
'video/x-ms-wmv',
|
||||
'video/x-m4v',
|
||||
'video/x-flv',
|
||||
]);
|
||||
|
||||
private readonly log = new Logger(ThumbnailService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Thumbnail) private readonly thumbnailRepo: EntityRepository<Thumbnail>,
|
||||
@InjectRepository(File) private readonly fileRepo: EntityRepository<File>,
|
||||
@InjectRepository('Thumbnail') private readonly thumbnailRepo: EntityRepository<Thumbnail>,
|
||||
@InjectRepository('File') private readonly fileRepo: EntityRepository<File>,
|
||||
private readonly storageService: StorageService,
|
||||
private readonly fileService: FileService
|
||||
) {}
|
||||
|
@ -28,25 +51,28 @@ export class ThumbnailService {
|
|||
|
||||
async createThumbnail(file: File) {
|
||||
const start = Date.now();
|
||||
const supported = checkThumbnailSupport(file.type);
|
||||
if (!supported) {
|
||||
|
||||
let data: Buffer;
|
||||
if (ThumbnailService.IMAGE_TYPES.has(file.type)) {
|
||||
data = await this.createImageThumbnail(file);
|
||||
} else if (ThumbnailService.VIDEO_TYPES.has(file.type)) {
|
||||
data = await this.createVideoThumbnail(file);
|
||||
} else {
|
||||
throw new BadRequestException('That file type does not support thumbnails.');
|
||||
}
|
||||
|
||||
const filePath = this.storageService.getPathFromHash(file.hash);
|
||||
const thumbnailStream = await generateThumbnailToStream(file.type, filePath, {
|
||||
type: ThumbnailService.THUMBNAIL_TYPE,
|
||||
size: { height: ThumbnailService.THUMBNAIL_SIZE },
|
||||
});
|
||||
|
||||
const data = await getStream.buffer(thumbnailStream);
|
||||
|
||||
// todo: fileMetadata should be added elsewhere or maybe generated by the thumbnail generator
|
||||
const fileMetadata = await sharp(filePath).metadata();
|
||||
file.metadata = { height: fileMetadata.height, width: fileMetadata.width };
|
||||
|
||||
// todo: thumbnailMetadata should be generated by the thumbnail generator
|
||||
// todo: ideally we could extract both file and thumbnail metadata during
|
||||
// thumbnail generation, saving two additional file reads.
|
||||
const thumbnailMetadata = await sharp(data).metadata();
|
||||
|
||||
if (ThumbnailService.IMAGE_TYPES.has(file.type)) {
|
||||
// todo: this should probably be done elsewhere, this is just a convenient
|
||||
// time to grab this data.
|
||||
const filePath = this.storageService.getPathFromHash(file.hash);
|
||||
const fileMetadata = await sharp(filePath).metadata();
|
||||
file.metadata = { height: fileMetadata.height, width: fileMetadata.width };
|
||||
}
|
||||
|
||||
const duration = Date.now() - start;
|
||||
const thumbnail = this.thumbnailRepo.create({
|
||||
data: data,
|
||||
|
@ -64,6 +90,66 @@ export class ThumbnailService {
|
|||
return thumbnail;
|
||||
}
|
||||
|
||||
private async createImageThumbnail(file: File) {
|
||||
const supported = ThumbnailService.IMAGE_TYPES.has(file.type);
|
||||
if (!supported) throw new Error('Unsupported image type.');
|
||||
this.log.debug(`Generating thumbnail for ${file.id} (${file.type})`);
|
||||
const filePath = this.storageService.getPathFromHash(file.hash);
|
||||
return sharp(filePath).resize(ThumbnailService.THUMBNAIL_SIZE).toFormat('webp').toBuffer();
|
||||
}
|
||||
|
||||
private async createVideoThumbnail(file: File) {
|
||||
const supported = ThumbnailService.VIDEO_TYPES.has(file.type);
|
||||
if (!supported) throw new Error('Unsupported video type.');
|
||||
|
||||
const tempId = randomUUID();
|
||||
const tempDir = join(tmpdir(), `.thumbnail-workspace-${tempId}`);
|
||||
const filePath = this.storageService.getPathFromHash(file.hash);
|
||||
this.log.debug(`Generating video thumbnail at "${tempDir}"`);
|
||||
|
||||
// i have no clue why but the internet told me that doing it in multiple invocations is faster
|
||||
// and it is so whatever. maybe there is a way to do this faster, but this is already pretty fast.
|
||||
const positions = ['5%', '10%', '20%', '40%'];
|
||||
const size = `${ThumbnailService.THUMBNAIL_SIZE}x?`;
|
||||
for (let positionIndex = 0; positionIndex < positions.length; positionIndex++) {
|
||||
const percent = positions[positionIndex];
|
||||
const stream = ffmpeg(filePath).screenshot({
|
||||
count: 1,
|
||||
timemarks: [percent],
|
||||
folder: tempDir,
|
||||
size: size,
|
||||
fastSeek: true,
|
||||
filename: `%b-${positionIndex + 1}.webp`,
|
||||
});
|
||||
|
||||
await once(stream, 'end');
|
||||
}
|
||||
|
||||
const files = await readdir(tempDir);
|
||||
let largest: { size: number; path: string } | undefined;
|
||||
for (const file of files) {
|
||||
const path = join(tempDir, file);
|
||||
const stats = await stat(path);
|
||||
if (!largest || stats.size > largest.size) {
|
||||
largest = { size: stats.size, path };
|
||||
}
|
||||
}
|
||||
|
||||
if (!largest) {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
throw new Error('No thumbnails were generated');
|
||||
}
|
||||
|
||||
this.log.debug(`Largest thumbnail is at "${largest.path}", ${largest.size} bytes`);
|
||||
const content = await readFile(largest.path);
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
return content;
|
||||
}
|
||||
|
||||
static checkSupport(fileType: string) {
|
||||
return ThumbnailService.IMAGE_TYPES.has(fileType) || ThumbnailService.VIDEO_TYPES.has(fileType);
|
||||
}
|
||||
|
||||
async sendThumbnail(fileId: string, request: FastifyRequest, reply: FastifyReply) {
|
||||
const existing = await this.thumbnailRepo.findOne(fileId, { populate: ['data'] });
|
||||
if (existing) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { User } from './user.entity';
|
||||
import { User } from './user.entity.js';
|
||||
|
||||
@Entity({ tableName: 'users_verification' })
|
||||
export class UserVerification {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Controller, Get, Param, Response } from '@nestjs/common';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { UserService } from './user.service';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import { UserService } from './user.service.js';
|
||||
|
||||
@Controller()
|
||||
export class UserController {
|
||||
|
|
|
@ -11,10 +11,10 @@ import {
|
|||
} from '@mikro-orm/core';
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper';
|
||||
import { File } from '../file/file.entity';
|
||||
import { Invite } from '../invite/invite.entity';
|
||||
import { UserVerification } from './user-verification.entity';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper.js';
|
||||
import { File } from '../file/file.entity.js';
|
||||
import { Invite } from '../invite/invite.entity.js';
|
||||
import { UserVerification } from './user-verification.entity.js';
|
||||
|
||||
@Entity({ tableName: 'users' })
|
||||
@ObjectType()
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { File } from '../file/file.entity';
|
||||
import { FileModule } from '../file/file.module';
|
||||
import { InviteModule } from '../invite/invite.module';
|
||||
import { Paste } from '../paste/paste.entity';
|
||||
import { UserVerification } from './user-verification.entity';
|
||||
import { UserController } from './user.controller';
|
||||
import { User } from './user.entity';
|
||||
import { UserResolver } from './user.resolver';
|
||||
import { UserService } from './user.service';
|
||||
import { AuthModule } from '../auth/auth.module.js';
|
||||
import { File } from '../file/file.entity.js';
|
||||
import { FileModule } from '../file/file.module.js';
|
||||
import { InviteModule } from '../invite/invite.module.js';
|
||||
import { Paste } from '../paste/paste.entity.js';
|
||||
import { UserVerification } from './user-verification.entity.js';
|
||||
import { UserController } from './user.controller.js';
|
||||
import { User } from './user.entity.js';
|
||||
import { UserResolver } from './user.resolver.js';
|
||||
import { UserService } from './user.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
|
@ -5,19 +5,19 @@ import { BadRequestException, UnauthorizedException, UseGuards } from '@nestjs/c
|
|||
import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import ms from 'ms';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { paginate, parseCursor } from '../../helpers/pagination';
|
||||
import { UserId } from '../auth/auth.decorators';
|
||||
import { AuthService, TokenType } from '../auth/auth.service';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard';
|
||||
import type { JWTPayloadUser } from '../auth/strategies/jwt.strategy';
|
||||
import { File, FilePage } from '../file/file.entity';
|
||||
import { InviteService } from '../invite/invite.service';
|
||||
import { Paste, PastePage } from '../paste/paste.entity';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { ResendVerificationEmailDto } from './dto/resend-verification-email.dto';
|
||||
import { UserVerification } from './user-verification.entity';
|
||||
import { User } from './user.entity';
|
||||
import { UserService } from './user.service';
|
||||
import { paginate, parseCursor } from '../../helpers/pagination.js';
|
||||
import { UserId } from '../auth/auth.decorators.js';
|
||||
import { AuthService, TokenType } from '../auth/auth.service.js';
|
||||
import { JWTAuthGuard } from '../auth/guards/jwt.guard.js';
|
||||
import type { JWTPayloadUser } from '../auth/strategies/jwt.strategy.js';
|
||||
import { File, FilePage } from '../file/file.entity.js';
|
||||
import { InviteService } from '../invite/invite.service.js';
|
||||
import { Paste, PastePage } from '../paste/paste.entity.js';
|
||||
import { CreateUserDto } from './dto/create-user.dto.js';
|
||||
import { ResendVerificationEmailDto } from './dto/resend-verification-email.dto.js';
|
||||
import { UserVerification } from './user-verification.entity.js';
|
||||
import { User } from './user.entity.js';
|
||||
import { UserService } from './user.service.js';
|
||||
|
||||
@Resolver(() => User)
|
||||
export class UserResolver {
|
||||
|
|
|
@ -4,21 +4,21 @@ import { BadRequestException, ConflictException, ForbiddenException, Injectable
|
|||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import dedent from 'dedent';
|
||||
import { compile } from 'handlebars';
|
||||
import handlebars from 'handlebars';
|
||||
import ms from 'ms';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { config } from '../../config';
|
||||
import type { Permission } from '../../constants';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper';
|
||||
import { sendMail } from '../../helpers/send-mail.helper';
|
||||
import { File } from '../file/file.entity';
|
||||
import type { Invite } from '../invite/invite.entity';
|
||||
import { InviteService } from '../invite/invite.service';
|
||||
import { Paste } from '../paste/paste.entity';
|
||||
import type { CreateUserDto } from './dto/create-user.dto';
|
||||
import type { Pagination } from './dto/pagination.dto';
|
||||
import { UserVerification } from './user-verification.entity';
|
||||
import { User } from './user.entity';
|
||||
import { config } from '../../config.js';
|
||||
import type { Permission } from '../../constants.js';
|
||||
import { generateContentId } from '../../helpers/generate-content-id.helper.js';
|
||||
import { sendMail } from '../../helpers/send-mail.helper.js';
|
||||
import { File } from '../file/file.entity.js';
|
||||
import type { Invite } from '../invite/invite.entity.js';
|
||||
import { InviteService } from '../invite/invite.service.js';
|
||||
import { Paste } from '../paste/paste.entity.js';
|
||||
import type { CreateUserDto } from './dto/create-user.dto.js';
|
||||
import type { Pagination } from './dto/pagination.dto.js';
|
||||
import { UserVerification } from './user-verification.entity.js';
|
||||
import { User } from './user.entity.js';
|
||||
|
||||
const EMAIL_TEMPLATE_SOURCE = dedent`
|
||||
<body>
|
||||
|
@ -31,7 +31,7 @@ const EMAIL_TEMPLATE_SOURCE = dedent`
|
|||
@Injectable()
|
||||
export class UserService {
|
||||
private static readonly VERIFICATION_EXPIRY = ms('6 hours');
|
||||
private static readonly EMAIL_TEMPLATE = compile<{ verifyUrl: string }>(EMAIL_TEMPLATE_SOURCE);
|
||||
private static readonly EMAIL_TEMPLATE = handlebars.compile<{ verifyUrl: string }>(EMAIL_TEMPLATE_SOURCE);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(User) private readonly userRepo: EntityRepository<User>,
|
||||
|
@ -134,6 +134,7 @@ export class UserService {
|
|||
username: data.username,
|
||||
invite: invite.id,
|
||||
permissions: invite.permissions ?? 0,
|
||||
otpEnabled: false,
|
||||
});
|
||||
|
||||
if (data.email) {
|
||||
|
|
|
@ -3,16 +3,17 @@
|
|||
import { LoadStrategy } from '@mikro-orm/core';
|
||||
import type { MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs';
|
||||
import { Logger, NotFoundException } from '@nestjs/common';
|
||||
import { join } from 'path';
|
||||
import { config } from './config';
|
||||
import { FileMetadata } from './modules/file/file-metadata.embeddable';
|
||||
import { File } from './modules/file/file.entity';
|
||||
import { Invite } from './modules/invite/invite.entity';
|
||||
import { Link } from './modules/link/link.entity';
|
||||
import { Paste } from './modules/paste/paste.entity';
|
||||
import { Thumbnail } from './modules/thumbnail/thumbnail.entity';
|
||||
import { UserVerification } from './modules/user/user-verification.entity';
|
||||
import { User } from './modules/user/user.entity';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { config } from './config.js';
|
||||
import { FileMetadata } from './modules/file/file-metadata.embeddable.js';
|
||||
import { File } from './modules/file/file.entity.js';
|
||||
import { Invite } from './modules/invite/invite.entity.js';
|
||||
import { Link } from './modules/link/link.entity.js';
|
||||
import { Paste } from './modules/paste/paste.entity.js';
|
||||
import { Thumbnail } from './modules/thumbnail/thumbnail.entity.js';
|
||||
import { UserVerification } from './modules/user/user-verification.entity.js';
|
||||
import { User } from './modules/user/user.entity.js';
|
||||
|
||||
export const ormLogger = new Logger('MikroORM');
|
||||
export const migrationsTableName = 'mikro_orm_migrations';
|
||||
|
@ -30,7 +31,7 @@ export default {
|
|||
throw new NotFoundException();
|
||||
},
|
||||
migrations: {
|
||||
path: join(__dirname, 'migrations'),
|
||||
path: join(dirname(fileURLToPath(import.meta.url)), 'migrations'),
|
||||
tableName: migrationsTableName,
|
||||
},
|
||||
} as MikroOrmModuleSyncOptions;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable unicorn/no-keyword-prefix */
|
||||
import type { Type } from '@nestjs/common';
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
import { Edge } from './edge.type';
|
||||
import { Edge } from './edge.type.js';
|
||||
|
||||
export interface Paginated<T> {
|
||||
totalCount: number;
|
||||
|
|
|
@ -1,13 +1,38 @@
|
|||
{
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
// https://www.npmjs.com/package/@tsconfig/node16
|
||||
// https://github.com/sindresorhus/tsconfig/blob/main/tsconfig.json
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"moduleDetection": "force",
|
||||
"target": "es2021", // node 16
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"pretty": true,
|
||||
"newLine": "lf",
|
||||
"stripInternal": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": false,
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noEmitOnError": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"importsNotUsedAsValues": "error",
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"outDir": "dist",
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"strictPropertyInitialization": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"extends": "@sylo-digital/scripts/tsconfig/base.json",
|
||||
"include": ["src", "src/blocklist.json", "src/fastify.d.ts"]
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["es2021", "dom"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
// https://github.com/bevry/istextorbinary/issues/270
|
||||
"istextorbinary": ["node_modules/istextorbinary/compiled-types/index.d.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
module.exports = require("@sylo-digital/scripts/eslint/base");
|
|
@ -1,29 +0,0 @@
|
|||
# @ryanke/thumbnail-generator
|
||||
|
||||
Generates thumbnails for videos and images. Should be fairly fast but of course not nearly as fast as some other options in other languages and so you should probably not use it for anything on-demand. Or maybe you should, who knows.
|
||||
|
||||
> At the moment video thumbnail generation from a stream is not possible, a path must be provided.
|
||||
|
||||
## usage
|
||||
|
||||
```ts
|
||||
import {
|
||||
generateThumbnailToStream,
|
||||
generateVideoThumbnailToStream,
|
||||
checkThumbnailSupport,
|
||||
} from "@ryanke/thumbnail-generator";
|
||||
|
||||
// to handle images and videos in one, pass in the mime type with a path, stream or buffer
|
||||
// in the ideal world you should always prefer a path, but when thats not possible streams and buffers are fine.
|
||||
const stream = await generateThumbnailToStream("image/jpeg", inputStream);
|
||||
const stream = await generateThumbnailToStream("video/mp4", "/path/to/video.mp4", {
|
||||
size: { height: 200 },
|
||||
});
|
||||
|
||||
// if you already know what you want, use a generator directly
|
||||
const stream = await generateVideoThumbnailToStream(inputStream);
|
||||
|
||||
// passing in a mime type that is unsupported will throw an error, so make sure to check
|
||||
// for support before attempting to generate a thumbnail.
|
||||
checkThumbnailSupport("image/jpeg"); // true
|
||||
```
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"name": "@ryanke/thumbnail-generator",
|
||||
"version": "0.0.1",
|
||||
"repository": "https://github.com/sylv/micro",
|
||||
"author": "Ryan <ryan@sylver.me>",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --sourcemap --format esm,cjs --target node16 --dts --splitting",
|
||||
"test": "jest",
|
||||
"lint": "eslint src --fix --cache",
|
||||
"clean": "rm -rf ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sylo-digital/scripts": "^1.0.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"sharp": "^0.30.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fluent-ffmpeg": "^2.1.20",
|
||||
"@types/jest": "^28.1.4",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/node": "^16.11.21",
|
||||
"@types/sharp": "^0.30.2",
|
||||
"eslint": "^8.19.0",
|
||||
"jest": "^28.1.2",
|
||||
"tsup": "^6.1.3",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "@sylo-digital/scripts/jest/node"
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import mime from 'mime-types';
|
||||
import sharp from 'sharp';
|
||||
import { Readable } from 'stream';
|
||||
import type { ThumbnailOptions } from './types';
|
||||
|
||||
export const IMAGE_TYPES = new Set(
|
||||
Object.keys(sharp.format)
|
||||
.map((key) => mime.lookup(key))
|
||||
.filter((key) => key && key.startsWith('image'))
|
||||
);
|
||||
|
||||
export async function imageThumbnailGenerator(input: string | Buffer | Readable, options?: ThumbnailOptions) {
|
||||
const format = options?.type === 'image/jpeg' ? 'jpeg' : 'webp';
|
||||
if (input instanceof Readable) {
|
||||
const transformer = sharp().resize(options?.size?.width, options?.size?.height).toFormat(format);
|
||||
const transformed = input.pipe(transformer);
|
||||
return { stream: transformed };
|
||||
}
|
||||
|
||||
return {
|
||||
stream: sharp(input).resize(options?.size?.width, options?.size?.height).toFormat(format),
|
||||
};
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
import { createReadStream, createWriteStream } from 'fs';
|
||||
import { copyFile, rename } from 'fs/promises';
|
||||
import type { Readable } from 'stream';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { imageThumbnailGenerator, IMAGE_TYPES } from './image-generator';
|
||||
import { logger } from './logger';
|
||||
import type { ThumbnailOptions } from './types';
|
||||
import { videoThumbnailGenerator, VIDEO_TYPES } from './video-generator';
|
||||
|
||||
export * from './types';
|
||||
|
||||
/**
|
||||
* Check if the given type is supported.
|
||||
* @returns true if the type is supported, false otherwise.
|
||||
*/
|
||||
export function checkThumbnailSupport(type: string) {
|
||||
return IMAGE_TYPES.has(type) || VIDEO_TYPES.has(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a video thumbnail and save it to a file.
|
||||
* @param input The input video, assumed to be an image with no additional checks.
|
||||
*/
|
||||
export async function generateVideoThumbnailToStream(input: string, options?: ThumbnailOptions): Promise<Readable> {
|
||||
const output = await videoThumbnailGenerator(input, options);
|
||||
const stream = createReadStream(output.path);
|
||||
stream.on('end', () => {
|
||||
output.cleanup().catch((error) => {
|
||||
logger(`Failed cleaning up "${output.path}"`, error);
|
||||
});
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a video thumbnail and save it to a file
|
||||
* @param input The input video, assumed to be a ffmpeg-compatible video with no additional checks
|
||||
* @param dest The output path
|
||||
*/
|
||||
export async function generateVideoThumbnailToPath(
|
||||
input: string,
|
||||
dest: string,
|
||||
options?: ThumbnailOptions
|
||||
): Promise<void> {
|
||||
const output = await videoThumbnailGenerator(input, options);
|
||||
|
||||
try {
|
||||
logger(`Moving "${output.path}" to "${dest}"`);
|
||||
await rename(output.path, dest);
|
||||
} catch (error: any) {
|
||||
// cross-device errors
|
||||
if (error.code === 'EXDEV') {
|
||||
logger(`Cross-device error moving "${output.path}" to "${dest}", falling back to copy`);
|
||||
await copyFile(output.path, dest);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
output.cleanup().catch((error) => {
|
||||
logger(`Failed cleaning up "${output.path}"`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an image thumbnail and return it as a stream
|
||||
* @param input The input image, assumed to be an image with no additional checks.
|
||||
* @param dest The output path
|
||||
*/
|
||||
export async function generateImageThumbnailToStream(
|
||||
input: string | Buffer | Readable,
|
||||
options?: ThumbnailOptions
|
||||
): Promise<Readable> {
|
||||
const output = await imageThumbnailGenerator(input, options);
|
||||
return output.stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an image thumbnail and save it to a file.
|
||||
* @param input The input image, assumed to be an image with no additional checks.
|
||||
* @param dest The output path
|
||||
*/
|
||||
export async function generateImageThumbnailToPath(
|
||||
input: string | Buffer | Readable,
|
||||
dest: string,
|
||||
options?: ThumbnailOptions
|
||||
): Promise<void> {
|
||||
const output = await imageThumbnailGenerator(input, options);
|
||||
const stream = createWriteStream(dest);
|
||||
await pipeline(output.stream, stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a thumbnail and save it to a file.
|
||||
* @param inputType The type of the input, for example `image/jpeg` or `video/mp4`.
|
||||
* @param dest The output path
|
||||
* @throws if the given input type is not supported
|
||||
*/
|
||||
export async function generateThumbnailToPath(
|
||||
inputType: string,
|
||||
input: string,
|
||||
dest: string,
|
||||
options?: ThumbnailOptions
|
||||
): Promise<void> {
|
||||
if (IMAGE_TYPES.has(inputType)) {
|
||||
return generateImageThumbnailToPath(input, dest, options);
|
||||
} else if (VIDEO_TYPES.has(inputType)) {
|
||||
return generateVideoThumbnailToPath(input, dest, options);
|
||||
}
|
||||
throw new Error(`Unsupported input type "${inputType}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a thumbnail and return it as a stream.
|
||||
* @param inputType The type of the input, for example `image/jpeg` or `video/mp4`.
|
||||
* @throws if the given input type is not supported
|
||||
*/
|
||||
export async function generateThumbnailToStream(
|
||||
inputType: string,
|
||||
input: string | Buffer | Readable,
|
||||
options?: ThumbnailOptions
|
||||
): Promise<Readable> {
|
||||
if (IMAGE_TYPES.has(inputType)) {
|
||||
return generateImageThumbnailToStream(input, options);
|
||||
} else if (VIDEO_TYPES.has(inputType)) {
|
||||
if (typeof input !== 'string') {
|
||||
throw new TypeError('Video thumbnails can only be generated given a path.');
|
||||
}
|
||||
|
||||
return generateVideoThumbnailToStream(input, options);
|
||||
}
|
||||
throw new Error(`Unsupported input type "${inputType}"`);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import { debuglog } from 'util';
|
||||
|
||||
export const logger = debuglog('thumbnail-generator');
|
|
@ -1,13 +0,0 @@
|
|||
export interface ThumbnailOptions {
|
||||
/**
|
||||
* The size of the thumbnail to generate
|
||||
* Either value can be excluded to maintain aspect ratio.
|
||||
*/
|
||||
size?: { height?: number; width?: number };
|
||||
/**
|
||||
* Skip some improvements to improve performance.
|
||||
* The most important one is the video generator generating only one thumbnail instead of multiple and picking the one with the most detail.
|
||||
*/
|
||||
fast?: boolean;
|
||||
type?: 'image/webp' | 'image/jpeg';
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
import { once } from 'events';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { readdir, rm, stat } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { logger } from './logger';
|
||||
import type { ThumbnailOptions } from './types';
|
||||
|
||||
export const VIDEO_TYPES = new Set([
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/x-matroska',
|
||||
'video/x-ms-wmv',
|
||||
'video/x-m4v',
|
||||
'video/x-flv',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Generate a thumbnail for a video.
|
||||
* Extracts the "best" thumbnail by extracting multiple jpeg thumbnails and using the largest.
|
||||
*/
|
||||
export async function videoThumbnailGenerator(filePath: string, options?: ThumbnailOptions) {
|
||||
const tempId = randomUUID();
|
||||
const tempDir = join(tmpdir(), `.thumbnail-workspace-${tempId}`);
|
||||
logger(`Generating video thumbnail at "${tempDir}"`);
|
||||
|
||||
// i have no clue why but the internet told me that doing it in multiple invocations is faster
|
||||
// and it is so whatever. maybe there is a way to do this faster, but this is already pretty fast.
|
||||
const positions = options?.fast ? ['5%'] : ['5%', '10%', '20%', '40%'];
|
||||
const size = options?.size ? `${options.size.width ?? '?'}x${options.size.height ?? '?'}` : '200x?';
|
||||
const ext = options?.type === 'image/jpeg' ? 'jpg' : 'webp';
|
||||
for (let positionIndex = 0; positionIndex < positions.length; positionIndex++) {
|
||||
const percent = positions[positionIndex];
|
||||
const stream = ffmpeg(filePath).screenshot({
|
||||
count: 1,
|
||||
timemarks: [percent],
|
||||
folder: tempDir,
|
||||
size: size,
|
||||
fastSeek: true,
|
||||
filename: `%b-${positionIndex + 1}.${ext}`,
|
||||
});
|
||||
|
||||
await once(stream, 'end');
|
||||
}
|
||||
|
||||
const files = await readdir(tempDir);
|
||||
let largest: { size: number; path: string } | undefined;
|
||||
for (const file of files) {
|
||||
const path = join(tempDir, file);
|
||||
const stats = await stat(path);
|
||||
if (!largest || stats.size > largest.size) {
|
||||
largest = { size: stats.size, path };
|
||||
}
|
||||
}
|
||||
|
||||
if (!largest) {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
throw new Error('No thumbnails were generated');
|
||||
}
|
||||
|
||||
logger(`Largest thumbnail is at "${largest.path}", ${largest.size} bytes`);
|
||||
|
||||
return {
|
||||
path: largest.path,
|
||||
cleanup: async () => {
|
||||
logger(`Cleaning up "${tempDir}"`);
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"extends": "@sylo-digital/scripts/tsconfig/base.json",
|
||||
"include": ["src"]
|
||||
}
|
|
@ -15,24 +15,24 @@
|
|||
"generate": "graphql-codegen --config codegen.yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.9",
|
||||
"@headlessui/react": "^1.6.1",
|
||||
"@apollo/client": "^3.7.0",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@ryanke/pandora": "^0.0.9",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"classnames": "^2.3.1",
|
||||
"concurrently": "^7.2.2",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"dayjs": "^1.11.1",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"classnames": "^2.3.2",
|
||||
"concurrently": "^7.4.0",
|
||||
"copy-to-clipboard": "^3.3.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"deepmerge": "^4.2.2",
|
||||
"formik": "^2.2.9",
|
||||
"generate-avatar": "1.4.10",
|
||||
"graphql": "^16.5.0",
|
||||
"graphql": "^16.6.0",
|
||||
"http-status-codes": "^2.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"nanoid": "^3.3.4",
|
||||
"nanoid": "^4.0.0",
|
||||
"next": "12.2.0",
|
||||
"postcss": "^8.4.13",
|
||||
"postcss": "^8.4.17",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "18.2.0",
|
||||
|
@ -42,19 +42,19 @@
|
|||
"rehype-raw": "^6.1.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"swr": "^1.3.0",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^2.7.0",
|
||||
"@graphql-codegen/typescript": "2.6.0",
|
||||
"@graphql-codegen/typescript-operations": "2.4.3",
|
||||
"@graphql-codegen/typescript-react-apollo": "3.2.17",
|
||||
"@sylo-digital/scripts": "^1.0.2",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@graphql-codegen/cli": "^2.13.5",
|
||||
"@graphql-codegen/typescript": "2.7.3",
|
||||
"@graphql-codegen/typescript-operations": "2.5.3",
|
||||
"@graphql-codegen/typescript-react-apollo": "3.3.3",
|
||||
"@sylo-digital/scripts": "^1.0.12",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/node": "16",
|
||||
"@types/react": "^18.0.8",
|
||||
"@types/react": "^18.0.21",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Container, Spinner } from '@ryanke/pandora';
|
||||
import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Title } from '../components/title';
|
||||
import { Title } from './title';
|
||||
|
||||
export const PageLoader: FC<{ title?: string }> = ({ title }) => {
|
||||
useEffect(() => {
|
||||
|
|
|
@ -20,7 +20,7 @@ export const apiUri = isServer ? process.env.API_URL : `/api`;
|
|||
export async function http(pathOrUrl: string, options?: RequestInit): Promise<Response> {
|
||||
const hasProtocol = pathOrUrl.startsWith('http');
|
||||
const isAbsolute = pathOrUrl.startsWith('/');
|
||||
const url = hasProtocol || isAbsolute ? pathOrUrl : `${apiUri}${pathOrUrl}`;
|
||||
const url = hasProtocol || isAbsolute ? pathOrUrl : `${apiUri}${isAbsolute ? '' : '/'}${pathOrUrl}`;
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const clone = response.clone();
|
||||
|
|
8166
pnpm-lock.yaml
8166
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue