File

src/uploads/uploads.service.ts

Index

Properties
Methods

Constructor

constructor(databaseService: DatabaseService, configService: ConfigService, utilService: UtilsService, bezosService: BezosService, csamService: CsamService, usersService: UsersService, emailService: EmailsService, cacheService: CacheService)
Parameters :
Name Type Optional
databaseService DatabaseService No
configService ConfigService No
utilService UtilsService No
bezosService BezosService No
csamService CsamService No
usersService UsersService No
emailService EmailsService No
cacheService CacheService No

Methods

Private Async cacheUpload
cacheUpload(name: string, mime: string, date: string, size: string, embed: string, ext: string)
Parameters :
Name Type Optional
name string No
mime string No
date string No
size string No
embed string No
ext string No
Returns : any
Private Async deleteFiles
deleteFiles(files: string | string[])
Parameters :
Name Type Optional
files string | string[] No
Returns : any
Public Async fetchUpload
fetchUpload(snowflake: string, filter: UploadGetterDto)
Parameters :
Name Type Optional
snowflake string No
filter UploadGetterDto No
Returns : unknown
Public Async getAllUserUploads
getAllUserUploads(user: string)
Parameters :
Name Type Optional
user string No
Returns : unknown
Public Async getFromPath
getFromPath(p: string)
Parameters :
Name Type Optional
p string No
Returns : unknown
Public Async getUploadData
getUploadData(upload: string)
Parameters :
Name Type Optional
upload string No
Returns : unknown
Public Async getUploadOwner
getUploadOwner(upload: string)
Parameters :
Name Type Optional
upload string No
Returns : Promise<string | undefined>
Public Async getUserEmbed
getUserEmbed(user: string)
Parameters :
Name Type Optional
user string No
Returns : Promise<EmbedDto>
Private Async handleCsamUpload
handleCsamUpload(user: string, filename: string, file: Buffer, mime: string)
Parameters :
Name Type Optional
user string No
filename string No
file Buffer No
mime string No
Returns : any
Private Async handler
handler(file: Buffer, name: string, filename: string, encoding: string, mimetype: string, req: FastifyRequest)
Parameters :
Name Type Optional
file Buffer No
name string No
filename string No
encoding string No
mimetype string No
req FastifyRequest<literal type> No
Returns : unknown
Public Async isUploadFav
isUploadFav(upload: string)
Parameters :
Name Type Optional
upload string No
Returns : Promise<boolean>
Public Async removeFromCache
removeFromCache(name: string)
Parameters :
Name Type Optional
name string No
Returns : any
Public Async removeUserUpload
removeUserUpload(snowflake: string, upload: string)
Parameters :
Name Type Optional
snowflake string No
upload string No
Returns : unknown
Private Async sanitizeFileContent
sanitizeFileContent(file: Buffer, mimeType: string)

Slow cunt method

Parameters :
Name Type Optional
file Buffer No
mimeType string No
Returns : Promise<Buffer>
Private Async saveUserUpload
saveUserUpload(filename: string, req: FastifyRequest, mime: string, size: string, client: string, ext: string, quarantine)
Parameters :
Name Type Optional Default value
filename string No
req FastifyRequest No
mime string No
size string No
client string No
ext string No
quarantine No false
Returns : any
Public Async setUserEmbed
setUserEmbed(embed: EmbedSetterDto, user: string)
Parameters :
Name Type Optional
embed EmbedSetterDto No
user string No
Returns : any
Public Async toggleUploadFav
toggleUploadFav(owner: string, upload: string)
Parameters :
Name Type Optional
owner string No
upload string No
Returns : unknown
Private Async upload
upload(req: FastifyRequest, file: Buffer, name: string, ext: string, mimetype: string)
Parameters :
Name Type Optional
req FastifyRequest<literal type> No
file Buffer No
name string No
ext string No
mimetype string No
Returns : any
Public Async uploadFileFromApi
uploadFileFromApi(req: FastifyRequest, forceRaw)
Parameters :
Name Type Optional Default value
req FastifyRequest<literal type> No
forceRaw No false
Returns : unknown
Public Async wipeUserUploads
wipeUserUploads(user: string)
Parameters :
Name Type Optional
user string No
Returns : unknown

Properties

Private Readonly logger
Default value : new Logger(UploadsService.name)
Public Readonly usersService
Type : UsersService
Decorators :
@Inject(undefined)
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,
    };
  }
}

results matching ""

    No results matching ""