mirror of
https://github.com/swissmakers/swiss-datashare.git
synced 2026-04-05 07:47:01 +02:00
163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
HttpException,
|
|
HttpStatus,
|
|
Injectable,
|
|
InternalServerErrorException,
|
|
NotFoundException,
|
|
} from "@nestjs/common";
|
|
import { JwtService } from "@nestjs/jwt";
|
|
import * as crypto from "crypto";
|
|
import * as fs from "fs";
|
|
import * as mime from "mime-types";
|
|
import { ConfigService } from "src/config/config.service";
|
|
import { PrismaService } from "src/prisma/prisma.service";
|
|
import { validate as isValidUUID } from "uuid";
|
|
import { SHARE_DIRECTORY } from "../constants";
|
|
|
|
@Injectable()
|
|
export class FileService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private jwtService: JwtService,
|
|
private config: ConfigService,
|
|
) {}
|
|
|
|
async create(
|
|
data: string,
|
|
chunk: { index: number; total: number },
|
|
file: { id?: string; name: string },
|
|
shareId: string,
|
|
) {
|
|
if (!file.id) {
|
|
file.id = crypto.randomUUID();
|
|
} else if (!isValidUUID(file.id)) {
|
|
throw new BadRequestException("Invalid file ID format");
|
|
}
|
|
|
|
const share = await this.prisma.share.findUnique({
|
|
where: { id: shareId },
|
|
include: { files: true, reverseShare: true },
|
|
});
|
|
|
|
if (share.uploadLocked)
|
|
throw new BadRequestException("Share is already completed");
|
|
|
|
let diskFileSize: number;
|
|
try {
|
|
diskFileSize = fs.statSync(
|
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
|
|
).size;
|
|
} catch {
|
|
diskFileSize = 0;
|
|
}
|
|
|
|
// If the sent chunk index and the expected chunk index doesn't match throw an error
|
|
const chunkSize = this.config.get("share.chunkSize");
|
|
const expectedChunkIndex = Math.ceil(diskFileSize / chunkSize);
|
|
|
|
if (expectedChunkIndex != chunk.index)
|
|
throw new BadRequestException({
|
|
message: "Unexpected chunk received",
|
|
error: "unexpected_chunk_index",
|
|
expectedChunkIndex,
|
|
});
|
|
|
|
const buffer = Buffer.from(data, "base64");
|
|
|
|
// Check if there is enough space on the server
|
|
const space = await fs.promises.statfs(SHARE_DIRECTORY);
|
|
const availableSpace = space.bavail * space.bsize;
|
|
if (availableSpace < buffer.byteLength) {
|
|
throw new InternalServerErrorException("Not enough space on the server");
|
|
}
|
|
|
|
// Check if share size limit is exceeded
|
|
const fileSizeSum = share.files.reduce(
|
|
(n, { size }) => n + parseInt(size),
|
|
0,
|
|
);
|
|
|
|
const shareSizeSum = fileSizeSum + diskFileSize + buffer.byteLength;
|
|
|
|
if (
|
|
shareSizeSum > this.config.get("share.maxSize") ||
|
|
(share.reverseShare?.maxShareSize &&
|
|
shareSizeSum > parseInt(share.reverseShare.maxShareSize))
|
|
) {
|
|
throw new HttpException(
|
|
"Max share size exceeded",
|
|
HttpStatus.PAYLOAD_TOO_LARGE,
|
|
);
|
|
}
|
|
|
|
fs.appendFileSync(
|
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
|
|
buffer,
|
|
);
|
|
|
|
const isLastChunk = chunk.index == chunk.total - 1;
|
|
if (isLastChunk) {
|
|
fs.renameSync(
|
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
|
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}`,
|
|
);
|
|
const fileSize = fs.statSync(
|
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}`,
|
|
).size;
|
|
await this.prisma.file.create({
|
|
data: {
|
|
id: file.id,
|
|
name: file.name,
|
|
size: fileSize.toString(),
|
|
share: { connect: { id: shareId } },
|
|
},
|
|
});
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
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(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
|
|
|
|
return {
|
|
metaData: {
|
|
mimeType: mime.contentType(fileMetaData.name.split(".").pop()),
|
|
...fileMetaData,
|
|
size: fileMetaData.size,
|
|
},
|
|
file,
|
|
};
|
|
}
|
|
|
|
async remove(shareId: string, fileId: string) {
|
|
const fileMetaData = await this.prisma.file.findUnique({
|
|
where: { id: fileId },
|
|
});
|
|
|
|
if (!fileMetaData) throw new NotFoundException("File not found");
|
|
|
|
fs.unlinkSync(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
|
|
|
|
await this.prisma.file.delete({ where: { id: fileId } });
|
|
}
|
|
|
|
async deleteAllFiles(shareId: string) {
|
|
await fs.promises.rm(`${SHARE_DIRECTORY}/${shareId}`, {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
}
|
|
|
|
getZip(shareId: string) {
|
|
return fs.createReadStream(`${SHARE_DIRECTORY}/${shareId}/archive.zip`);
|
|
}
|
|
}
|