feat: remove appwrite and add nextjs backend

This commit is contained in:
Elias Schneider
2022-10-09 22:30:32 +02:00
parent 7728351158
commit 4bab33ad8a
153 changed files with 13400 additions and 2811 deletions

View File

@@ -0,0 +1,22 @@
import { Expose, plainToClass } from "class-transformer";
import { ShareDTO } from "src/share/dto/share.dto";
export class FileDTO {
@Expose()
id: string;
@Expose()
name: string;
@Expose()
size: string;
@Expose()
url: boolean;
share: ShareDTO;
constructor(partial: Partial<FileDTO>) {
return plainToClass(FileDTO, partial, { excludeExtraneousValues: true });
}
}

View File

@@ -0,0 +1,107 @@
import {
Controller,
Get,
Param,
ParseFilePipeBuilder,
Post,
Res,
StreamableFile,
UploadedFile,
UseGuards,
UseInterceptors,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { FileInterceptor } from "@nestjs/platform-express";
import { Response } from "express";
import { JwtGuard } from "src/auth/guard/jwt.guard";
import { FileDownloadGuard } from "src/file/guard/fileDownload.guard";
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
import { FileService } from "./file.service";
@Controller("shares/:shareId/files")
export class FileController {
constructor(
private fileService: FileService,
private config: ConfigService
) {}
@Post()
@UseGuards(JwtGuard)
@UseInterceptors(
FileInterceptor("file", {
dest: "./uploads/_temp/",
})
)
async create(
@UploadedFile(
new ParseFilePipeBuilder()
.addMaxSizeValidator({
maxSize: parseInt(process.env.MAX_FILE_SIZE),
})
.build()
)
file: Express.Multer.File,
@Param("shareId") shareId: string
) {
return await this.fileService.create(file, shareId);
}
@Get(":fileId/download")
@UseGuards(ShareSecurityGuard)
async getFileDownloadUrl(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string,
@Param("fileId") fileId: string
) {
const url = this.fileService.getFileDownloadUrl(shareId, fileId);
return { url };
}
@Get("zip/download")
@UseGuards(ShareSecurityGuard)
async getZipArchiveDownloadURL(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string,
@Param("fileId") fileId: string
) {
const url = this.fileService.getFileDownloadUrl(shareId, fileId);
res.set({
"Content-Type": "application/zip",
});
return { url };
}
@Get("zip")
@UseGuards(FileDownloadGuard)
async getZip(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string
) {
const zip = this.fileService.getZip(shareId);
res.set({
"Content-Type": "application/zip",
"Content-Disposition": `attachment ; filename="pingvin-share-${shareId}"`,
});
return new StreamableFile(zip);
}
@Get(":fileId")
@UseGuards(FileDownloadGuard)
async getFile(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string,
@Param("fileId") fileId: string
) {
const file = await this.fileService.get(shareId, fileId);
res.set({
"Content-Type": file.metaData.mimeType,
"Content-Length": file.metaData.size,
"Content-Disposition": `attachment ; filename="${file.metaData.name}"`,
});
return new StreamableFile(file.file);
}
}

View File

@@ -0,0 +1,14 @@
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { ShareModule } from "src/share/share.module";
import { ShareService } from "src/share/share.service";
import { FileController } from "./file.controller";
import { FileService } from "./file.service";
@Module({
imports: [JwtModule.register({}), ShareModule],
controllers: [FileController],
providers: [FileService],
exports: [FileService],
})
export class FileModule {}

View File

@@ -0,0 +1,112 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { randomUUID } from "crypto";
import * as fs from "fs";
import * as mime from "mime-types";
import { join } from "path";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class FileService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
private config: ConfigService
) {}
async create(file: Express.Multer.File, shareId: string) {
const share = await this.prisma.share.findUnique({
where: { id: shareId },
});
if (share.uploadLocked)
throw new BadRequestException("Share is already completed");
const fileId = randomUUID();
await fs.promises.mkdir(`./uploads/shares/${shareId}`, { recursive: true });
fs.promises.rename(
`./uploads/_temp/${file.filename}`,
`./uploads/shares/${shareId}/${fileId}`
);
return await this.prisma.file.create({
data: {
id: fileId,
name: file.originalname,
size: file.size.toString(),
share: { connect: { id: shareId } },
},
});
}
async get(shareId: string, fileId: string) {
const fileMetaData = await this.prisma.file.findUnique({
where: { id: fileId },
});
if (!fileMetaData) throw new NotFoundException("File not found");
const file = fs.createReadStream(
join(process.cwd(), `uploads/shares/${shareId}/${fileId}`)
);
return {
metaData: {
mimeType: mime.contentType(fileMetaData.name.split(".").pop()),
...fileMetaData,
size: fileMetaData.size,
},
file,
};
}
async deleteAllFiles(shareId: string) {
await fs.promises.rm(`./uploads/shares/${shareId}`, {
recursive: true,
force: true,
});
}
getZip(shareId: string) {
return fs.createReadStream(`./uploads/shares/${shareId}/archive.zip`);
}
getFileDownloadUrl(shareId: string, fileId: string) {
const downloadToken = this.generateFileDownloadToken(shareId, fileId);
return `${this.config.get(
"APP_URL"
)}/api/shares/${shareId}/files/${fileId}?token=${downloadToken}`;
}
generateFileDownloadToken(shareId: string, fileId: string) {
if (fileId == "zip") fileId = undefined;
return this.jwtService.sign(
{
shareId,
fileId,
},
{
expiresIn: "10min",
secret: this.config.get("JWT_SECRET"),
}
);
}
verifyFileDownloadToken(shareId: string, fileId: string, token: string) {
try {
const claims = this.jwtService.verify(token, {
secret: this.config.get("JWT_SECRET"),
});
return claims.shareId == shareId && claims.fileId == fileId;
} catch {
return false;
}
}
}

View File

@@ -0,0 +1,23 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Request } from "express";
import { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class FileDownloadGuard implements CanActivate {
constructor(
private reflector: Reflector,
private fileService: FileService,
private prisma: PrismaService
) {}
async canActivate(context: ExecutionContext) {
const request: Request = context.switchToHttp().getRequest();
const token = request.query.token as string;
const { shareId, fileId } = request.params;
return this.fileService.verifyFileDownloadToken(shareId, fileId, token);
}
}