// Use crypto.randomUUID() to create unique IDs, see: // https://nodejs.org/api/crypto.html#cryptorandomuuidoptions const { randomUUID } = require('crypto'); // Use https://www.npmjs.com/package/content-type to create/parse Content-Type headers const contentType = require('content-type'); const logger = require('../logger'); const MarkdownIt = require('markdown-it'); const sharp = require('sharp'); // Functions for working with fragment metadata/data using our DB const { readFragment, writeFragment, readFragmentData, writeFragmentData, listFragments, deleteFragment, } = require('./data'); class Fragment { constructor({ id, ownerId, created, updated, type, size = 0 }) { if(!ownerId){ throw new Error(`OwnerId of Fragment i required`); } if(!type){ throw new Error(`Type of Fragment i required`); } this.id = id || randomUUID(); this.ownerId = ownerId; if (typeof size !== 'number' || size < 0) throw new Error('size must be a positive number'); this.size = size; if (!Fragment.isSupportedType(type)) throw new Error(`${type} is an invalid type`); this.type = type; this.created = created || new Date().toString(); this.updated = updated || new Date().toString(); } /** * Get all fragments (id or full) for the given user * @param {string} ownerId user's hashed email * @param {boolean} expand whether to expand ids to full fragments * @returns Promise<Array<Fragment>> */ static async byUser(ownerId, expand = false) { const result = await listFragments(ownerId, expand); return result; } /** * Gets a fragment for the user by the given id. * @param {string} ownerId user's hashed email * @param {string} id fragment's id * @returns Promise<Fragment> */ static async byId(ownerId, id) { const fragment = await readFragment(ownerId, id); if (!fragment) { throw new Error(`Fragment ${id} not found!`); } return fragment; } /** * Delete the user's fragment data and metadata for the given id * @param {string} ownerId user's hashed email * @param {string} id fragment's id * @returns Promise<void> */ static delete(ownerId, id) { return deleteFragment(ownerId, id); } /** * Saves the current fragment to the database * @returns Promise<void> */ save() { this.updated = new Date().toString(); return writeFragment(this); } /** * Gets the fragment's data from the database * @returns Promise<Buffer> */ getData() { return readFragmentData(this.ownerId, this.id); } /** * Set's the fragment's data in the database * @param {Buffer} data * @returns Promise<void> */ async setData(data) { if (!data) throw new Error('Data is empty'); if (!Buffer.isBuffer(data)) throw new Error('Data is not buffer'); this.size = Buffer.byteLength(data); await this.save(); return await writeFragmentData(this.ownerId, this.id, data); } /** * Returns the mime type (e.g., without encoding) for the fragment's type: * "text/html; charset=utf-8" -> "text/html" * @returns {string} fragment's mime type (without encoding) */ get mimeType() { const { type } = contentType.parse(this.type); return type; } /** * Returns true if this fragment is a text/* mime type * @returns {boolean} true if fragment's type is text/* */ get isText() { const { type } = contentType.parse(this.type); return type == 'text/plain' ? true : false; } /** * Returns the formats into which this fragment type can be converted * @returns {Array<string>} list of supported mime types */ get formats() { return [contentType.format({ type: 'text/plain' })]; } /** * Check for the extension to see if it can be converted to a image type * @returns bool */ checkImageExtension(ext2) { const imageTypes = ['.png', '.jpeg', '.webp', '.gif']; if (imageTypes.find((element) => element == ext2)) return true; return false; } /** * Check for the url and convert the result * @returns object */ async convertData(data, ext2, inputType) { let convertedFragmentData = data; logger.debug({ ext2 }, 'extension'); if (this.mimeType !== inputType) { const md = new MarkdownIt(); try { switch (inputType) { case 'text/html': if (this.mimeType === 'text/markdown') { const dataData_h1Content = `# ${data.toString()}`; convertedFragmentData = md.render(dataData_h1Content); convertedFragmentData = Buffer.from(convertedFragmentData); } break; case 'image/jpeg': convertedFragmentData = await sharp(data).jpeg().toBuffer(); break; case 'image/png': convertedFragmentData = await sharp(data).png().toBuffer(); break; case 'image/webp': convertedFragmentData = await sharp(data).webp().toBuffer(); break; case 'image/gif': convertedFragmentData = await sharp(data).gif().toBuffer(); break; default: break; } } catch (err) { throw 'Error parsing image'; } } //logger.debug({ convertedFragmentData }, 'After ToString'); return convertedFragmentData; } /** * Returns true if we know how to work with this content type * @param {string} value a Content-Type value (e.g., 'text/plain' or 'text/plain: charset=utf-8') * @returns {boolean} true if we support this Content-Type (i.e., type/subtype) */ static isSupportedType(value) { const validTypes = [ 'text/plain', 'text/plain; charset=utf-8', 'text/markdown', 'text/html', 'application/json', 'image/png', 'image/jpeg', 'image/webp', 'image/gif' ]; const result = validTypes.includes(value); return result; } } module.exports.Fragment = Fragment;