diff --git a/.gitignore b/.gitignore index 895a6a8dc4..c43834ac98 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ print.*.json db.json junit.xml .DS_Store -storage \ No newline at end of file +storage diff --git a/back/model-config.json b/back/model-config.json index b643ab54f1..2952648cf7 100644 --- a/back/model-config.json +++ b/back/model-config.json @@ -73,6 +73,9 @@ "ImageCollectionSize": { "dataSource": "vn" }, + "ImageConfig": { + "dataSource": "vn" + }, "ImageContainer": { "dataSource": "imageStorage" }, diff --git a/back/models/image-config.json b/back/models/image-config.json new file mode 100644 index 0000000000..325859d916 --- /dev/null +++ b/back/models/image-config.json @@ -0,0 +1,27 @@ +{ + "name": "ImageConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "hedera.imageConfig" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + }, + "maxSize": { + "type": "number" + }, + "useXsendfile": { + "type": "number" + }, + "url": { + "type": "string" + }, + "dirLevels": { + "type": "number" + } + } +} diff --git a/back/models/image.js b/back/models/image.js index e13f9e100c..41db43cae1 100644 --- a/back/models/image.js +++ b/back/models/image.js @@ -1,112 +1,194 @@ const fs = require('fs-extra'); const path = require('path'); const gm = require('gm'); - +const crypto = require('crypto'); +const {models} = require('vn-loopback/server/server'); +const PNG = 'png'; +const FORMAT = PNG; +const DEFAULT_GRAVITY = 'Center'; +const DEFAULT_QUALITY = 100; +const SIZE_UNIT = 'x'; +const formatWidthHeight = (width, height) => `${width}${SIZE_UNIT}${height}`; +const parseSize = value => formatWidthHeight(value.width, value.height).split(SIZE_UNIT); module.exports = Self => { require('../methods/image/download')(Self); require('../methods/image/upload')(Self); require('../methods/image/scrub')(Self); - - Self.resize = async function({collectionName, srcFile, fileName, entityId}) { - const models = Self.app.models; - - const collection = await models.ImageCollection.findOne( - { - fields: [ - 'id', - 'maxWidth', - 'maxHeight', - 'model', - 'property', - ], - where: {name: collectionName}, - include: { - relation: 'sizes', - scope: { - fields: ['width', 'height', 'crop'], - }, - }, - } - ); - + Self.handleFolderDestination = async fileName => { // Insert image row - const imageName = path.parse(fileName).name; - await models.Image.upsertWithWhere( - { - name: imageName, - collectionFk: collectionName + const {name} = path.parse(fileName); + const hash = crypto.createHash('sha1').update(name).digest('hex'); + const pairs = hash.match(/(..?)/g); + const {dirLevels} = await models.ImageConfig.findOne({ + fields: ['dirLevels'], + }); + const dstDir = pairs.slice(0, dirLevels).reverse().join('/'); + + return {name, dstDir}; + }; + Self.getCollection = async collectionName => { + const collection = await models.ImageCollection.findOne({ + fields: ['id', 'maxWidth', 'maxHeight', 'model', 'property'], + where: {name: collectionName}, + include: { + relation: 'sizes', + scope: { + fields: ['width', 'height', 'crop'], + }, }, - { - name: imageName, - collectionFk: collectionName, - updated: Date.vnNow() / 1000, - } - ); - - // Update entity image file name - const model = models[collection.model]; - if (!model) throw new Error('No matching model found'); - - const entity = await model.findById(entityId); - if (entity) { - await entity.updateAttribute( - collection.property, - imageName - ); - } - - // Resize - const container = await models.ImageContainer.container( - collectionName - ); - const rootPath = container.client.root; - const collectionDir = path.join(rootPath, collectionName); - - // To max size - const {maxWidth, maxHeight} = collection; - const fullSizePath = path.join(collectionDir, 'full'); - const toFullSizePath = `${fullSizePath}/${fileName}`; - - await fs.mkdir(fullSizePath, {recursive: true}); - await new Promise((resolve, reject) => { - gm(srcFile) - .resize(maxWidth, maxHeight, '>') - .setFormat('png') - .quality(100) - .write(toFullSizePath, function(err) { - if (err) reject(err); - if (!err) resolve(); - }); }); - // To collection sizes - for (const size of collection.sizes()) { - const {width, height} = size; + return collection; + }; - const sizePath = path.join(collectionDir, `${width}x${height}`); - const toSizePath = `${sizePath}/${fileName}`; + Self.getCollectionDir = async collectionName => { + const container = await models.ImageContainer.container(collectionName); + const rootPath = container.client.root; + const collectionDir = path.join(rootPath, collectionName); + return collectionDir; + }; - await fs.mkdir(sizePath, {recursive: true}); + Self.getFullSizePath = (fileName, collectionDir, dstDir) => { + try { + const fullSizePath = path.join(collectionDir, `full/${dstDir}`); + const fullSizeOriginalPath = path.join(collectionDir, `full`); + const toFullSizePath = `${fullSizePath}/${fileName}`; + const toFullSizeOriginalPath = `${fullSizeOriginalPath}/${fileName}`; + return { + fullSizePath, + toFullSizePath, + toFullSizeOriginalPath + }; + } catch (e) { + throw new Error(e); + } + }; + Self.removeLink = async(child, parent = null) => { + try { + await fs.unlink(child); + await fs.symlink(parent, child); + } catch (e) { + throw new Error(e); + } + }; + Self.createLink = async(parent, child = null) => { + try { + const exists = await fs.exists(parent); + exists && await fs.unlink(parent); + await fs.symlink(child, parent); + } catch (e) { + throw new Error(e); + } + }; + + Self.resize = async function({ + collectionName, + srcFile, + fileName, + entityId, + }) { + const {name, dstDir} = await Self.handleFolderDestination(fileName); + try { + const baseUpsert = { + name, + collectionFk: collectionName, + }; + await models.Image.upsertWithWhere(baseUpsert, + { + ...baseUpsert, + updated: Date.vnNow() / 1000, + }); + + const collection = await Self.getCollection(collectionName); + const {maxWidth, maxHeight} = collection; + + // Update entity image file name + const model = models[collection.model]; + if (!model) throw new Error('No matching model found'); + + const entity = await model.findById(entityId); + if (entity) + await entity.updateAttribute(collection.property, name); + + // Resize + const collectionDir = await Self.getCollectionDir(collectionName); + + // To max size + const _fullSizePath = Self.getFullSizePath(fileName, collectionDir, dstDir); + + const {fullSizePath, toFullSizePath, toFullSizeOriginalPath} = _fullSizePath; + + await fs.mkdir(fullSizePath, {recursive: true}); + + const gmInstance = gm(srcFile); + let fileWidth = null; + let fileHeight = null; + + gmInstance.size(function(err, size) { + if (err) throw new Error(err); + [fileWidth, fileHeight] = parseSize(size); + }); await new Promise((resolve, reject) => { - const gmInstance = gm(srcFile); - - if (size.crop) { - gmInstance - .resize(width, height, '^') - .gravity('Center') - .crop(width, height); - } - - if (!size.crop) gmInstance.resize(width, height, '>'); - gmInstance - .setFormat('png') - .quality(100) - .write(toSizePath, function(err) { + .resize(maxWidth, maxHeight, '>') + .setFormat(PNG) + .quality(DEFAULT_QUALITY) + .write(toFullSizePath + `.${FORMAT}`, function(err, data) { if (err) reject(err); - if (!err) resolve(); + if (!err) resolve(data); }); }); + + await Self.createLink(toFullSizeOriginalPath, toFullSizePath); + // To collection sizes + for (const size of collection.sizes()) { + const [width, height] = parseSize(size); + + const sizePath = path.join( + collectionDir, + `${formatWidthHeight(width, height)}/${dstDir}` + ); + + try { + await fs.mkdir(sizePath, {recursive: true}); + } catch (e) { + throw new Error(e); + } + + const toSizePath = `${sizePath}/${fileName}`; + if (+fileWidth < +width && +fileHeight < +height) { + await new Promise((resolve, reject) => { + if (size.crop) { + gmInstance + .resize(width, height, '^') + .gravity(DEFAULT_GRAVITY) + .crop(width, height).res(function(err, data) { + if (err) reject(err); + if (!err) resolve(data); + }); + } else gmInstance.resize(width, height, '>'); + + gmInstance + .setFormat(PNG) + .quality(DEFAULT_QUALITY) + .write(toSizePath + `.${FORMAT}`, function(err, data) { + if (err) reject(err); + if (!err) resolve(data); + }); + }); + } + + const sizeOriginalPath = path.join( + collectionDir, + formatWidthHeight(width, height) + ); + + const toSizeOriginalPath = `${sizeOriginalPath}/${fileName}`; + + await Self.createLink(toSizeOriginalPath, toSizePath); + } + } catch (e) { + throw new Error(e); } }; }; diff --git a/back/models/specs/images.spec.js b/back/models/specs/images.spec.js new file mode 100644 index 0000000000..0c4dbeafad --- /dev/null +++ b/back/models/specs/images.spec.js @@ -0,0 +1,145 @@ +const fs = require('fs-extra'); +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); +const nameItem = 'Pallet'; +const collectionName = 'user'; +let obj = {}; +const STORAGE_IMAGE_USER = 'storage/image/user'; +const _23_F2 = '23/f2'; +const FULL_23_F2 = `full/${_23_F2}`; +fdescribe('loopback model Image', () => { + const userId = 1107; + + const activeCtx = { + accessToken: {userId: userId}, + http: { + req: { + headers: {origin: 'http://localhost'}, + }, + }, + }; + beforeAll(async() => { + const {name: fileName, id: entityId} = await models.Item.findOne({where: {name: nameItem}}); + obj = { + collectionName, + srcFile: 'front/core/directives/no-image.png', + fileName, + entityId, + }; + // Ruta del directorio y nombre del archivo que buscas + + // Llama a la función para mostrar el listado y verificar el archivo + mostrarListadoYVerificar(`${STORAGE_IMAGE_USER}/full`, nameItem); + cleanResources(); + // try { + // await fs.unlink(`${STORAGE_IMAGE_USER}/full/${FULL_23_F2}/${nameItem}`); + // await fs.unlink(`${STORAGE_IMAGE_USER}/160x100/${FULL_23_F2}/${nameItem}`); + + // await fs.unlink(`${STORAGE_IMAGE_USER}/520x520/${FULL_23_F2}/${nameItem}`); + + // await fs.unlink(`${STORAGE_IMAGE_USER}/1600x1600/${FULL_23_F2}/${nameItem}`); + // await fs.unlink(`${STORAGE_IMAGE_USER}/full/${nameItem}`); + // await fs.unlink(`${STORAGE_IMAGE_USER}/160x100/${nameItem}`); + + // await fs.unlink(`${STORAGE_IMAGE_USER}/520x520/${nameItem}`); + + // await fs.unlink(`${STORAGE_IMAGE_USER}/1600x1600/${nameItem}`); + // } catch (e) { + // console.warn(e); + // } + }); + + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx, + }); + }); + + it('should handle folder destination', async() => { + try { + const {name, dstDir} = await models.Image.handleFolderDestination(obj.fileName); + + expect(name).toEqual(nameItem); + expect(dstDir).toEqual(`${_23_F2}`); + const collectionDir = await models.Image.getCollectionDir(collectionName); + + expect(collectionDir).toEqual(`${STORAGE_IMAGE_USER}`); + } catch (e) { + throw new Error(e); + } + }); + + it('should handle full size path', async() => { + try { + const {dstDir} = await models.Image.handleFolderDestination(obj.fileName); + const collectionDir = await models.Image.getCollectionDir(collectionName); + const _fullSizePath = models.Image.getFullSizePath(obj.fileName, collectionDir, dstDir); + const {fullSizePath, toFullSizePath, toFullSizeOriginalPath} = _fullSizePath; + + expect(fullSizePath).toEqual(`${STORAGE_IMAGE_USER}/${FULL_23_F2}`); + expect(toFullSizePath).toEqual(`${STORAGE_IMAGE_USER}/${FULL_23_F2}/${nameItem}`); + expect(toFullSizeOriginalPath).toEqual(`${STORAGE_IMAGE_USER}/full/${nameItem}`); + } catch (e) { + throw new Error(e); + } + }); + + it('should resize', async() => { + try { + await models.Image.resize(obj); + const fileExists = await fs.exists(`${STORAGE_IMAGE_USER}/${FULL_23_F2}/${nameItem}.png`); + + expect(fileExists).toBeTrue(); + + const linkExists = await fs.readlink(`${STORAGE_IMAGE_USER}/full/${nameItem}`); + + expect(linkExists).toEqual(`${STORAGE_IMAGE_USER}/${FULL_23_F2}/${nameItem}`); + } catch (e) { + throw new Error(e); + } + }); + + afterAll(async() => { + // cleanResources(); + }); +}); + +async function cleanResources() { + try { + await fs.unlink(`${STORAGE_IMAGE_USER}/160x160/${nameItem}`); + // await fs.unlink(`${STORAGE_IMAGE_USER}/160x160/${_23_F2}/${nameItem}`); + + await fs.unlink(`${STORAGE_IMAGE_USER}/520x520/${nameItem}`); + // await fs.unlink(`${STORAGE_IMAGE_USER}/520x520/${_23_F2}/${nameItem}`); + + await fs.unlink(`${STORAGE_IMAGE_USER}/1600x1600/${nameItem}`); + // await fs.unlink(`${STORAGE_IMAGE_USER}/1600x1600/${_23_F2}/${nameItem}`); + + await fs.unlink(`${STORAGE_IMAGE_USER}/full/${nameItem}`); + // await fs.unlink(`${STORAGE_IMAGE_USER}/full/${_23_F2}/${nameItem}`); + } catch (e) { + console.warn(e); + } +} + +function mostrarListadoYVerificar(rutaDirectorio, archivoBuscado) { + // Lee el contenido del directorio + fs.readdir(rutaDirectorio, (err, archivos) => { + if (err) { + console.error('Error al leer el directorio:', err); + return; + } + + // Muestra el listado de archivos + console.log('Listado de archivos en el directorio:', rutaDirectorio); + archivos.forEach(archivo => { + console.log(archivo); + }); + + // Verifica si el archivo buscado está en el listado + if (archivos.includes(archivoBuscado)) + console.log(`El archivo "${archivoBuscado}" está presente en el directorio.`); + else + console.log(`El archivo "${archivoBuscado}" no está presente en el directorio.`); + }); +} diff --git a/db/dump/fixtures.before.sql b/db/dump/fixtures.before.sql index 47cba44b5d..c07f8ac873 100644 --- a/db/dump/fixtures.before.sql +++ b/db/dump/fixtures.before.sql @@ -3057,6 +3057,10 @@ INSERT INTO `vn`.`buyConfig` (`id`, `monthsAgo`) VALUES (1, 6); +INSERT INTO `hedera`.`imageConfig` (`id`, `maxSize`, `useXsendfile`, `dirLevels`, `url`) + VALUES + (1, 20, 0, 2, 'marvel.com'); + INSERT INTO `vn`.`invoiceInSerial` (`code`, `description`, `cplusTerIdNifFk`, `taxAreaFk`) VALUES ('C', 'Asgard', 1, 'WORLD'), @@ -3065,9 +3069,7 @@ INSERT INTO `vn`.`invoiceInSerial` (`code`, `description`, `cplusTerIdNifFk`, `t ('W', 'Vanaheim', 1, 'WORLD'); -INSERT INTO `hedera`.`imageConfig` (`id`, `maxSize`, `useXsendfile`, `url`) - VALUES - (1, 0, 0, 'marvel.com'); + INSERT INTO vn.XDiario (id, ASIEN, FECHA, SUBCTA, CONTRA, CONCEPTO, EURODEBE, EUROHABER, BASEEURO, SERIE, FACTURA, IVA, RECEQUIV, CLAVE, CAMBIO, DEBEME, HABERME, AUXILIAR, MONEDAUSO, TIPOOPE, NFACTICK, TERIDNIF, TERNIF, TERNOM, OPBIENES, L340, enlazado, FECHA_EX, LRECT349, empresa_id, LDIFADUAN, METAL, METALIMP, CLIENTE, METALEJE, FECHA_OP, FACTURAEX, TIPOCLAVE, TIPOEXENCI, TIPONOSUJE, TIPOFACT, TIPORECTIF, SERIE_RT, FACTU_RT, BASEIMP_RT, BASEIMP_RF, RECTIFICA, FECHA_RT, FECREGCON, enlazadoSage) VALUES @@ -3867,4 +3869,4 @@ INSERT INTO vn.sectorCollection INSERT INTO vn.sectorCollectionSaleGroup SET id = 9, sectorCollectionFk = 3, - saleGroupFk = 6; \ No newline at end of file + saleGroupFk = 6; diff --git a/db/routines/vn/procedures/00-hederaImageConfig.sql b/db/routines/vn/procedures/00-hederaImageConfig.sql new file mode 100644 index 0000000000..f1cdd44ead --- /dev/null +++ b/db/routines/vn/procedures/00-hederaImageConfig.sql @@ -0,0 +1,4 @@ +ALTER TABLE `hedera`.`imageConfig` ADD dirLevels INT UNSIGNED NOT NULL DEFAULT 2; +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('ImageConfig', '*', 'READ', 'ALLOW', 'ROLE', 'employee'); diff --git a/storage/image/move.sh b/storage/image/move.sh new file mode 100644 index 0000000000..a981eab8f0 --- /dev/null +++ b/storage/image/move.sh @@ -0,0 +1,19 @@ +#!/bin/bash +DIR_LEVELS=2 +for collection in */ ; do + for size in "$collection"*/ ; do + for image in "$size"* ; do + if [ -f "$image" ]; then + fileName=$(basename "$image") + imageName="${fileName%.*}" + hash=$(echo -n "$imageName" | sha1sum | awk '{print $1}') + first=$(echo "$hash" | cut -c"$inicio"-"$DIR_LEVELS") + second=$(echo "$hash" | cut -c"$DIR_LEVELS"-"$fin") + path=$(dirname "$image")/${first}/${second} + mkdir -p $path + ln -s "$image" "$fileName" + mv $image $path/$fileName + fi + done + done +done diff --git a/storage/image/prod.sh b/storage/image/prod.sh new file mode 100644 index 0000000000..d764c0dd48 --- /dev/null +++ b/storage/image/prod.sh @@ -0,0 +1,28 @@ +#!/bin/bash +MIN_DIR_LEVELS=0 +DIR_LEVELS=2 +START=1 +END=3 +# Directorio que contiene las carpetas con las fotos +MAIN_DIR=$1 + +# Iterar a través de cada carpeta en el directorio principal +for image in "$MAIN_DIR"/*; do + # Verificar si es un directorio + # Iterar a través de cada imagen en la subcarpeta + # Verificar si es un archivo + if [ -f "$image" ]; then + # Obtener el nombre de la imagen + fileName=$(basename "$image") + imageName="${fileName%.*}" + hash=$(echo -n "$imageName" | sha1sum | awk '{print $1}') + first=$(echo "$hash" | cut -c"$START"-"$DIR_LEVELS") + second=$(echo "$hash" | cut -c"$DIR_LEVELS"-"$END") + path=$(dirname "$image")/${first}/${second} + mkdir -p $path + # Crear un enlace simbólico en la carpeta principal + mv $image $path/$fileName + fi + # done + # fi +done diff --git a/storage/image/reestructure.sh b/storage/image/reestructure.sh new file mode 100644 index 0000000000..06786bffd8 --- /dev/null +++ b/storage/image/reestructure.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +for collection in */ ; do + for size in "$collection"*/ ; do + for image in "$size"* ; do + fileName=$(basename "$image") + imageName="${fileName%.*}" + hash=$(echo -n "$imageName" | sha1sum | awk '{print $1}') + mkdir -p $(dirname "$image")/${hash:2:2}/${hash:0:2} + ln -s $image $(dirname "$image")/${hash:2:2}/${hash:0:2}/$fileName + done + done +done diff --git a/storage/image/script.sh b/storage/image/script.sh new file mode 100644 index 0000000000..6c7a3edd0e --- /dev/null +++ b/storage/image/script.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Directorio que contiene las carpetas con las fotos +directorio_principal=$1 + +# Iterar a través de cada carpeta en el directorio principal +for carpeta in "$directorio_principal"/*; do + # Verificar si es un directorio + if [ -d "$carpeta" ]; then + # Iterar a través de cada imagen en la subcarpeta + for imagen in "$carpeta"/*.png; do + # Verificar si es un archivo + if [ -f "$imagen" ]; then + # Obtener el nombre de la imagen + nombre_imagen=$(basename "$imagen") + + # Crear un enlace simbólico en la carpeta principal + ln -s "$imagen" "$1/_$nombre_imagen" + fi + done + fi +done diff --git a/storage/image/user/1600x1600/Pallet.png b/storage/image/user/1600x1600/Pallet.png new file mode 100644 index 0000000000..4eaa22de5d Binary files /dev/null and b/storage/image/user/1600x1600/Pallet.png differ