DTP Base provides a scalable and secure Node.js application development harness ready for production service.
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.
 
 
 
 

258 lines
7.5 KiB

// image.js
// Copyright (C) 2024 DTP Technologies, LLC
// All Rights Reserved
'use strict';
import path from 'node:path';
import fs from 'node:fs';
import mongoose from 'mongoose';
const ChatImage = mongoose.model('Image');
import sharp from 'sharp';
import { SiteService, SiteAsync } from '../../lib/site-lib.js';
export default class ImageService extends SiteService {
static get name ( ) { return 'ImageService'; }
static get slug ( ) { return 'image'; }
constructor (dtp) {
super(dtp, ImageService);
}
async start ( ) {
await super.start();
const { user: userService } = this.dtp.services;
this.populateImage = [
{
path: 'owner',
select: userService.USER_SELECT,
},
];
await fs.promises.mkdir(process.env.IMAGE_WORK_PATH, { recursive: true });
}
async create (owner, imageDefinition, file) {
const NOW = new Date();
const { minio: minioService } = this.dtp.services;
this.log.debug('processing uploaded image', { imageDefinition, file });
const sharpImage = await 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 ChatImage();
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();
}
async getImageById (imageId) {
const image = await ChatImage
.findById(imageId)
.populate(this.populateImage);
return image;
}
async getRecentImagesForOwner (owner) {
const images = await ChatImage
.find({ owner: owner._id })
.sort({ created: -1 })
.limit(10)
.populate(this.populateImage)
.lean();
return images;
}
async deleteImage (image) {
const { minio: minioService } = this.dtp.services;
try {
if (image.file && image.file.bucket && image.file.key) {
// this.log.debug('removing image from storage', { file: image.file });
await minioService.removeObject(image.file.bucket, image.file.key);
}
} catch (error) {
this.log.error('failed to remove image from storage', { error });
// fall through
}
await ChatImage.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.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 ChatImage();
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 });
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);
}
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,
};
}
async reportStats ( ) {
const chart = await ChatImage.aggregate([
{
$match: { },
},
{
$group: {
_id: { $dateToString: { format: '%Y-%m', date: '$created' } },
count: { $sum: 1 },
bytes: { $sum: '$size' },
},
},
{
$sort: { _id: 1 },
},
]);
const stats = { };
stats.count = chart.reduce((prev, value) => {
return prev + value.count;
}, 0);
stats.bytes = chart.reduce((prev, value) => {
return prev + value.bytes;
}, 0);
return { chart, stats };
}
}