first commit
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import sharp from "sharp";
|
||||
|
||||
const MAX_SIZE_BYTES = 512 * 1024; // 512 KB
|
||||
|
||||
/**
|
||||
* Convierte una imagen a WebP y asegura que no supere MAX_SIZE_BYTES.
|
||||
* Mantiene el aspect ratio y ajusta calidad si la reducción supera el 50%.
|
||||
* @param inputBuffer Buffer con la imagen original
|
||||
* @returns Buffer de la imagen en WebP
|
||||
*/
|
||||
export const processImageToWebP = async (inputBuffer: Buffer): Promise<Buffer> => {
|
||||
let quality = 80;
|
||||
let width: number | null = null;
|
||||
const metadata = await sharp(inputBuffer).metadata();
|
||||
const originalWidth = metadata.width || 0;
|
||||
if (!originalWidth) throw new Error("No se pudo obtener el ancho de la imagen original");
|
||||
|
||||
let outputBuffer: Buffer;
|
||||
|
||||
while (true) {
|
||||
const transformer = sharp(inputBuffer).webp({ quality });
|
||||
if (width) {
|
||||
transformer.resize({ width, withoutEnlargement: true });
|
||||
}
|
||||
|
||||
outputBuffer = await transformer.toBuffer();
|
||||
|
||||
if (outputBuffer.length <= MAX_SIZE_BYTES) break;
|
||||
|
||||
// Reducimos width progresivamente
|
||||
if (!width) {
|
||||
width = Math.floor(originalWidth * 0.9); // reducimos 10% la primera vez
|
||||
} else {
|
||||
width = Math.floor(width * 0.9); // reducciones sucesivas
|
||||
}
|
||||
|
||||
// Calculamos porcentaje de reducción respecto al tamaño original
|
||||
const reductionPercent = (originalWidth - width) / originalWidth;
|
||||
|
||||
// Si se redujo más del 50%, empezamos a bajar la calidad
|
||||
if (reductionPercent >= 0.5 && quality > 40) {
|
||||
quality -= 5;
|
||||
width = null; // volvemos a tamaño original pero con menor calidad
|
||||
}
|
||||
|
||||
// Evitamos bucle infinito
|
||||
if (width && width < 10) break;
|
||||
}
|
||||
|
||||
return outputBuffer;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { SimpleAuthenticationDetailsProvider, Region } from "oci-common";
|
||||
import { ObjectStorageClient } from "oci-objectstorage";
|
||||
|
||||
const userOcId = process.env.USER_OCID!;
|
||||
const tenancyOcId = process.env.TENANCY_OCID!;
|
||||
const privateKeyPath = process.env.PRIVATE_KEY_PATH!;
|
||||
const fingerprint = process.env.FINGERPRINT!;
|
||||
const namespace = process.env.NAMESPACE!;
|
||||
|
||||
if (!userOcId || !tenancyOcId || !privateKeyPath || !fingerprint || !namespace) {
|
||||
throw new Error(
|
||||
"Faltan variables de entorno de Oracle (USER_OCID, TENANCY_OCID, PRIVATE_KEY_PATH, FINGERPRINT, NAMESPACE)"
|
||||
);
|
||||
}
|
||||
|
||||
const privateKey = readFileSync(privateKeyPath, "utf-8");
|
||||
|
||||
const region = Region.fromRegionId("sa-saopaulo-1");
|
||||
|
||||
const provider = new SimpleAuthenticationDetailsProvider(
|
||||
tenancyOcId,
|
||||
userOcId,
|
||||
fingerprint,
|
||||
privateKey,
|
||||
null,
|
||||
region
|
||||
);
|
||||
|
||||
const client = new ObjectStorageClient({ authenticationDetailsProvider: provider });
|
||||
|
||||
export const subirArchivo = async (bucketName: string, objectName: string, data: Buffer) => {
|
||||
const request = {
|
||||
namespaceName: namespace,
|
||||
bucketName,
|
||||
objectName,
|
||||
putObjectBody: data,
|
||||
};
|
||||
return client.putObject(request);
|
||||
};
|
||||
|
||||
export const descargarArchivo = async (bucketName: string, objectName: string) => {
|
||||
const request = {
|
||||
namespaceName: namespace,
|
||||
bucketName,
|
||||
objectName,
|
||||
};
|
||||
return client.getObject(request);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { processImageToWebP } from "./imageCompresionService";
|
||||
import * as oracleService from "../services/oracleStorageService";
|
||||
|
||||
const BUCKET = process.env.BUCKET_NAME!;
|
||||
if (!BUCKET) throw new Error("Faltan variables de entorno: BUCKET_NAME");
|
||||
|
||||
export type ProcessFileResult = {
|
||||
objectName: string;
|
||||
originalName: string;
|
||||
bufferToUpload: Buffer<ArrayBufferLike>;
|
||||
mimeType: string;
|
||||
};
|
||||
|
||||
export const processFile = async (file: Express.Multer.File) => {
|
||||
let bufferToUpload = file.buffer;
|
||||
let mimeType = file.mimetype;
|
||||
const originalName = file.originalname;
|
||||
|
||||
// Si es imagen, procesar con sharp (resize + convertir a jpeg)
|
||||
if (mimeType.startsWith("image/")) {
|
||||
bufferToUpload = await processImageToWebP(bufferToUpload);
|
||||
mimeType = "image/webp";
|
||||
}
|
||||
|
||||
// Generar objectName único para evitar colisiones
|
||||
const timestamp = Date.now();
|
||||
// escapamos/normalizamos el originalName ligeramente
|
||||
const safeOriginal = originalName.replace(/\s+/g, "_");
|
||||
const objectName = `${timestamp}-${safeOriginal}`;
|
||||
|
||||
// Subir a Oracle
|
||||
await oracleService.subirArchivo(BUCKET, objectName, bufferToUpload);
|
||||
|
||||
return {
|
||||
objectName,
|
||||
originalName,
|
||||
bufferToUpload,
|
||||
mimeType,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export type ValidateFilesResult = {
|
||||
files: Express.Multer.File[];
|
||||
};
|
||||
|
||||
export const validateFiles = async (req: Request, res: Response): Promise<ValidateFilesResult> => {
|
||||
const files = req.files as Express.Multer.File[] | undefined;
|
||||
if (!files || files.length === 0) throw new Error("No se encontraron archivos");
|
||||
|
||||
return {
|
||||
files,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { validateFiles } from "./validateFiles";
|
||||
import { validateSessionUser } from "./validateUser";
|
||||
import CompanySchema from "../models/companies";
|
||||
import EmployeeSchema from "../models/employees";
|
||||
|
||||
export type ValidateOrganizationResult = {
|
||||
files: Express.Multer.File[];
|
||||
organizationId: string;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export const validateOrganization = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<ValidateOrganizationResult> => {
|
||||
const filesResul = await validateFiles(req, res);
|
||||
const { sessionUser, organizationId } = req.body;
|
||||
if (!organizationId || !sessionUser) {
|
||||
throw new Error("Faltan campos obligatorios: userId o profileId");
|
||||
}
|
||||
|
||||
await validateSessionUser(sessionUser);
|
||||
|
||||
const company = await CompanySchema.findOne({
|
||||
_id: organizationId,
|
||||
});
|
||||
|
||||
if (!company) {
|
||||
throw new Error("No se ha encontrado la organización");
|
||||
}
|
||||
|
||||
const employee = await EmployeeSchema.findOne({
|
||||
userId: sessionUser,
|
||||
companyId: organizationId,
|
||||
});
|
||||
|
||||
if (!employee) {
|
||||
throw new Error("El usuario no pertenece a la organización");
|
||||
}
|
||||
|
||||
return {
|
||||
files: filesResul.files,
|
||||
organizationId,
|
||||
sessionUser,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import mongoose from "mongoose";
|
||||
import UserSchema from "../models/users";
|
||||
|
||||
export const validateSessionUser = async (sessionUser: string): Promise<void> => {
|
||||
const user = await UserSchema.findOne({
|
||||
_id: new mongoose.Types.ObjectId(sessionUser),
|
||||
});
|
||||
|
||||
console.log(new mongoose.Types.ObjectId(sessionUser));
|
||||
|
||||
if (!user) {
|
||||
throw new Error(
|
||||
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user