feat: remove appwrite and add nextjs backend
This commit is contained in:
22
backend/src/file/dto/file.dto.ts
Normal file
22
backend/src/file/dto/file.dto.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
107
backend/src/file/file.controller.ts
Normal file
107
backend/src/file/file.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
14
backend/src/file/file.module.ts
Normal file
14
backend/src/file/file.module.ts
Normal 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 {}
|
||||
112
backend/src/file/file.service.ts
Normal file
112
backend/src/file/file.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
backend/src/file/guard/fileDownload.guard.ts
Normal file
23
backend/src/file/guard/fileDownload.guard.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user