UNPKG

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
"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();