src/uploads/uploads.service.ts
Properties |
|
Methods |
|
constructor(databaseService: DatabaseService, configService: ConfigService, utilService: UtilsService, bezosService: BezosService, csamService: CsamService, usersService: UsersService, emailService: EmailsService, cacheService: CacheService)
|
|||||||||||||||||||||||||||
|
Defined in src/uploads/uploads.service.ts:27
|
|||||||||||||||||||||||||||
|
Parameters :
|
| Private Async cacheUpload | |||||||||||||||||||||
cacheUpload(name: string, mime: string, date: string, size: string, embed: string, ext: string)
|
|||||||||||||||||||||
|
Defined in src/uploads/uploads.service.ts:262
|
|||||||||||||||||||||
|
Parameters :
Returns :
any
|
| Private Async deleteFiles | ||||||
deleteFiles(files: string | string[])
|
||||||
|
Defined in src/uploads/uploads.service.ts:184
|
||||||
|
Parameters :
Returns :
any
|
| Public Async fetchUpload | |||||||||
fetchUpload(snowflake: string, filter: UploadGetterDto)
|
|||||||||
|
Defined in src/uploads/uploads.service.ts:309
|
|||||||||
|
Parameters :
Returns :
unknown
|
| Public Async getAllUserUploads | ||||||
getAllUserUploads(user: string)
|
||||||
|
Defined in src/uploads/uploads.service.ts:410
|
||||||
|
Parameters :
Returns :
unknown
|
| Public Async getFromPath | ||||||
getFromPath(p: string)
|
||||||
|
Defined in src/uploads/uploads.service.ts:444
|
||||||
|
Parameters :
Returns :
unknown
|
| Public Async getUploadData | ||||||
getUploadData(upload: string)
|
||||||
|
Defined in src/uploads/uploads.service.ts:355
|
||||||
|
Parameters :
Returns :
unknown
|
| Public Async getUploadOwner | ||||||
getUploadOwner(upload: string)
|
||||||
|
Defined in src/uploads/uploads.service.ts:348
|
||||||
|
Parameters :
Returns :
Promise<string | undefined>
|
| Public Async getUserEmbed | ||||||
getUserEmbed(user: string)
|
||||||
|
Defined in src/uploads/uploads.service.ts:297
|
||||||
|
Parameters :
Returns :
Promise<EmbedDto>
|
| Private Async handleCsamUpload |
handleCsamUpload(user: string, filename: string, file: Buffer, mime: string)
|
|
Defined in src/uploads/uploads.service.ts:198
|
|
Returns :
any
|
| Private Async handler | |||||||||||||||||||||
handler(file: Buffer, name: string, filename: string, encoding: string, mimetype: string, req: FastifyRequest
|
|||||||||||||||||||||
|
Defined in src/uploads/uploads.service.ts:78
|
|||||||||||||||||||||
|
Parameters :
Returns :
unknown
|
| Public Async isUploadFav | ||||||
isUploadFav(upload: string)
|
||||||
|
Defined in src/uploads/uploads.service.ts:383
|
||||||
|
Parameters :
Returns :
Promise<boolean>
|
| Public Async removeFromCache | ||||||
removeFromCache(name: string)
|
||||||
|
Defined in src/uploads/uploads.service.ts:284
|
||||||
|
Parameters :
Returns :
any
|
| Public Async removeUserUpload |
removeUserUpload(snowflake: string, upload: string)
|
|
Defined in src/uploads/uploads.service.ts:361
|
|
Returns :
unknown
|
| Private Async sanitizeFileContent | |||||||||
sanitizeFileContent(file: Buffer, mimeType: string)
|
|||||||||
|
Defined in src/uploads/uploads.service.ts:162
|
|||||||||
|
Slow cunt method
Parameters :
Returns :
Promise<Buffer>
|
| Private Async saveUserUpload | ||||||||||||||||||||||||||||||||
saveUserUpload(filename: string, req: FastifyRequest, mime: string, size: string, client: string, ext: string, quarantine)
|
||||||||||||||||||||||||||||||||
|
Defined in src/uploads/uploads.service.ts:226
|
||||||||||||||||||||||||||||||||
|
Parameters :
Returns :
any
|
| Public Async setUserEmbed | |||||||||
setUserEmbed(embed: EmbedSetterDto, user: string)
|
|||||||||
|
Defined in src/uploads/uploads.service.ts:288
|
|||||||||
|
Parameters :
Returns :
any
|
| Public Async toggleUploadFav |
toggleUploadFav(owner: string, upload: string)
|
|
Defined in src/uploads/uploads.service.ts:393
|
|
Returns :
unknown
|
| Private Async upload | ||||||||||||||||||
upload(req: FastifyRequest
|
||||||||||||||||||
|
Defined in src/uploads/uploads.service.ts:97
|
||||||||||||||||||
|
Parameters :
Returns :
any
|
| Public Async uploadFileFromApi | ||||||||||||
uploadFileFromApi(req: FastifyRequest
|
||||||||||||
|
Defined in src/uploads/uploads.service.ts:42
|
||||||||||||
|
Parameters :
Returns :
unknown
|
| Public Async wipeUserUploads | ||||||
wipeUserUploads(user: string)
|
||||||
|
Defined in src/uploads/uploads.service.ts:422
|
||||||
|
Parameters :
Returns :
unknown
|
| Private Readonly logger |
Default value : new Logger(UploadsService.name)
|
|
Defined in src/uploads/uploads.service.ts:27
|
| Public Readonly usersService |
Type : UsersService
|
Decorators :
@Inject(undefined)
|
|
Defined in src/uploads/uploads.service.ts:35
|
import {
BadRequestException,
forwardRef,
Inject,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { FastifyRequest } from 'fastify-multipart';
import { DatabaseService } from '../database/database.service';
import { ConfigService } from '@nestjs/config';
import { UtilsService } from '../utils/utils.service';
import * as xss from 'xss';
import sanitizeSVG from '@mattkrick/sanitize-svg';
import { BezosService } from '../cloud/bezos.service';
import { CsamService } from '../cloud/csam/csam.service';
import { UsersService } from '../users/users.service';
import { EmailsService } from '../emails/emails.service';
import { CacheService } from '../cache/cache.service';
import { EdgeUploadCacheDto, EmbedDto } from '../../types';
import EmbedSetterDto from './dto/embed-setter.dto';
import { UploadGetterDto } from './dto/upload-getter.dto';
import { UploadEnum } from '../utils/enums/responses/upload.enum';
@Injectable()
export class UploadsService {
private readonly logger = new Logger(UploadsService.name);
constructor(
private readonly databaseService: DatabaseService,
private readonly configService: ConfigService,
private readonly utilService: UtilsService,
private readonly bezosService: BezosService,
private readonly csamService: CsamService,
@Inject(forwardRef(() => UsersService))
public readonly usersService: UsersService,
private readonly emailService: EmailsService,
private readonly cacheService: CacheService,
) {
this.logger.log('Constructor');
}
public async uploadFileFromApi(
req: FastifyRequest<{ Headers: { client: string } }>,
forceRaw = false,
) {
const start = Date.now();
if (!req.isMultipart()) {
throw new BadRequestException('Request is not multipart');
}
const name = await this.utilService.generateSnowflake(10);
const fStart = Date.now();
const f = await req.file();
this.logger.debug(`File parsed in ${Date.now() - fStart} ms`);
const hStart = Date.now();
const ext = await this.handler(
await f.toBuffer(),
name,
f.filename,
f.encoding,
f.mimetype,
req,
);
this.logger.debug(
`Call to underlying handler took ${Date.now() - hStart} ms`,
);
this.logger.debug(`Resolving entry promise after ${Date.now() - start} ms`);
if (forceRaw) {
return `https://i.file.glass/${name}.${ext}`;
}
if (req.uploadData.raw) {
return `https://${req.uploadData.cname}.${req.uploadData.domain}/${name}.${ext}`;
} else {
return `https://${req.uploadData.cname}.${req.uploadData.domain}/${name}`;
}
}
private async handler(
file: Buffer,
name: string,
filename: string,
encoding: string,
mimetype: string,
req: FastifyRequest<{ Headers: { client: string } }>,
) {
this.logger.debug('Handler called');
const start = Date.now();
const ext = this.utilService.getImageExtension(filename);
(async () => this.upload(req, file, name, ext, mimetype))();
this.logger.debug(
`Internal promise resolved in ${Date.now() - start} ms with extension:`,
ext,
);
return ext;
}
private async upload(
req: FastifyRequest<{ Headers: { client: string } }>,
file: Buffer,
name: string,
ext: string,
mimetype: string,
) {
this.logger.debug('Starting uploading sequence...');
this.logger.debug('Step 1 completed (conversion)');
const size = file.toString('ascii').length.toString();
try {
file = this.utilService.stripExif(file);
} catch (err) {
this.logger.debug('Not a JPEG');
}
const sanitizedFile = await this.sanitizeFileContent(file, mimetype);
await Promise.race([
this.bezosService.upload(sanitizedFile, `${name}.${ext}`, mimetype),
this.cacheUpload(
name,
mimetype,
Date.now().toString(),
size,
req.uploadData.embed as string,
ext,
),
]);
this.logger.debug('Step 5 completed (cached)');
const r = await this.csamService.isSafe(
file as Buffer,
`${name}.${ext}`,
mimetype,
);
this.logger.debug('Step 6 completed (CSAM call)');
if (!r) {
this.logger.warn(`CSAM upload found`);
await this.handleCsamUpload(req.user, `${name}.${ext}`, file, mimetype);
await this.saveUserUpload(
name,
req,
mimetype,
file.toString('ascii').length.toString(),
req.headers.client,
ext,
true,
);
await this.removeFromCache(name);
} else {
this.logger.log('File CSAM safe');
await this.saveUserUpload(
name,
req,
mimetype,
size,
req.headers.client,
ext,
false,
);
}
this.logger.debug('Uploading sequence finished');
}
/**
* Slow cunt method
*/
private async sanitizeFileContent(
file: Buffer,
mimeType: string,
): Promise<Buffer> {
try {
const blocked = this.configService.get<string[]>('susExtensions');
const type = this.utilService.splitMime(mimeType, 'extension');
if (blocked.includes(type)) {
this.logger.log('Sanitizing file');
if (type === 'svg') {
return (await sanitizeSVG(file)) as Buffer;
} else if (['html', 'xhtml'].includes(type)) {
return Buffer.from(xss.filterXSS(file.toString()));
}
} else {
return file;
}
} catch (err) {
throw err;
}
}
private async deleteFiles(files: string | string[]) {
const toDelete = typeof files === 'string' ? [files] : files;
await this.databaseService.write.uploads.deleteMany({
where: {
OR: toDelete
.map((file) => this.utilService.getImageName(file))
.map((file) => {
return { path: file };
}),
},
});
await this.bezosService.removeUploads(files);
}
private async handleCsamUpload(
user: string,
filename: string,
file: Buffer,
mime: string,
) {
await this.bezosService.removeUploads(filename);
await this.bezosService.uploadToCdn(
file,
filename,
mime,
'quarantine',
'private',
);
const devEmails = await this.usersService.getDevEmails();
const userEmail = await this.usersService.getUserEmail(user);
await new this.emailService.Mailer(userEmail, 'Upload Removed', true)
.append(
this.emailService.bodies.uploadDelete(
filename,
'Automatic CSAM filter removed the file.',
await this.usersService.getUsername(user),
),
)
.deliver();
//TODO! email devs with codes
}
private async saveUserUpload(
filename: string,
req: FastifyRequest,
mime: string,
size: string,
client: string,
ext: string,
quarantine = false,
) {
this.logger.debug(`Saving file: ${filename} with extension: ${ext}`);
await this.databaseService.write.uploads.create({
data: {
owner: req.user,
snowflake: await this.utilService.generateSnowflake(),
mime,
public: false,
views: 0,
size,
path: filename,
favorite: false,
likes: 0,
client,
date: Date.now().toString(),
saves: 0,
quarantined: quarantine,
extension: ext,
},
});
await this.databaseService.write.users.update({
where: { snowflake: req.user },
data: {
uploads: { increment: 1 },
},
});
}
private async cacheUpload(
name: string,
mime: string,
date: string,
size: string,
embed: string,
ext: string,
) {
const url = `${name}.${ext} `;
this.logger.debug('Embed', embed);
await this.cacheService.add<EdgeUploadCacheDto>(name, {
url,
isFav: false,
mime,
date,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
album: false,
embed: JSON.parse(embed as string),
size,
});
this.logger.log('Upload cached on edge');
}
public async removeFromCache(name: string) {
await this.cacheService.remove(name);
}
public async setUserEmbed(embed: EmbedSetterDto, user: string) {
const parsed = JSON.stringify(embed);
this.logger.debug('Parsed embed string', embed);
await this.databaseService.write.users.update({
where: { snowflake: user },
data: { embed: parsed },
});
}
public async getUserEmbed(user: string): Promise<EmbedDto> {
const r = await this.databaseService.read.users.findFirst({
where: {
snowflake: user,
},
select: {
embed: true,
},
});
return JSON.parse(r.embed);
}
public async fetchUpload(snowflake: string, filter: UploadGetterDto) {
const res = await this.databaseService.read.uploads.findMany({
select: {
mime: true,
path: true,
size: true,
date: true,
favorite: true,
album: true,
extension: true,
snowflake: true,
},
where: {
owner: snowflake,
...this.utilService.computeQueryObj(
filter.pathQuery,
filter.extQuery,
filter.favs,
),
},
skip: filter.skip,
take: filter.take,
orderBy: {
date: 'desc',
},
});
return res.map((img) => {
return {
url: `${img.path!}.${img.extension}`,
date: img.date!,
size: img.size!,
isFav: img.favorite!,
album: img.album || false,
extension: img.extension,
id: img.snowflake,
mime: img.mime,
};
});
}
public async getUploadOwner(upload: string): Promise<string | undefined> {
const r = await this.databaseService.read.uploads.findFirst({
where: { snowflake: upload },
select: { owner: true },
});
return r?.owner;
}
public async getUploadData(upload: string) {
return await this.databaseService.read.uploads.findFirst({
where: { snowflake: upload },
});
}
public async removeUserUpload(snowflake: string, upload: string) {
this.logger.debug(`Removing upload with snowflake:`, upload);
const u = await this.getUploadData(upload);
if (u.owner !== snowflake) {
throw new UnauthorizedException();
}
const p2 = this.databaseService.write.users.update({
where: { snowflake },
data: {
uploads: { decrement: 1 },
},
});
const p3 = this.deleteFiles(`${u.path}.${u.extension}`);
await Promise.all([p2, p3]);
return this.utilService.formatStr(
this.utilService.Enums.Responses.UploadEnum.DELETED,
`${u.path}.${u.extension}`,
);
}
public async isUploadFav(upload: string): Promise<boolean> {
const r = await this.databaseService.read.uploads.count({
where: {
snowflake: upload,
favorite: true,
},
});
return r > 0;
}
public async toggleUploadFav(owner: string, upload: string) {
const u = await this.getUploadData(upload);
if (u.owner !== owner) {
throw new UnauthorizedException();
}
const isFav = await this.isUploadFav(upload);
await this.databaseService.write.uploads.update({
where: {
snowflake: upload,
},
data: {
favorite: !isFav,
},
});
return isFav ? UploadEnum.MADE_UNFAV : UploadEnum.MADE_FAV;
}
public async getAllUserUploads(user: string) {
return await this.databaseService.read.uploads.findMany({
where: {
owner: user,
},
select: {
path: true,
extension: true,
},
});
}
public async wipeUserUploads(user: string) {
const uploads = await this.getAllUserUploads(user);
if (!uploads.length) {
this.logger.debug('The user has no uploads, skipping');
return null;
}
if (uploads.length > 999) {
const parts = this.utilService.partitionArray(uploads, 999);
this.logger.debug(`Uploads partitioned, size: ${parts.length}`);
for (const part of parts) {
await this.bezosService.removeUploads(
part.map((u) => `${u.path}.${u.extension}`),
);
}
} else {
await this.bezosService.removeUploads(
uploads.map((u) => `${u.path}.${u.extension}`),
);
}
}
public async getFromPath(p: string) {
const data = await this.databaseService.read.uploads.findFirst({
where: {
path: p,
},
select: {
snowflake: true,
owner: true,
},
});
return {
snowflake: data?.snowflake || '',
owner: data?.owner || '',
exists: !!data,
};
}
}