You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
327 lines
9.7 KiB
327 lines
9.7 KiB
// image.js
|
|
// Copyright (C) 2022 DTP Technologies, LLC
|
|
// License: Apache-2.0
|
|
|
|
'use strict';
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const mongoose = require('mongoose');
|
|
const SiteImage = mongoose.model('Image');
|
|
|
|
const sharp = require('sharp');
|
|
|
|
const { SiteService, SiteAsync } = require('../../lib/site-lib');
|
|
|
|
class ImageService extends SiteService {
|
|
|
|
constructor(dtp) {
|
|
super(dtp, module.exports);
|
|
this.populateImage = [
|
|
{
|
|
path: 'owner',
|
|
select: '_id username username_lc displayName picture'
|
|
},
|
|
];
|
|
}
|
|
|
|
async start() {
|
|
await super.start();
|
|
await fs.promises.mkdir(process.env.DTP_IMAGE_WORK_PATH, { recursive: true });
|
|
}
|
|
|
|
async create(owner, imageDefinition, file) {
|
|
const NOW = new Date();
|
|
const { minio: minioService } = this.dtp.services;
|
|
try {
|
|
this.log.debug('processing uploaded image', { imageDefinition, file });
|
|
|
|
const sharpImage = sharp(file.path);
|
|
const metadata = await sharpImage.metadata();
|
|
|
|
// create an Image model instance, but leave it here in application memory.
|
|
// we don't persist it to the db until MinIO accepts the binary data.
|
|
const image = new SiteImage();
|
|
image.created = NOW;
|
|
image.owner = owner._id;
|
|
image.type = file.mimetype;
|
|
image.size = file.size;
|
|
image.file.bucket = process.env.MINIO_IMAGE_BUCKET;
|
|
image.metadata = this.makeImageMetadata(metadata);
|
|
|
|
const imageId = image._id.toString();
|
|
const ownerId = owner._id.toString();
|
|
const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/${imageId.slice(0, 3)}/${imageId}`;
|
|
image.file.key = fileKey;
|
|
|
|
// upload the image file to MinIO
|
|
const response = await minioService.uploadFile({
|
|
bucket: image.file.bucket,
|
|
key: image.file.key,
|
|
filePath: file.path,
|
|
metadata: {
|
|
'Content-Type': file.mimetype,
|
|
'Content-Length': file.size,
|
|
},
|
|
});
|
|
|
|
// store the eTag from MinIO in the Image model
|
|
image.file.etag = response.etag;
|
|
|
|
// save the Image model to the db
|
|
await image.save();
|
|
|
|
this.log.info('processed uploaded image', { ownerId, imageId, fileKey });
|
|
return image.toObject();
|
|
} catch (error) {
|
|
this.log.error('failed to process image', { error });
|
|
throw error;
|
|
} finally {
|
|
this.log.info('removing uploaded image from local file system', { file: file.path });
|
|
await fs.promises.rm(file.path);
|
|
}
|
|
}
|
|
|
|
async getImageById(imageId) {
|
|
const image = await SiteImage
|
|
.findById(imageId)
|
|
.populate(this.populateImage);
|
|
return image;
|
|
}
|
|
|
|
async getRecentImagesForOwner(owner, limit = 10) {
|
|
const images = await SiteImage
|
|
.find({ owner: owner._id })
|
|
.sort({ created: -1 })
|
|
.limit(limit)
|
|
.populate(this.populateImage)
|
|
.lean();
|
|
return images;
|
|
}
|
|
|
|
async getRecentImages (pagination) {
|
|
const images = await SiteImage
|
|
.find()
|
|
.sort({ created: -1 })
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp)
|
|
.populate(this.populateImage)
|
|
.lean();
|
|
const totalImageCount = await SiteImage.estimatedDocumentCount();
|
|
return { images, totalImageCount };
|
|
}
|
|
|
|
async downloadImage (image, filename) {
|
|
const { minio: minioService } = this.dtp.services;
|
|
return minioService.downloadFile({
|
|
bucket: image.file.bucket,
|
|
key: image.file.key,
|
|
filePath: filename,
|
|
});
|
|
}
|
|
|
|
async deleteImage(image) {
|
|
const { minio: minioService } = this.dtp.services;
|
|
|
|
this.log.debug('removing image from storage', { bucket: image.file.bucket, key: image.file.key });
|
|
await minioService.removeObject(image.file.bucket, image.file.key);
|
|
|
|
this.log.debug('removing image from MongoDB', { _id: image._id });
|
|
await SiteImage.deleteOne({ _id: image._id });
|
|
}
|
|
|
|
async processImageFile(owner, file, outputs, options) {
|
|
this.log.debug('processing image file', { owner, file, outputs });
|
|
const sharpImage = sharp(file.path);
|
|
return this.processImage(owner, sharpImage, outputs, options);
|
|
}
|
|
|
|
async processImage(owner, sharpImage, outputs, options) {
|
|
const NOW = new Date();
|
|
const service = this;
|
|
const { minio: minioService } = this.dtp.services;
|
|
|
|
options = Object.assign({
|
|
removeWorkFiles: true,
|
|
}, options);
|
|
|
|
const imageWorkPath = process.env.DTP_IMAGE_WORK_PATH || '/tmp';
|
|
const metadata = await sharpImage.metadata();
|
|
|
|
async function processOutputImage(output) {
|
|
const outputMetadata = service.makeImageMetadata(metadata);
|
|
outputMetadata.width = output.width;
|
|
outputMetadata.height = output.height;
|
|
|
|
service.log.debug('processing image', { output, outputMetadata });
|
|
|
|
const image = new SiteImage();
|
|
image.created = NOW;
|
|
image.owner = owner._id;
|
|
image.type = `image/${output.format}`;
|
|
image.metadata = outputMetadata;
|
|
|
|
try {
|
|
let chain = sharpImage
|
|
.clone()
|
|
.resize({
|
|
width: output.width,
|
|
height: output.height,
|
|
options: output.resizeOptions,
|
|
})
|
|
;
|
|
chain = chain[output.format](output.formatParameters);
|
|
|
|
output.filePath = path.join(imageWorkPath, `${image._id}.${output.width}x${output.height}.${output.format}`);
|
|
output.mimetype = `image/${output.format}`;
|
|
await chain.toFile(output.filePath);
|
|
output.stat = await fs.promises.stat(output.filePath);
|
|
} catch (error) {
|
|
service.log.error('failed to process output image', { output, error });
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
const imageId = image._id.toString();
|
|
const ownerId = owner._id.toString();
|
|
const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/images/${imageId.slice(0, 3)}/${imageId}.${output.format}`;
|
|
|
|
image.file.bucket = process.env.MINIO_IMAGE_BUCKET;
|
|
image.file.key = fileKey;
|
|
image.size = output.stat.size;
|
|
|
|
// upload the image file to MinIO
|
|
const response = await minioService.uploadFile({
|
|
bucket: image.file.bucket,
|
|
key: image.file.key,
|
|
filePath: output.filePath,
|
|
metadata: {
|
|
'Content-Type': output.mimetype,
|
|
'Content-Length': output.stat.size,
|
|
},
|
|
});
|
|
|
|
// store the eTag from MinIO in the Image model
|
|
image.file.etag = response.etag;
|
|
|
|
// save the Image model to the db
|
|
await image.save();
|
|
|
|
service.log.info('processed uploaded image', { ownerId, imageId, fileKey });
|
|
|
|
if (options.removeWorkFiles) {
|
|
service.log.debug('removing work file', { path: output.filePath });
|
|
await fs.promises.unlink(output.filePath);
|
|
delete output.filePath;
|
|
}
|
|
|
|
output.image = {
|
|
_id: image._id,
|
|
bucket: image.file.bucket,
|
|
key: image.file.key,
|
|
};
|
|
} catch (error) {
|
|
service.log.error('failed to persist output image', { output, error });
|
|
if (options.removeWorkFiles) {
|
|
service.log.debug('removing work file', { path: output.filePath });
|
|
await SiteAsync.each(outputs, async (output) => {
|
|
await fs.promises.unlink(output.filePath);
|
|
delete output.filePath;
|
|
}, 4);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
await SiteAsync.each(outputs, processOutputImage, 4);
|
|
}
|
|
|
|
async getSiteIconInfo() {
|
|
const siteDomain = this.dtp.config.site.domainKey;
|
|
|
|
const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
|
|
const siteIconDir = path.join(siteImagesDir, 'icon', siteDomain);
|
|
|
|
let icon;
|
|
|
|
try {
|
|
|
|
await fs.promises.access(siteIconDir);
|
|
const iconMetadata = await sharp(path.join(siteIconDir, 'icon-512x512.png')).metadata();
|
|
icon = {
|
|
metadata: iconMetadata,
|
|
path: `/img/icon/${siteDomain}/icon-512x512.png`,
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
icon = null;
|
|
}
|
|
|
|
return icon;
|
|
}
|
|
|
|
async updateSiteIcon(imageDefinition, file) {
|
|
|
|
this.log.debug('updating site icon', { imageDefinition, file });
|
|
try {
|
|
|
|
|
|
const siteDomain = this.dtp.config.site.domainKey;
|
|
|
|
const siteImagesDir = path.join(this.dtp.config.root, 'client', 'img');
|
|
|
|
const siteIconDir = path.join(siteImagesDir, 'icon', siteDomain);
|
|
|
|
const sourceIconFilePath = file.path;
|
|
|
|
const sizes = [16, 32, 36, 48, 57, 60, 70, 72, 76, 96, 114, 120, 144, 150, 152, 180, 192, 256, 310, 384, 512];
|
|
|
|
await fs.promises.mkdir(siteIconDir, { force: true, recursive: true });
|
|
|
|
for (var size of sizes) {
|
|
await sharp(sourceIconFilePath).resize({
|
|
fit: sharp.fit.contain,
|
|
width: size,
|
|
height: size,
|
|
}).png()
|
|
.toFile(path.join(siteIconDir, `icon-${size}x${size}.png`));
|
|
}
|
|
|
|
await fs.promises.cp(sourceIconFilePath, path.join(siteIconDir, `${siteDomain}.png`));
|
|
|
|
await fs.promises.cp(sourceIconFilePath, path.join(siteImagesDir, 'social-cards', `${siteDomain}.png`));
|
|
return path.join(siteIconDir, 'icon-512x512.png');
|
|
} catch (error) {
|
|
this.log.error('failed to update site icon', { error });
|
|
throw error;
|
|
} finally {
|
|
this.log.info('removing uploaded image from local file system', { file: file.path });
|
|
await fs.promises.rm(file.path);
|
|
}
|
|
|
|
}
|
|
|
|
makeImageMetadata(metadata) {
|
|
return {
|
|
format: metadata.format,
|
|
size: metadata.size,
|
|
width: metadata.width,
|
|
height: metadata.height,
|
|
space: metadata.space,
|
|
channels: metadata.channels,
|
|
depth: metadata.depth,
|
|
density: metadata.density,
|
|
hasAlpha: metadata.hasAlpha,
|
|
orientation: metadata.orientation,
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
slug: 'image',
|
|
name: 'image',
|
|
className: 'ImageService',
|
|
create: (dtp) => { return new ImageService(dtp); },
|
|
};
|