Files
Palmr/apps/server/src/modules/reverse-share/service.ts
Daniel Luiz Alves 95939f8f47 refactor: rename temp-chunks to temp-uploads and update related configurations
- Changed references from 'temp-chunks' to 'temp-uploads' across .dockerignore, Dockerfile, and various configuration files for consistency.
- Introduced a new directories configuration file to manage directory paths more effectively.
- Updated file handling in the server code to utilize streaming for uploads and downloads, improving performance and memory management.
- Enhanced cleanup processes for temporary directories to maintain a tidy file structure.
2025-07-06 00:06:09 -03:00

663 lines
22 KiB
TypeScript

import { PrismaClient } from "@prisma/client";
import { FileService } from "../file/service";
import {
CreateReverseShareInput,
ReverseShareResponseSchema,
UpdateReverseShareInput,
UploadToReverseShareInput,
} from "./dto";
import { ReverseShareRepository } from "./repository";
interface ReverseShareData {
id: string;
name: string | null;
description: string | null;
expiration: Date | null;
maxFiles: number | null;
maxFileSize: bigint | null;
allowedFileTypes: string | null;
password: string | null;
pageLayout: string;
isActive: boolean;
nameFieldRequired: string;
emailFieldRequired: string;
createdAt: Date;
updatedAt: Date;
creatorId: string;
files: any[];
alias?: {
id: string;
alias: string;
reverseShareId: string;
createdAt: Date;
updatedAt: Date;
} | null;
}
const prisma = new PrismaClient();
export class ReverseShareService {
private reverseShareRepository = new ReverseShareRepository();
private fileService = new FileService();
async createReverseShare(data: CreateReverseShareInput, creatorId: string) {
const reverseShare = await this.reverseShareRepository.create(data, creatorId);
return ReverseShareResponseSchema.parse(this.formatReverseShareResponse(reverseShare));
}
async listUserReverseShares(creatorId: string) {
const reverseShares = await this.reverseShareRepository.findByCreatorId(creatorId);
const formatted = reverseShares.map((reverseShare: ReverseShareData) =>
ReverseShareResponseSchema.parse(this.formatReverseShareResponse(reverseShare))
);
return formatted;
}
async getReverseShareById(id: string, creatorId?: string) {
const reverseShare = await this.reverseShareRepository.findById(id);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (creatorId && reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to access this reverse share");
}
return ReverseShareResponseSchema.parse(this.formatReverseShareResponse(reverseShare));
}
async getReverseShareForUpload(id: string, password?: string) {
const reverseShare = await this.reverseShareRepository.findById(id);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (!reverseShare.isActive) {
throw new Error("Reverse share is inactive");
}
if (reverseShare.expiration && new Date(reverseShare.expiration) < new Date()) {
throw new Error("Reverse share has expired");
}
if (reverseShare.password) {
if (!password) {
throw new Error("Password required");
}
const isValidPassword = await this.reverseShareRepository.comparePassword(password, reverseShare.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
}
const currentFileCount = await this.reverseShareRepository.countFilesByReverseShareId(id);
return {
id: reverseShare.id,
name: reverseShare.name,
description: reverseShare.description,
maxFiles: reverseShare.maxFiles,
maxFileSize: reverseShare.maxFileSize ? Number(reverseShare.maxFileSize) : null,
allowedFileTypes: reverseShare.allowedFileTypes,
pageLayout: reverseShare.pageLayout,
hasPassword: !!reverseShare.password,
currentFileCount,
nameFieldRequired: reverseShare.nameFieldRequired,
emailFieldRequired: reverseShare.emailFieldRequired,
};
}
async getReverseShareForUploadByAlias(alias: string, password?: string) {
const reverseShare = await this.reverseShareRepository.findByAlias(alias);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (!reverseShare.isActive) {
throw new Error("Reverse share is inactive");
}
if (reverseShare.expiration && new Date(reverseShare.expiration) < new Date()) {
throw new Error("Reverse share has expired");
}
if (reverseShare.password) {
if (!password) {
throw new Error("Password required");
}
const isValidPassword = await this.reverseShareRepository.comparePassword(password, reverseShare.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
}
const currentFileCount = await this.reverseShareRepository.countFilesByReverseShareId(reverseShare.id);
return {
id: reverseShare.id,
name: reverseShare.name,
description: reverseShare.description,
maxFiles: reverseShare.maxFiles,
maxFileSize: reverseShare.maxFileSize ? Number(reverseShare.maxFileSize) : null,
allowedFileTypes: reverseShare.allowedFileTypes,
pageLayout: reverseShare.pageLayout,
hasPassword: !!reverseShare.password,
currentFileCount,
nameFieldRequired: reverseShare.nameFieldRequired,
emailFieldRequired: reverseShare.emailFieldRequired,
};
}
async updateReverseShare(id: string, data: Partial<UpdateReverseShareInput>, creatorId: string) {
const reverseShare = await this.reverseShareRepository.findById(id);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to update this reverse share");
}
const updatedReverseShare = await this.reverseShareRepository.update(id, data);
return ReverseShareResponseSchema.parse(this.formatReverseShareResponse(updatedReverseShare));
}
async deleteReverseShare(id: string, creatorId: string) {
const reverseShare = await this.reverseShareRepository.findById(id);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to delete this reverse share");
}
for (const file of reverseShare.files) {
try {
await this.fileService.deleteObject(file.objectName);
} catch (error) {
console.error(`Failed to delete file ${file.objectName}:`, error);
}
}
const deletedReverseShare = await this.reverseShareRepository.delete(id);
return ReverseShareResponseSchema.parse(this.formatReverseShareResponse(deletedReverseShare));
}
async getPresignedUrl(id: string, objectName: string, password?: string) {
const reverseShare = await this.reverseShareRepository.findById(id);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (!reverseShare.isActive) {
throw new Error("Reverse share is inactive");
}
if (reverseShare.expiration && new Date(reverseShare.expiration) < new Date()) {
throw new Error("Reverse share has expired");
}
if (reverseShare.password) {
if (!password) {
throw new Error("Password required");
}
const isValidPassword = await this.reverseShareRepository.comparePassword(password, reverseShare.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
}
const expires = 3600; // 1 hour
const url = await this.fileService.getPresignedPutUrl(objectName, expires);
return { url, expiresIn: expires };
}
async getPresignedUrlByAlias(alias: string, objectName: string, password?: string) {
const reverseShare = await this.reverseShareRepository.findByAlias(alias);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (!reverseShare.isActive) {
throw new Error("Reverse share is inactive");
}
if (reverseShare.expiration && new Date(reverseShare.expiration) < new Date()) {
throw new Error("Reverse share has expired");
}
if (reverseShare.password) {
if (!password) {
throw new Error("Password required");
}
const isValidPassword = await this.reverseShareRepository.comparePassword(password, reverseShare.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
}
const expires = 3600; // 1 hour
const url = await this.fileService.getPresignedPutUrl(objectName, expires);
return { url, expiresIn: expires };
}
async registerFileUpload(reverseShareId: string, fileData: UploadToReverseShareInput, password?: string) {
const reverseShare = await this.reverseShareRepository.findById(reverseShareId);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (!reverseShare.isActive) {
throw new Error("Reverse share is inactive");
}
if (reverseShare.expiration && new Date(reverseShare.expiration) < new Date()) {
throw new Error("Reverse share has expired");
}
if (reverseShare.password) {
if (!password) {
throw new Error("Password required");
}
const isValidPassword = await this.reverseShareRepository.comparePassword(password, reverseShare.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
}
if (reverseShare.maxFiles) {
const currentFileCount = await this.reverseShareRepository.countFilesByReverseShareId(reverseShareId);
if (currentFileCount >= reverseShare.maxFiles) {
throw new Error("Maximum number of files reached");
}
}
if (reverseShare.maxFileSize && BigInt(fileData.size) > reverseShare.maxFileSize) {
throw new Error("File size exceeds limit");
}
if (reverseShare.allowedFileTypes) {
const allowedTypes = reverseShare.allowedFileTypes.split(",").map((type) => type.trim().toLowerCase());
if (!allowedTypes.includes(fileData.extension.toLowerCase())) {
throw new Error("File type not allowed");
}
}
const file = await this.reverseShareRepository.createFile(reverseShareId, {
...fileData,
size: BigInt(fileData.size),
});
return this.formatFileResponse(file);
}
async registerFileUploadByAlias(alias: string, fileData: UploadToReverseShareInput, password?: string) {
const reverseShare = await this.reverseShareRepository.findByAlias(alias);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (!reverseShare.isActive) {
throw new Error("Reverse share is inactive");
}
if (reverseShare.expiration && new Date(reverseShare.expiration) < new Date()) {
throw new Error("Reverse share has expired");
}
if (reverseShare.password) {
if (!password) {
throw new Error("Password required");
}
const isValidPassword = await this.reverseShareRepository.comparePassword(password, reverseShare.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
}
if (reverseShare.maxFiles) {
const currentFileCount = await this.reverseShareRepository.countFilesByReverseShareId(reverseShare.id);
if (currentFileCount >= reverseShare.maxFiles) {
throw new Error("Maximum number of files reached");
}
}
if (reverseShare.maxFileSize && BigInt(fileData.size) > reverseShare.maxFileSize) {
throw new Error("File size exceeds limit");
}
if (reverseShare.allowedFileTypes) {
const allowedTypes = reverseShare.allowedFileTypes.split(",").map((type) => type.trim().toLowerCase());
if (!allowedTypes.includes(fileData.extension.toLowerCase())) {
throw new Error("File type not allowed");
}
}
const file = await this.reverseShareRepository.createFile(reverseShare.id, {
...fileData,
size: BigInt(fileData.size),
});
return this.formatFileResponse(file);
}
async downloadReverseShareFile(fileId: string, creatorId: string) {
const file = await this.reverseShareRepository.findFileById(fileId);
if (!file) {
throw new Error("File not found");
}
if (file.reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to download this file");
}
const fileName = file.name;
const expires = 3600; // 1 hour
const url = await this.fileService.getPresignedGetUrl(file.objectName, expires, fileName);
return { url, expiresIn: expires };
}
async deleteReverseShareFile(fileId: string, creatorId: string) {
const file = await this.reverseShareRepository.findFileById(fileId);
if (!file) {
throw new Error("File not found");
}
if (file.reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to delete this file");
}
await this.fileService.deleteObject(file.objectName);
const deletedFile = await this.reverseShareRepository.deleteFile(fileId);
return this.formatFileResponse(deletedFile);
}
async checkPassword(id: string, password: string) {
const reverseShare = await this.reverseShareRepository.findById(id);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (!reverseShare.password) {
return { valid: true };
}
const isValid = await this.reverseShareRepository.comparePassword(password, reverseShare.password);
return { valid: isValid };
}
async updatePassword(id: string, password: string | null, creatorId: string) {
const reverseShare = await this.reverseShareRepository.findById(id);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to update this reverse share");
}
const updatedReverseShare = await this.reverseShareRepository.update(id, { password });
return ReverseShareResponseSchema.parse(this.formatReverseShareResponse(updatedReverseShare));
}
async activateReverseShare(id: string, creatorId: string) {
const reverseShare = await this.reverseShareRepository.findById(id);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to activate this reverse share");
}
const updatedReverseShare = await this.reverseShareRepository.update(id, { isActive: true });
return ReverseShareResponseSchema.parse(this.formatReverseShareResponse(updatedReverseShare));
}
async deactivateReverseShare(id: string, creatorId: string) {
const reverseShare = await this.reverseShareRepository.findById(id);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to deactivate this reverse share");
}
const updatedReverseShare = await this.reverseShareRepository.update(id, { isActive: false });
return ReverseShareResponseSchema.parse(this.formatReverseShareResponse(updatedReverseShare));
}
async createOrUpdateAlias(reverseShareId: string, alias: string, userId: string) {
const reverseShare = await this.reverseShareRepository.findById(reverseShareId);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
if (reverseShare.creatorId !== userId) {
throw new Error("Unauthorized to update this reverse share");
}
const existingAlias = await prisma.reverseShareAlias.findUnique({
where: { alias },
});
if (existingAlias && existingAlias.reverseShareId !== reverseShareId) {
throw new Error("Alias already in use");
}
const reverseShareAlias = await prisma.reverseShareAlias.upsert({
where: { reverseShareId },
create: { reverseShareId, alias },
update: { alias },
});
return {
...reverseShareAlias,
createdAt: reverseShareAlias.createdAt.toISOString(),
updatedAt: reverseShareAlias.updatedAt.toISOString(),
};
}
async updateReverseShareFile(
fileId: string,
data: { name?: string; description?: string | null },
creatorId: string
) {
const file = await this.reverseShareRepository.findFileById(fileId);
if (!file) {
throw new Error("File not found");
}
if (file.reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to edit this file");
}
const updateData = { ...data };
if (data.name) {
const originalExtension = file.extension;
const nameWithoutExtension = data.name.replace(/\.[^/.]+$/, "");
const extensionWithDot = originalExtension.startsWith(".") ? originalExtension : `.${originalExtension}`;
updateData.name = `${nameWithoutExtension}${extensionWithDot}`;
}
const updatedFile = await this.reverseShareRepository.updateFile(fileId, updateData);
return this.formatFileResponse(updatedFile);
}
async copyReverseShareFileToUserFiles(fileId: string, creatorId: string) {
const file = await this.reverseShareRepository.findFileById(fileId);
if (!file) {
throw new Error("File not found");
}
if (file.reverseShare.creatorId !== creatorId) {
throw new Error("Unauthorized to copy this file");
}
const { prisma } = await import("../../shared/prisma.js");
const { ConfigService } = await import("../config/service.js");
const configService = new ConfigService();
const maxFileSize = BigInt(await configService.getValue("maxFileSize"));
if (file.size > maxFileSize) {
const maxSizeMB = Number(maxFileSize) / (1024 * 1024);
throw new Error(`File size exceeds the maximum allowed size of ${maxSizeMB}MB`);
}
const maxTotalStorage = BigInt(await configService.getValue("maxTotalStoragePerUser"));
const userFiles = await prisma.file.findMany({
where: { userId: creatorId },
select: { size: true },
});
const currentStorage = userFiles.reduce((acc: bigint, userFile: any) => acc + userFile.size, BigInt(0));
if (currentStorage + file.size > maxTotalStorage) {
const availableSpace = Number(maxTotalStorage - currentStorage) / (1024 * 1024);
throw new Error(`Insufficient storage space. You have ${availableSpace.toFixed(2)}MB available`);
}
const newObjectName = `${creatorId}/${Date.now()}-${file.name}`;
if (this.fileService.isFilesystemMode()) {
const { FilesystemStorageProvider } = await import("../../providers/filesystem-storage.provider.js");
const provider = FilesystemStorageProvider.getInstance();
// Use streaming copy for filesystem mode
const sourcePath = provider.getFilePath(file.objectName);
const fs = await import("fs");
const { pipeline } = await import("stream/promises");
const sourceStream = fs.createReadStream(sourcePath);
const decryptStream = provider.createDecryptStream();
// Create a passthrough stream to get the decrypted content
const { PassThrough } = await import("stream");
const passThrough = new PassThrough();
// First, decrypt the source file into the passthrough stream
await pipeline(sourceStream, decryptStream, passThrough);
// Then upload the decrypted content
await provider.uploadFileFromStream(newObjectName, passThrough);
} else {
const downloadUrl = await this.fileService.getPresignedGetUrl(file.objectName, 300);
const uploadUrl = await this.fileService.getPresignedPutUrl(newObjectName, 300);
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new Error(`Failed to download file: ${response.statusText}`);
}
if (!response.body) {
throw new Error("No response body received");
}
const uploadResponse = await fetch(uploadUrl, {
method: "PUT",
body: response.body,
headers: {
"Content-Type": "application/octet-stream",
},
});
if (!uploadResponse.ok) {
throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);
}
}
const newFileRecord = await prisma.file.create({
data: {
name: file.name,
description: file.description || `Copied from: ${file.reverseShare.name || "Unnamed"}`,
extension: file.extension,
size: file.size,
objectName: newObjectName,
userId: creatorId,
},
});
return {
id: newFileRecord.id,
name: newFileRecord.name,
description: newFileRecord.description,
extension: newFileRecord.extension,
size: newFileRecord.size.toString(),
objectName: newFileRecord.objectName,
userId: newFileRecord.userId,
createdAt: newFileRecord.createdAt.toISOString(),
updatedAt: newFileRecord.updatedAt.toISOString(),
};
}
private formatReverseShareResponse(reverseShare: ReverseShareData) {
const result = {
id: reverseShare.id,
name: reverseShare.name,
description: reverseShare.description,
expiration: reverseShare.expiration?.toISOString() || null,
maxFiles: reverseShare.maxFiles,
maxFileSize: reverseShare.maxFileSize ? Number(reverseShare.maxFileSize) : null,
allowedFileTypes: reverseShare.allowedFileTypes,
pageLayout: reverseShare.pageLayout,
isActive: reverseShare.isActive,
hasPassword: !!reverseShare.password,
createdAt: reverseShare.createdAt.toISOString(),
updatedAt: reverseShare.updatedAt.toISOString(),
creatorId: reverseShare.creatorId,
files: (reverseShare.files || []).map((file: any) => ({
id: file.id,
name: file.name,
description: file.description,
extension: file.extension,
size: file.size.toString(),
objectName: file.objectName,
uploaderEmail: file.uploaderEmail,
uploaderName: file.uploaderName,
reverseShareId: file.reverseShareId,
createdAt: file.createdAt.toISOString(),
updatedAt: file.updatedAt.toISOString(),
})),
alias: reverseShare.alias
? {
id: reverseShare.alias.id,
alias: reverseShare.alias.alias,
reverseShareId: reverseShare.alias.reverseShareId,
createdAt: reverseShare.alias.createdAt.toISOString(),
updatedAt: reverseShare.alias.updatedAt.toISOString(),
}
: null,
nameFieldRequired: reverseShare.nameFieldRequired,
emailFieldRequired: reverseShare.emailFieldRequired,
};
return result;
}
private formatFileResponse(file: any) {
return {
id: file.id,
name: file.name,
description: file.description,
extension: file.extension,
size: file.size.toString(),
objectName: file.objectName,
uploaderEmail: file.uploaderEmail,
uploaderName: file.uploaderName,
reverseShareId: file.reverseShareId,
createdAt: file.createdAt.toISOString(),
updatedAt: file.updatedAt.toISOString(),
};
}
}