'use strict'; const async = require('async'); const nconf = require('nconf'); const crypto = require('crypto'); const path = require('path'); const winston = require('winston'); const mime = require('mime'); const validator = require('validator'); const db = require('../database'); const image = require('../image'); const topics = require('../topics'); const file = require('../file'); module.exports = function (Posts) { Posts.uploads = {}; const md5 = filename => crypto.createHash('md5').update(filename).digest('hex'); const pathPrefix = path.join(nconf.get('upload_path'), 'files'); const searchRegex = /\/assets\/uploads\/files\/([^\s")]+\.?[\w]*)/g; Posts.uploads.sync = async function (pid) { // Scans a post's content and updates sorted set of uploads const [content, currentUploads, isMainPost] = await Promise.all([ Posts.getPostField(pid, 'content'), Posts.uploads.list(pid), Posts.isMain(pid), ]); // Extract upload file paths from post content let match = searchRegex.exec(content); const uploads = []; while (match) { uploads.push(match[1].replace('-resized', '')); match = searchRegex.exec(content); } // Main posts can contain topic thumbs, which are also tracked by pid if (isMainPost) { const tid = await Posts.getPostField(pid, 'tid'); let thumbs = await topics.thumbs.get(tid); thumbs = thumbs.map(thumb => thumb.url.replace(path.join(nconf.get('upload_url'), 'files/'), '')).filter(path => !validator.isURL(path, { require_protocol: true, })); uploads.push(...thumbs); } // Create add/remove sets const add = uploads.filter(path => !currentUploads.includes(path)); const remove = currentUploads.filter(path => !uploads.includes(path)); await Promise.all([ Posts.uploads.associate(pid, add), Posts.uploads.dissociate(pid, remove), ]); }; Posts.uploads.list = async function (pid) { return await db.getSortedSetMembers('post:' + pid + ':uploads'); }; Posts.uploads.listWithSizes = async function (pid) { const paths = await Posts.uploads.list(pid); const sizes = await db.getObjects(paths.map(path => 'upload:' + md5(path))) || []; return sizes.map((sizeObj, idx) => ({ ...sizeObj, name: paths[idx], })); }; Posts.uploads.isOrphan = async function (filePath) { const length = await db.sortedSetCard('upload:' + md5(filePath) + ':pids'); return length === 0; }; Posts.uploads.getUsage = async function (filePaths) { // Given an array of file names, determines which pids they are used in if (!Array.isArray(filePaths)) { filePaths = [filePaths]; } const keys = filePaths.map(fileObj => 'upload:' + md5(fileObj.name.replace('-resized', '')) + ':pids'); return await Promise.all(keys.map(k => db.getSortedSetRange(k, 0, -1))); }; Posts.uploads.associate = async function (pid, filePaths) { // Adds an upload to a post's sorted set of uploads filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths; if (!filePaths.length) { return; } // Only process files that exist filePaths = await async.filter(filePaths, async filePath => await file.exists(path.join(pathPrefix, filePath))); const now = Date.now(); const scores = filePaths.map(() => now); const bulkAdd = filePaths.map(path => ['upload:' + md5(path) + ':pids', now, pid]); await Promise.all([ db.sortedSetAdd('post:' + pid + ':uploads', scores, filePaths), db.sortedSetAddBulk(bulkAdd), Posts.uploads.saveSize(filePaths), ]); }; Posts.uploads.dissociate = async function (pid, filePaths) { // Removes an upload from a post's sorted set of uploads filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths; if (!filePaths.length) { return; } const bulkRemove = filePaths.map(path => ['upload:' + md5(path) + ':pids', pid]); await Promise.all([ db.sortedSetRemove('post:' + pid + ':uploads', filePaths), db.sortedSetRemoveBulk(bulkRemove), ]); }; Posts.uploads.dissociateAll = async (pid) => { const current = await Posts.uploads.list(pid); await Promise.all(current.map(async path => await Posts.uploads.dissociate(pid, path))); }; Posts.uploads.saveSize = async (filePaths) => { filePaths = filePaths.filter((fileName) => { const type = mime.getType(fileName); return type && type.match(/image./); }); await Promise.all(filePaths.map(async function (fileName) { try { const size = await image.size(path.join(pathPrefix, fileName)); winston.verbose('[posts/uploads/' + fileName + '] Saving size'); await db.setObject('upload:' + md5(fileName), { width: size.width, height: size.height, }); } catch (err) { winston.error('[posts/uploads] Error while saving post upload sizes (' + fileName + '): ' + err.message); } })); }; };