template-saas/apps/backend/dist/modules/storage/services/storage.service.js
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

349 lines
13 KiB
JavaScript

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var StorageService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const uuid_1 = require("uuid");
const file_entity_1 = require("../entities/file.entity");
const pending_upload_entity_1 = require("../entities/pending-upload.entity");
const storage_usage_entity_1 = require("../entities/storage-usage.entity");
const s3_provider_1 = require("../providers/s3.provider");
const ALLOWED_MIME_TYPES = {
images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'],
documents: [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
],
spreadsheets: [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
data: ['text/csv', 'application/json', 'text/plain'],
};
const BLOCKED_EXTENSIONS = ['.exe', '.bat', '.sh', '.php', '.js', '.cmd', '.com', '.scr'];
let StorageService = StorageService_1 = class StorageService {
constructor(fileRepository, pendingUploadRepository, usageRepository, s3Provider) {
this.fileRepository = fileRepository;
this.pendingUploadRepository = pendingUploadRepository;
this.usageRepository = usageRepository;
this.s3Provider = s3Provider;
this.logger = new common_1.Logger(StorageService_1.name);
}
async getUploadUrl(tenantId, userId, dto) {
if (!this.s3Provider.isConfigured()) {
throw new common_1.BadRequestException('Storage not configured');
}
if (!this.isAllowedMimeType(dto.mimeType)) {
throw new common_1.BadRequestException(`File type not allowed: ${dto.mimeType}`);
}
if (this.hasBlockedExtension(dto.filename)) {
throw new common_1.BadRequestException('File extension not allowed');
}
const canUpload = await this.canUpload(tenantId, dto.sizeBytes);
if (!canUpload.allowed) {
throw new common_1.ForbiddenException(canUpload.reason);
}
const uploadId = (0, uuid_1.v4)();
const folder = dto.folder || 'files';
const path = this.s3Provider.generatePath(tenantId, folder, uploadId, dto.filename);
const bucket = this.s3Provider.getBucket();
const presigned = await this.s3Provider.getUploadUrl({
bucket,
key: path,
contentType: dto.mimeType,
contentLength: dto.sizeBytes,
metadata: {
'tenant-id': tenantId,
'upload-id': uploadId,
'user-id': userId,
},
});
const pendingUpload = this.pendingUploadRepository.create({
id: uploadId,
tenant_id: tenantId,
user_id: userId,
filename: this.sanitizeFilename(dto.filename),
original_name: dto.filename,
mime_type: dto.mimeType,
size_bytes: dto.sizeBytes,
folder,
bucket,
path,
provider: this.s3Provider.getProvider(),
expires_at: presigned.expiresAt,
});
await this.pendingUploadRepository.save(pendingUpload);
return {
uploadId,
url: presigned.url,
expiresAt: presigned.expiresAt,
maxSize: dto.sizeBytes,
};
}
async confirmUpload(tenantId, userId, dto) {
const pending = await this.pendingUploadRepository.findOne({
where: {
id: dto.uploadId,
tenant_id: tenantId,
status: 'pending',
},
});
if (!pending) {
throw new common_1.NotFoundException('Upload not found or expired');
}
if (pending.expires_at < new Date()) {
await this.pendingUploadRepository.update(pending.id, { status: 'expired' });
throw new common_1.BadRequestException('Upload expired');
}
const headResult = await this.s3Provider.headObject(pending.path);
if (!headResult) {
await this.pendingUploadRepository.update(pending.id, { status: 'failed' });
throw new common_1.BadRequestException('File not found in storage');
}
const file = this.fileRepository.create({
tenant_id: tenantId,
uploaded_by: userId,
filename: pending.filename,
original_name: pending.original_name,
mime_type: pending.mime_type,
size_bytes: headResult.contentLength,
bucket: pending.bucket,
path: pending.path,
provider: pending.provider,
folder: pending.folder,
status: file_entity_1.FileStatus.READY,
visibility: file_entity_1.FileVisibility.PRIVATE,
metadata: dto.metadata || {},
});
await this.fileRepository.save(file);
await this.pendingUploadRepository.update(pending.id, {
status: 'completed',
completed_at: new Date(),
});
return this.toFileResponse(file);
}
async listFiles(tenantId, dto) {
const { page = 1, limit = 20, folder, mimeType, search } = dto;
const skip = (page - 1) * limit;
const where = {
tenant_id: tenantId,
deleted_at: (0, typeorm_2.IsNull)(),
status: file_entity_1.FileStatus.READY,
};
if (folder) {
where.folder = folder;
}
if (mimeType) {
where.mime_type = (0, typeorm_2.Like)(`${mimeType}%`);
}
if (search) {
where.original_name = (0, typeorm_2.ILike)(`%${search}%`);
}
const [files, total] = await this.fileRepository.findAndCount({
where,
order: { created_at: 'DESC' },
skip,
take: limit,
});
return {
data: files.map((f) => this.toFileResponse(f)),
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async getFile(tenantId, fileId) {
const file = await this.fileRepository.findOne({
where: {
id: fileId,
tenant_id: tenantId,
deleted_at: (0, typeorm_2.IsNull)(),
},
});
if (!file) {
throw new common_1.NotFoundException('File not found');
}
return this.toFileResponse(file);
}
async getDownloadUrl(tenantId, fileId) {
const file = await this.fileRepository.findOne({
where: {
id: fileId,
tenant_id: tenantId,
deleted_at: (0, typeorm_2.IsNull)(),
status: file_entity_1.FileStatus.READY,
},
});
if (!file) {
throw new common_1.NotFoundException('File not found');
}
const result = await this.s3Provider.getDownloadUrl(file.path);
return {
url: result.url,
expiresAt: result.expiresAt,
};
}
async updateFile(tenantId, fileId, dto) {
const file = await this.fileRepository.findOne({
where: {
id: fileId,
tenant_id: tenantId,
deleted_at: (0, typeorm_2.IsNull)(),
},
});
if (!file) {
throw new common_1.NotFoundException('File not found');
}
if (dto.folder)
file.folder = dto.folder;
if (dto.visibility)
file.visibility = dto.visibility;
if (dto.metadata)
file.metadata = { ...file.metadata, ...dto.metadata };
await this.fileRepository.save(file);
return this.toFileResponse(file);
}
async deleteFile(tenantId, fileId) {
const file = await this.fileRepository.findOne({
where: {
id: fileId,
tenant_id: tenantId,
deleted_at: (0, typeorm_2.IsNull)(),
},
});
if (!file) {
throw new common_1.NotFoundException('File not found');
}
file.deleted_at = new Date();
file.status = file_entity_1.FileStatus.DELETED;
await this.fileRepository.save(file);
try {
await this.s3Provider.deleteObject(file.path);
}
catch (error) {
this.logger.warn(`Failed to delete file from S3: ${file.path}`, error);
}
}
async getUsage(tenantId) {
let usage = await this.usageRepository.findOne({
where: { tenant_id: tenantId },
});
if (!usage) {
usage = this.usageRepository.create({
tenant_id: tenantId,
total_files: 0,
total_bytes: 0,
});
await this.usageRepository.save(usage);
}
const folderStats = await this.fileRepository
.createQueryBuilder('f')
.select('f.folder', 'folder')
.addSelect('COUNT(*)', 'count')
.where('f.tenant_id = :tenantId', { tenantId })
.andWhere('f.deleted_at IS NULL')
.andWhere('f.status = :status', { status: file_entity_1.FileStatus.READY })
.groupBy('f.folder')
.getRawMany();
const filesByFolder = {};
for (const stat of folderStats) {
filesByFolder[stat.folder] = parseInt(stat.count, 10);
}
const usagePercent = usage.max_bytes
? Math.round((Number(usage.total_bytes) / Number(usage.max_bytes)) * 10000) / 100
: 0;
return {
totalFiles: usage.total_files,
totalBytes: Number(usage.total_bytes),
maxBytes: usage.max_bytes ? Number(usage.max_bytes) : null,
maxFileSize: usage.max_file_size ? Number(usage.max_file_size) : null,
usagePercent,
filesByFolder,
};
}
isAllowedMimeType(mimeType) {
const allAllowed = Object.values(ALLOWED_MIME_TYPES).flat();
return allAllowed.includes(mimeType);
}
hasBlockedExtension(filename) {
const lower = filename.toLowerCase();
return BLOCKED_EXTENSIONS.some((ext) => lower.endsWith(ext));
}
sanitizeFilename(filename) {
return filename.replace(/[^a-zA-Z0-9.-]/g, '_');
}
async canUpload(tenantId, sizeBytes) {
const usage = await this.usageRepository.findOne({
where: { tenant_id: tenantId },
});
if (!usage) {
return { allowed: true };
}
if (usage.max_file_size && sizeBytes > Number(usage.max_file_size)) {
return {
allowed: false,
reason: `File size exceeds limit (max: ${this.formatBytes(Number(usage.max_file_size))})`,
};
}
if (usage.max_bytes &&
Number(usage.total_bytes) + sizeBytes > Number(usage.max_bytes)) {
return {
allowed: false,
reason: `Storage limit exceeded (max: ${this.formatBytes(Number(usage.max_bytes))})`,
};
}
return { allowed: true };
}
formatBytes(bytes) {
if (bytes < 1024)
return `${bytes} B`;
if (bytes < 1024 * 1024)
return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024)
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
toFileResponse(file) {
return {
id: file.id,
filename: file.filename,
originalName: file.original_name,
mimeType: file.mime_type,
sizeBytes: Number(file.size_bytes),
folder: file.folder,
visibility: file.visibility,
thumbnails: file.thumbnails,
metadata: file.metadata,
createdAt: file.created_at,
updatedAt: file.updated_at,
};
}
};
exports.StorageService = StorageService;
exports.StorageService = StorageService = StorageService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, typeorm_1.InjectRepository)(file_entity_1.FileEntity)),
__param(1, (0, typeorm_1.InjectRepository)(pending_upload_entity_1.PendingUploadEntity)),
__param(2, (0, typeorm_1.InjectRepository)(storage_usage_entity_1.StorageUsageEntity)),
__metadata("design:paramtypes", [typeorm_2.Repository,
typeorm_2.Repository,
typeorm_2.Repository,
s3_provider_1.S3Provider])
], StorageService);
//# sourceMappingURL=storage.service.js.map