// cache.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; const mongoose = require('mongoose'); const Attachment = mongoose.model('Attachment'); const { SiteService } = require('../../lib/site-lib'); class AttachmentService extends SiteService { constructor (dtp) { super(dtp, module.exports); } async start ( ) { await super.start(); const { user: userService } = this.dtp.services; this.populateAttachment = [ { path: 'item', }, { path: 'owner', select: userService.USER_SELECT, }, ]; this.queue = this.getJobQueue('media', this.dtp.config.jobQueues.media); // this.template = this.loadViewTemplate('attachment/components/attachment-standalone.pug'); } async create (owner, attachmentDefinition, file) { const { minio: minioService } = this.dtp.services; const NOW = new Date(); /* * Fill in as much of the attachment as we can prior to uploading */ let attachment = new Attachment(); attachment.created = NOW; attachment.ownerType = owner.type; attachment.owner = owner._id; attachment.itemType = attachmentDefinition.itemType; attachment.item = mongoose.Types.ObjectId(attachmentDefinition.item._id || attachmentDefinition.item); attachment.flags.isSensitive = attachmentDefinition.isSensitive === 'on'; /* * Upload the original file to storage */ const attachmentId = attachment._id.toString(); attachment.original.bucket = process.env.MINIO_ATTACHMENT_BUCKET || 'dtp-attachments'; attachment.original.key = this.getAttachmentKey(attachment, 'original'); attachment.original.mime = file.mimetype; attachment.original.size = file.size; const response = await minioService.uploadFile({ bucket: attachment.file.bucket, key: attachment.file.key, filePath: file.path, metadata: { 'X-DTP-Attachment-ID': attachmentId, 'Content-Type': attachment.metadata.mime, 'Content-Length': file.size, }, }); /* * Complete the attachment definition, and save it. */ attachment.original.etag = response.etag; await attachment.save(); attachment = await this.getById(attachment._id); await this.queue.add('attachment-ingest', { attachmentId: attachment._id }); return attachment; } getAttachmentKey (attachment, slug) { const attachmentId = attachment._id.toString(); const prefix = attachmentId.slice(-4); // last 4 for best entropy return `/attachment/${prefix}/${attachmentId}/${attachmentId}-${slug}}`; } /** * Retrieves populated Attachment documents attached to an item. * @param {String} itemType The type of item (ex: 'ChatMessage') * @param {*} itemId The _id of the item (ex: message._id) * @returns Array of attachments associated with the item. */ async getForItem (itemType, itemId) { const attachments = await Attachment .find({ itemType, item: itemId }) .sort({ order: 1, created: 1 }) .populate(this.populateAttachment) .lean(); return attachments; } /** * Retrieves populated Attachment documents created by a specific owner. * @param {User} owner The owner for which Attachments are being fetched. * @param {*} pagination Optional pagination of data set * @returns Array of attachments owned by the specified owner. */ async getForOwner (owner, pagination) { const attachments = await Attachment .find({ ownerType: owner.type, owner: owner._id }) .sort({ order: 1, created: 1 }) .skip(pagination.skip) .limit(pagination.cpp) .populate(this.populateAttachment) .lean(); return attachments; } /** * Access all attachments sorted by most recent with pagination. This is for * use by Admin tools. * @param {*} pagination required pagination parameters (skip and cpp) * @returns Array of attachments */ async getRecent (pagination) { const attachments = await Attachment .find() .sort({ created: -1 }) .skip(pagination.skip) .limit(pagination.cpp) .populate(this.populateAttachment) .lean(); return attachments; } /** * * @param {mongoose.Types.ObjectId} attachmentId The ID of the attachment * @param {Object} options `withOriginal` true|false * @returns A populated Attachment document configured per options. */ async getById (attachmentId, options) { options = Object.assign({ withOriginal: false, }, options || { }); let q = Attachment.findById(attachmentId); if (options.withOriginal) { q = q.select('+original'); } const attachment = await q.populate(this.populateAttachment).lean(); return attachment; } /** * Updates the status of an Attachment. * @param {Attachment} attachment The attachment being modified. * @param {*} status The new status of the attachment */ async setStatus (attachment, status) { await Attachment.updateOne({ _id: attachment._id }, { $set: { status } }); } /** * Passes an attachment and options through a Pug template to generate HTML * output ready to be inserted into a DOM to present the attachment in the UI. * @param {Attachment} attachment * @param {Object} attachmentOptions Additional options passed to the template * @returns HTML output of the template */ async render (attachment, attachmentOptions) { return this.attachmentTemplate({ attachment, attachmentOptions }); } /** * Removes all attachments and everything on storage about them for a * specified User. * @param {User} owner the owner of the attachments to be removed */ async removeForOwner (owner) { const handler = this.remove.bind(this); await Attachment .find({ owner: owner._id }) .lean() .cursor() .eachAsync(handler); } /** * Creates a Bull Queue job to delete an Attachment including it's processed * and original media files. * @param {Attachment} attachment The attachment to be deleted. * @returns Bull Queue job handle for the newly created job to delete the * attachment. */ async remove (attachment) { this.log.info('creating job to delete attachment', { attachmentId: attachment._id }); return await this.queue.add('attachment-delete', { attachmentId: attachment._id }); } } module.exports = { logId: 'attachment', index: 'attachment', className: 'AttachmentService', create: (dtp) => { return new AttachmentService(dtp); }, };