adonis-responsive-attachment
Version:
Generate and persist optimised and responsive breakpoint images on the fly in your AdonisJS application.
539 lines (538 loc) • 21.8 kB
JavaScript
"use strict";
/*
* adonis-responsive-attachment
*
* (c) Ndianabasi Udonkang <ndianabasi@furnish.ng>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _ResponsiveAttachment_options;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResponsiveAttachment = exports.tempUploadFolder = void 0;
/// <reference path="../../adonis-typings/index.ts" />
const detect_file_type_1 = __importDefault(require("detect-file-type"));
const promises_1 = require("fs/promises");
const lodash_1 = require("lodash");
const image_manipulation_helper_1 = require("../Helpers/image_manipulation_helper");
exports.tempUploadFolder = 'image_upload_tmp';
/**
* Attachment class represents an attachment data type
* for Lucid models
*/
class ResponsiveAttachment {
/**
* Reference to the drive
*/
static getDrive() {
return this.drive;
}
/**
* Set the drive instance
*/
static setDrive(drive) {
this.drive = drive;
}
/**
* Set the logger instance
*/
static setLogger(logger) {
this.logger = logger;
}
/**
* Reference to the logger instance
*/
static getLogger() {
return this.logger;
}
/**
* Create attachment instance from the bodyparser
* file
*/
static async fromFile(file, fileName) {
if (!file) {
throw new SyntaxError('You should provide a non-falsy value');
}
if (!file.tmpPath) {
throw new Error('[Adonis Responsive Attachment] Please provide a valid file');
}
// Get the file buffer
const buffer = await (0, promises_1.readFile)(file.tmpPath);
const isHEIC = (0, image_manipulation_helper_1.detectHEIC)(buffer);
if (isHEIC) {
file.subtype = isHEIC.ext;
file.extname = isHEIC.ext;
file.type = 'image';
}
if (image_manipulation_helper_1.allowedFormats.includes(file?.subtype) === false) {
throw new RangeError(`[Adonis Responsive Attachment] Uploaded file is not an allowable image. Make sure that you uploaded only the following format: "jpeg", "png", "webp", "tiff", "avif", and "heif".`);
}
const computedFileName = fileName ? fileName : file.fieldName;
const attributes = {
extname: file.extname,
mimeType: `${file.type}/${file.subtype}`,
size: file.size,
fileName: computedFileName.replace(/[^\d\w]+/g, '_').toLowerCase(),
};
return new ResponsiveAttachment(attributes, buffer);
}
/**
* Create attachment instance from the bodyparser via a buffer
*/
static fromBuffer(buffer, name) {
return new Promise((resolve, reject) => {
try {
let bufferProperty;
detect_file_type_1.default.fromBuffer(buffer, function (err, result) {
if (err) {
throw new Error(err instanceof Error ? err.message : err);
}
if (!result) {
throw new Error('Please provide a valid file buffer');
}
bufferProperty = result;
});
const isHEIC = (0, image_manipulation_helper_1.detectHEIC)(buffer);
let { mime, ext } = bufferProperty;
if (typeof isHEIC === 'object') {
mime = isHEIC.mime;
ext = isHEIC.ext;
}
const subtype = mime.split('/').pop();
if (image_manipulation_helper_1.allowedFormats.includes(subtype) === false) {
throw new RangeError(`Uploaded file is not an allowable image. Make sure that you uploaded only the following format: "jpeg", "png", "webp", "tiff", "avif", and "heif".`);
}
const attributes = {
extname: ext,
mimeType: mime,
size: buffer.length,
fileName: name?.replace(/[^\d\w]+/g, '_')?.toLowerCase() ?? '',
};
return resolve(new ResponsiveAttachment(attributes, buffer));
}
catch (error) {
return reject(error);
}
});
}
/**
* Create attachment instance from the database response
*/
static fromDbResponse(response) {
let attributes = null;
if (typeof response === 'string') {
try {
attributes = JSON.parse(response);
}
catch (error) {
ResponsiveAttachment.logger.warn('[Adonis Responsive Attachment] Incompatible image data skipped: %s', response);
attributes = null;
}
}
else {
attributes = response;
}
if (!attributes)
return null;
const attachment = new ResponsiveAttachment(attributes);
/**
* Images fetched from DB are always persisted
*/
attachment.isPersisted = true;
return attachment;
}
constructor(attributes, buffer) {
this.buffer = buffer;
/**
* Attachment options
*/
_ResponsiveAttachment_options.set(this, void 0);
/**
* Find if the image has been persisted or not.
*/
this.isPersisted = false;
/**
* Is `true` when the instance is created locally using the
* bodyparser file object or a file buffer.
*/
this.isLocal = !!this.buffer;
this.name = attributes.name;
this.size = attributes.size;
this.width = attributes.width;
this.format = attributes.format;
this.blurhash = attributes.blurhash;
this.height = attributes.height;
this.extname = attributes.extname;
this.mimeType = attributes.mimeType;
this.url = attributes.url ?? undefined;
this.breakpoints = attributes.breakpoints ?? undefined;
this.fileName = attributes.fileName ?? '';
this.isLocal = !!this.buffer;
}
get attributes() {
return {
name: this.name,
size: this.size,
width: this.width,
format: this.format,
height: this.height,
extname: this.extname,
mimeType: this.mimeType,
url: this.url,
breakpoints: this.breakpoints,
buffer: this.buffer,
blurhash: this.blurhash,
};
}
/**
* Returns disk instance
*/
getDisk() {
const disk = __classPrivateFieldGet(this, _ResponsiveAttachment_options, "f")?.disk;
const drive = this.constructor.getDrive();
return disk ? drive.use(disk) : drive.use();
}
get getOptions() {
return __classPrivateFieldGet(this, _ResponsiveAttachment_options, "f") || {};
}
/**
* Returns disk instance
*/
get loggerInstance() {
return this.constructor.getLogger();
}
/**
* Define persistance options
*/
setOptions(options) {
/**
* CRITICAL: Don't set default values here. Only pass along
* just the provided options. The decorator will handle merging
* of this provided options with the decorator options appropriately.
*/
__classPrivateFieldSet(this, _ResponsiveAttachment_options, options || {}, "f");
return this;
}
async enhanceFile(options) {
// Optimise the image buffer and return the optimised buffer
// and the info of the image
const { buffer, info } = await (0, image_manipulation_helper_1.optimize)(this.buffer, options);
// Override the `imageInfo` object with the optimised `info` object
// As the optimised `info` object is preferred
// Also append the `buffer`
return (0, lodash_1.assign)({ ...this.attributes }, info, { buffer });
}
/**
* Save image to the disk. Results in noop when "this.isLocal = false"
*/
async save() {
const options = (0, image_manipulation_helper_1.getMergedOptions)(__classPrivateFieldGet(this, _ResponsiveAttachment_options, "f") || {});
try {
/**
* Do not persist already persisted image or if the
* instance is not local
*/
if (!this.isLocal || this.isPersisted) {
return this;
}
/**
* Optimise the original file and return the enhanced buffer and
* information of the enhanced buffer
*/
const enhancedImageData = await this.enhanceFile(options);
/**
* Generate the name of the original image
*/
this.name = options.keepOriginal
? (0, image_manipulation_helper_1.generateName)({
extname: enhancedImageData.extname,
options: options,
prefix: 'original',
fileName: this.fileName,
})
: undefined;
/**
* Update the local attributes with the attributes
* of the optimised original file
*/
if (options.keepOriginal) {
this.size = enhancedImageData.size;
this.width = enhancedImageData.width;
this.height = enhancedImageData.height;
this.format = enhancedImageData.format;
this.extname = enhancedImageData.extname;
this.mimeType = enhancedImageData.mimeType;
}
/**
* Inject the name into the `ImageInfo`
*/
enhancedImageData.name = this.name;
enhancedImageData.fileName = this.fileName;
/**
* Write the optimised original image to the disk
*/
if (options.keepOriginal) {
await this.getDisk().put(enhancedImageData.name, enhancedImageData.buffer);
}
/**
* Generate image thumbnail data
*/
const thumbnailImageData = await (0, image_manipulation_helper_1.generateThumbnail)(enhancedImageData, options);
if (thumbnailImageData) {
// Set blurhash to top-level image data
this.blurhash = thumbnailImageData.blurhash;
// Set the blurhash to the enhanced image data
enhancedImageData.blurhash = thumbnailImageData.blurhash;
}
const thumbnailIsRequired = options.responsiveDimensions && !options.disableThumbnail;
if (thumbnailImageData && thumbnailIsRequired) {
/**
* Write the thumbnail image to the disk
*/
await this.getDisk().put(thumbnailImageData.name, thumbnailImageData.buffer);
/**
* Delete buffer from `thumbnailImageData`
*/
delete thumbnailImageData.buffer;
(0, lodash_1.set)(enhancedImageData, 'breakpoints.thumbnail', thumbnailImageData);
}
/**
* Generate breakpoint image data
*/
const breakpointFormats = await (0, image_manipulation_helper_1.generateBreakpointImages)(enhancedImageData, options);
if (breakpointFormats && Array.isArray(breakpointFormats) && breakpointFormats.length > 0) {
for (const format of breakpointFormats) {
if (!format)
continue;
const { key, file: breakpointImageData } = format;
/**
* Write the breakpoint image to the disk
*/
await this.getDisk().put(breakpointImageData.name, breakpointImageData.buffer);
/**
* Delete buffer from `breakpointImageData`
*/
delete breakpointImageData.buffer;
(0, lodash_1.set)(enhancedImageData, ['breakpoints', key], breakpointImageData);
}
}
const { width, height } = await (0, image_manipulation_helper_1.getDimensions)(enhancedImageData.buffer);
delete enhancedImageData.buffer;
(0, lodash_1.assign)(enhancedImageData, {
width,
height,
});
/**
* Update the width and height
*/
if (options.keepOriginal) {
this.width = enhancedImageData.width;
this.height = enhancedImageData.height;
}
/**
* Update the local value of `breakpoints`
*/
this.breakpoints = enhancedImageData.breakpoints;
/**
* Images has been persisted
*/
this.isPersisted = true;
/**
* Delete the temporary file
*/
if (this.buffer) {
this.buffer = undefined;
}
/**
* Compute the URL
*/
await this.computeUrls().catch((error) => {
this.loggerInstance.error('Adonis Responsive Attachment error: %o', error);
});
return this;
}
catch (error) {
this.loggerInstance.fatal('Adonis Responsive Attachment error', error);
throw error;
}
}
/**
* Delete original and responsive images from the disk
*/
async delete() {
const options = (0, image_manipulation_helper_1.getMergedOptions)(__classPrivateFieldGet(this, _ResponsiveAttachment_options, "f") || {});
try {
if (!this.isPersisted) {
return;
}
/**
* Delete the original image
*/
if (options.keepOriginal && this.name) {
await this.getDisk().delete(this.name);
}
/**
* Delete the responsive images
*/
if (this.breakpoints) {
for (const key in this.breakpoints) {
if (Object.prototype.hasOwnProperty.call(this.breakpoints, key)) {
const breakpointImage = this.breakpoints[key];
if (breakpointImage.name) {
await this.getDisk().delete(breakpointImage.name);
}
}
}
}
this.isDeleted = true;
this.isPersisted = false;
}
catch (error) {
this.loggerInstance.fatal('[Adonis Responsive Attachment] error', error);
throw error;
}
}
async computeUrls(signedUrlOptions) {
/**
* Cannot compute url for a non persisted image
*/
if (!this.isPersisted) {
return;
}
/**
* Compute urls when preComputeUrls is set to true
* or the `preComputeUrls` function exists
*/
if (!__classPrivateFieldGet(this, _ResponsiveAttachment_options, "f")?.preComputeUrls && this.isLocal) {
return;
}
const disk = this.getDisk();
/**
* Generate url using the user defined preComputeUrls method
*/
if (typeof __classPrivateFieldGet(this, _ResponsiveAttachment_options, "f")?.preComputeUrls === 'function') {
const urls = await __classPrivateFieldGet(this, _ResponsiveAttachment_options, "f").preComputeUrls(disk, this).catch((error) => {
this.loggerInstance.error('Adonis Responsive Attachment error: %o', error);
return null;
});
if (urls) {
this.url = urls.url;
if (!this.urls)
this.urls = {};
if (!this.urls.breakpoints)
this.urls.breakpoints = {};
for (const key in urls.breakpoints) {
if (Object.prototype.hasOwnProperty.call(urls.breakpoints, key)) {
if (!this.urls.breakpoints[key])
this.urls.breakpoints[key] = { url: '' };
this.urls.breakpoints[key].url = urls.breakpoints[key].url;
}
}
return this.urls;
}
}
/**
* Iterative URL-computation logic
*/
const { buffer, ...originalAttributes } = this.attributes;
const attachmentData = originalAttributes;
if (attachmentData) {
if (!this.urls)
this.urls = {};
for (const key in attachmentData) {
if (['name', 'breakpoints'].includes(key) === false) {
continue;
}
const value = attachmentData[key];
let url;
if (key === 'name') {
if ((__classPrivateFieldGet(this, _ResponsiveAttachment_options, "f")?.keepOriginal ?? true) === false || !this.name) {
continue;
}
const name = value;
let imageVisibility;
try {
imageVisibility = await disk.getVisibility(name);
}
catch (error) {
this.loggerInstance.error('Adonis Responsive Attachment error: %s', error);
continue;
}
if (imageVisibility === 'private') {
url = await disk.getSignedUrl(name, signedUrlOptions || undefined);
}
else {
url = await disk.getUrl(name);
}
this.urls['url'] = url;
this.url = url;
}
if (key === 'breakpoints') {
if ((0, lodash_1.isEmpty)(value) === false) {
if (!this.urls.breakpoints) {
this.urls.breakpoints = {};
}
const breakpoints = value;
for (const breakpoint in breakpoints) {
if (Object.prototype.hasOwnProperty.call(breakpoints, breakpoint)) {
const breakpointImageData = breakpoints?.[breakpoint];
if (breakpointImageData) {
const imageVisibility = await disk.getVisibility(breakpointImageData.name);
if (imageVisibility === 'private') {
url = await disk.getSignedUrl(breakpointImageData.name, signedUrlOptions || undefined);
}
else {
url = await disk.getUrl(breakpointImageData.name);
}
this.urls['breakpoints'][breakpoint] = { url };
}
}
}
}
}
}
}
return this.urls;
}
/**
* Returns the signed or unsigned URL for each responsive image
*/
async getUrls(signingOptions) {
return this.computeUrls({ ...signingOptions }).catch((error) => {
this.loggerInstance.error('Adonis Responsive Attachment error: %o', error);
return undefined;
});
}
/**
* Convert attachment instance to object without the `url` property
* for persistence to the database
*/
toObject() {
const { buffer, url, ...originalAttributes } = this.attributes;
return (0, lodash_1.merge)((__classPrivateFieldGet(this, _ResponsiveAttachment_options, "f")?.keepOriginal ?? true) ? originalAttributes : {}, {
breakpoints: this.breakpoints,
});
}
/**
* Serialize attachment instance to JSON object to be sent over the wire
*/
toJSON() {
return (0, lodash_1.merge)(this.toObject(), this.urls ?? {});
}
}
exports.ResponsiveAttachment = ResponsiveAttachment;
_ResponsiveAttachment_options = new WeakMap();