diff --git a/install/package.json b/install/package.json index 8bfd634780..5ffca3c819 100644 --- a/install/package.json +++ b/install/package.json @@ -97,7 +97,7 @@ "multer": "2.0.1", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", - "nodebb-plugin-composer-default": "10.2.51", + "nodebb-plugin-composer-default": "10.3.0", "nodebb-plugin-dbsearch": "6.3.0", "nodebb-plugin-emoji": "6.0.3", "nodebb-plugin-emoji-android": "4.1.1", diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index bcb2f79e53..1904cded51 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -300,6 +300,8 @@ PostDataObject: type: boolean attachments: type: array + uploads: + type: array replies: type: object properties: diff --git a/public/openapi/write/topics/tid/thumbs.yaml b/public/openapi/write/topics/tid/thumbs.yaml index 3817d2a5a3..5d28264266 100644 --- a/public/openapi/write/topics/tid/thumbs.yaml +++ b/public/openapi/write/topics/tid/thumbs.yaml @@ -83,55 +83,6 @@ post: type: string name: type: string -put: - tags: - - topics - summary: migrate topic thumbnail - description: This operation migrates a thumbnails from a topic or draft, to another tid or draft. - parameters: - - in: path - name: tid - schema: - type: string - required: true - description: a valid topic id or draft uuid - example: 1 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - tid: - type: string - description: a valid topic id or draft uuid - example: '1' - responses: - '200': - description: Topic thumbnails migrated - content: - application/json: - schema: - type: object - properties: - status: - $ref: ../../../components/schemas/Status.yaml#/Status - response: - type: array - description: A list of the topic thumbnails in the destination topic - items: - type: object - properties: - id: - type: string - name: - type: string - path: - type: string - url: - type: string - description: Path to a topic thumbnail delete: tags: - topics diff --git a/public/openapi/write/topics/tid/thumbs/order.yaml b/public/openapi/write/topics/tid/thumbs/order.yaml index a0f1602bc8..a8acefbf0a 100644 --- a/public/openapi/write/topics/tid/thumbs/order.yaml +++ b/public/openapi/write/topics/tid/thumbs/order.yaml @@ -2,14 +2,14 @@ put: tags: - topics summary: reorder topic thumbnail - description: This operation sets the order for a topic thumbnail. It can handle either topics (if a valid `tid` is passed in), or drafts. A 404 is returned if the topic or draft does not actually contain that thumbnail path. Paths passed in should **not** contain the path to the uploads folder (`config.upload_url` on client side) + description: This operation sets the order for a topic thumbnail. A 404 is returned if the topic does not contain path. Paths passed in should **not** contain the path to the uploads folder (`config.upload_url` on client side) parameters: - in: path name: tid schema: type: string required: true - description: a valid topic id or draft uuid + description: a valid topic id example: 2 requestBody: required: true diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index 4a8a36a768..c52f8beefe 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -176,6 +176,12 @@ define('forum/topic/events', [ }); } + if (data.topic.thumbsupdated) { + require(['topicThumbs'], function (topicThumbs) { + topicThumbs.updateTopicThumbs(data.topic.tid); + }); + } + postTools.removeMenu(components.get('post', 'pid', data.post.pid)); } diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index 183c1bbb70..896d0b485b 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -34,6 +34,7 @@ module.exports = function (utils, Benchpress, relative_path) { humanReadableNumber, formattedNumber, txEscape, + uploadBasename, generatePlaceholderWave, register, __escape: identity, @@ -379,6 +380,12 @@ module.exports = function (utils, Benchpress, relative_path) { return String(text).replace(/%/g, '%').replace(/,/g, ','); } + function uploadBasename(str, sep = '/') { + const hasTimestampPrefix = /^\d+-/; + const name = str.substr(str.lastIndexOf(sep) + 1); + return hasTimestampPrefix.test(name) ? name.slice(14) : name; + } + function generatePlaceholderWave(items) { const html = items.map((i) => { if (i === 'divider') { diff --git a/public/src/modules/topicThumbs.js b/public/src/modules/topicThumbs.js index 70c13218d3..340d856ded 100644 --- a/public/src/modules/topicThumbs.js +++ b/public/src/modules/topicThumbs.js @@ -7,23 +7,27 @@ define('topicThumbs', [ Thumbs.get = id => api.get(`/topics/${id}/thumbs`, { thumbsOnly: 1 }); - Thumbs.getByPid = pid => api.get(`/posts/${encodeURIComponent(pid)}`, {}).then(post => Thumbs.get(post.tid)); - Thumbs.delete = (id, path) => api.del(`/topics/${id}/thumbs`, { path: path, }); + Thumbs.updateTopicThumbs = async (tid) => { + const thumbs = await Thumbs.get(tid); + const html = await app.parseAndTranslate('partials/topic/thumbs', { thumbs }); + $('[component="topic/thumb/list"]').html(html); + }; + Thumbs.deleteAll = (id) => { Thumbs.get(id).then((thumbs) => { Promise.all(thumbs.map(thumb => Thumbs.delete(id, thumb.url))); }); }; - Thumbs.upload = id => new Promise((resolve) => { + Thumbs.upload = () => new Promise((resolve) => { uploader.show({ title: '[[topic:composer.thumb-title]]', method: 'put', - route: config.relative_path + `/api/v3/topics/${id}/thumbs`, + route: config.relative_path + `/api/topic/thumb/upload`, }, function (url) { resolve(url); }); @@ -32,24 +36,16 @@ define('topicThumbs', [ Thumbs.modal = {}; Thumbs.modal.open = function (payload) { - const { id, pid } = payload; + const { id, postData } = payload; let { modal } = payload; - let numThumbs; + const thumbs = postData.thumbs || []; return new Promise((resolve) => { - Promise.all([ - Thumbs.get(id), - pid ? Thumbs.getByPid(pid) : [], - ]).then(results => new Promise((resolve) => { - const thumbs = results.reduce((memo, cur) => memo.concat(cur)); - numThumbs = thumbs.length; - - resolve(thumbs); - })).then(thumbs => Benchpress.render('modals/topic-thumbs', { thumbs })).then((html) => { + Benchpress.render('modals/topic-thumbs', { thumbs }).then((html) => { if (modal) { translator.translate(html, function (translated) { modal.find('.bootbox-body').html(translated); - Thumbs.modal.handleSort({ modal, numThumbs }); + Thumbs.modal.handleSort({ modal, thumbs }); }); } else { modal = bootbox.dialog({ @@ -62,7 +58,11 @@ define('topicThumbs', [ label: ' [[modules:thumbs.modal.add]]', className: 'btn-success', callback: () => { - Thumbs.upload(id).then(() => { + Thumbs.upload().then((thumbUrl) => { + postData.thumbs.push( + thumbUrl.replace(new RegExp(`^${config.upload_url}`), '') + ); + Thumbs.modal.open({ ...payload, modal }); require(['composer'], (composer) => { composer.updateThumbCount(id, $(`[component="composer"][data-uuid="${id}"]`)); @@ -79,7 +79,7 @@ define('topicThumbs', [ }, }); Thumbs.modal.handleDelete({ ...payload, modal }); - Thumbs.modal.handleSort({ modal, numThumbs }); + Thumbs.modal.handleSort({ modal, thumbs }); } }); }); @@ -94,42 +94,42 @@ define('topicThumbs', [ if (!ok) { return; } - - const id = ev.target.closest('[data-id]').getAttribute('data-id'); const path = ev.target.closest('[data-path]').getAttribute('data-path'); - api.del(`/topics/${id}/thumbs`, { - path: path, - }).then(() => { + const postData = payload.postData; + if (postData && postData.thumbs && postData.thumbs.includes(path)) { + postData.thumbs = postData.thumbs.filter(thumb => thumb !== path); Thumbs.modal.open(payload); require(['composer'], (composer) => { composer.updateThumbCount(uuid, $(`[component="composer"][data-uuid="${uuid}"]`)); }); - }).catch(alerts.error); + } }); } }); }; - Thumbs.modal.handleSort = ({ modal, numThumbs }) => { - if (numThumbs > 1) { + Thumbs.modal.handleSort = ({ modal, thumbs }) => { + if (thumbs.length > 1) { const selectorEl = modal.find('.topic-thumbs-modal'); selectorEl.sortable({ - items: '[data-id]', + items: '[data-path]', + }); + selectorEl.on('sortupdate', function () { + if (!thumbs) return; + const newOrder = []; + selectorEl.find('[data-path]').each(function () { + const path = $(this).attr('data-path'); + const thumb = thumbs.find(t => t === path); + if (thumb) { + newOrder.push(thumb); + } + }); + // Mutate thumbs array in place + thumbs.length = 0; + Array.prototype.push.apply(thumbs, newOrder); }); - selectorEl.on('sortupdate', Thumbs.modal.handleSortChange); } }; - Thumbs.modal.handleSortChange = (ev, ui) => { - const items = ui.item.get(0).parentNode.querySelectorAll('[data-id]'); - Array.from(items).forEach((el, order) => { - const id = el.getAttribute('data-id'); - let path = el.getAttribute('data-path'); - path = path.replace(new RegExp(`^${config.upload_url}`), ''); - - api.put(`/topics/${id}/thumbs/order`, { path, order }).catch(alerts.error); - }); - }; - return Thumbs; }); diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index e5a8e8e363..f9995b3b72 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -715,8 +715,12 @@ Mocks.notes.public = async (post) => { // Special handling for main posts (as:Article w/ as:Note preview) const noteAttachment = isMainPost ? [...attachment] : null; - const uploads = await posts.uploads.listWithSizes(post.pid); - const isThumb = await db.isSortedSetMembers(`topic:${post.tid}:thumbs`, uploads.map(u => u.name)); + const [uploads, thumbs] = await Promise.all([ + posts.uploads.listWithSizes(post.pid), + topics.getTopicField(post.tid, 'thumbs'), + ]); + const isThumb = uploads.map(u => Array.isArray(thumbs) ? thumbs.includes(u.name) : false); + uploads.forEach(({ name, width, height }, idx) => { const mediaType = mime.getType(name); const url = `${nconf.get('url') + nconf.get('upload_url')}/${name}`; diff --git a/src/api/posts.js b/src/api/posts.js index b39c173eb6..d8971796b3 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -120,9 +120,7 @@ postsAPI.edit = async function (caller, data) { data.timestamp = parseInt(data.timestamp, 10) || Date.now(); const editResult = await posts.edit(data); - if (editResult.topic.isMainPost) { - await topics.thumbs.migrate(data.uuid, editResult.topic.tid); - } + const selfPost = parseInt(caller.uid, 10) === parseInt(editResult.post.uid, 10); if (!selfPost && editResult.post.changed) { await events.log({ diff --git a/src/api/topics.js b/src/api/topics.js index 0155429ecc..38e3cefbf8 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -1,7 +1,5 @@ 'use strict'; -const validator = require('validator'); - const user = require('../user'); const topics = require('../topics'); const categories = require('../categories'); @@ -23,17 +21,13 @@ const socketHelpers = require('../socket.io/helpers'); const topicsAPI = module.exports; topicsAPI._checkThumbPrivileges = async function ({ tid, uid }) { - // req.params.tid could be either a tid (pushing a new thumb to an existing topic) - // or a post UUID (a new topic being composed) - const isUUID = validator.isUUID(tid); - // Sanity-check the tid if it's strictly not a uuid - if (!isUUID && (isNaN(parseInt(tid, 10)) || !await topics.exists(tid))) { + if ((isNaN(parseInt(tid, 10)) || !await topics.exists(tid))) { throw new Error('[[error:no-topic]]'); } // While drafts are not protected, tids are - if (!isUUID && !await privileges.topics.canEdit(tid, uid)) { + if (!await privileges.topics.canEdit(tid, uid)) { throw new Error('[[error:no-privileges]]'); } }; @@ -80,7 +74,6 @@ topicsAPI.create = async function (caller, data) { } const result = await topics.post(payload); - await topics.thumbs.migrate(data.uuid, result.topicData.tid); socketHelpers.emitToUids('event:new_post', { posts: [result.postData] }, [caller.uid]); socketHelpers.emitToUids('event:new_topic', result.topicData, [caller.uid]); @@ -233,17 +226,6 @@ topicsAPI.getThumbs = async (caller, { tid, thumbsOnly }) => { return await topics.thumbs.get(tid, { thumbsOnly }); }; -// topicsAPI.addThumb - -topicsAPI.migrateThumbs = async (caller, { from, to }) => { - await Promise.all([ - topicsAPI._checkThumbPrivileges({ tid: from, uid: caller.uid }), - topicsAPI._checkThumbPrivileges({ tid: to, uid: caller.uid }), - ]); - - await topics.thumbs.migrate(from, to); -}; - topicsAPI.deleteThumb = async (caller, { tid, path }) => { await topicsAPI._checkThumbPrivileges({ tid: tid, uid: caller.uid }); await topics.thumbs.delete(tid, path); diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index b46002bb65..06aded5913 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -138,25 +138,18 @@ Topics.addThumb = async (req, res) => { const files = await uploadsController.uploadThumb(req, res); // response is handled here - // Add uploaded files to topic zset + // Add uploaded files to topic hash if (files && files.length) { - await Promise.all(files.map(async (fileObj) => { + for (const fileObj of files) { + // eslint-disable-next-line no-await-in-loop await topics.thumbs.associate({ id: req.params.tid, path: fileObj.url, }); - })); + } } }; -Topics.migrateThumbs = async (req, res) => { - await api.topics.migrateThumbs(req, { - from: req.params.tid, - to: req.body.tid, - }); - - helpers.formatApiResponse(200, res, await api.topics.getThumbs(req, { tid: req.body.tid })); -}; Topics.deleteThumb = async (req, res) => { if (!req.body.path.startsWith('http')) { diff --git a/src/posts/data.js b/src/posts/data.js index d74a22e69d..28e6c24aaa 100644 --- a/src/posts/data.js +++ b/src/posts/data.js @@ -70,5 +70,13 @@ function modifyPost(post, fields) { if (!fields.length || fields.includes('attachments')) { post.attachments = (post.attachments || '').split(',').filter(Boolean); } + + if (!fields.length || fields.includes('uploads')) { + try { + post.uploads = post.uploads ? JSON.parse(post.uploads) : []; + } catch (err) { + post.uploads = []; + } + } } } diff --git a/src/posts/edit.js b/src/posts/edit.js index 077616b29e..b18bf99078 100644 --- a/src/posts/edit.js +++ b/src/posts/edit.js @@ -29,7 +29,7 @@ module.exports = function (Posts) { } const topicData = await topics.getTopicFields(postData.tid, [ - 'cid', 'mainPid', 'title', 'timestamp', 'scheduled', 'slug', 'tags', + 'cid', 'mainPid', 'title', 'timestamp', 'scheduled', 'slug', 'tags', 'thumbs', ]); await scheduledTopicCheck(data, topicData); @@ -142,6 +142,15 @@ module.exports = function (Posts) { await topics.validateTags(data.tags, topicData.cid, data.uid, tid); } + const thumbs = topics.thumbs.filterThumbs(data.thumbs); + const thumbsupdated = Array.isArray(data.thumbs) && + !_.isEqual(data.thumbs, topicData.thumbs); + + if (thumbsupdated) { + newTopicData.thumbs = JSON.stringify(thumbs); + newTopicData.numThumbs = thumbs.length; + } + const results = await plugins.hooks.fire('filter:topic.edit', { req: data.req, topic: newTopicData, @@ -172,6 +181,7 @@ module.exports = function (Posts) { renamed: renamed, tagsupdated: tagsupdated, tags: tags, + thumbsupdated: thumbsupdated, oldTags: topicData.tags, rescheduled: rescheduling(data, topicData), }; diff --git a/src/posts/uploads.js b/src/posts/uploads.js index 17e82250ba..372c30ca1e 100644 --- a/src/posts/uploads.js +++ b/src/posts/uploads.js @@ -46,12 +46,14 @@ module.exports = function (Posts) { 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), + const [postData, isMainPost] = await Promise.all([ + Posts.getPostFields(pid, ['content', 'uploads']), Posts.isMain(pid), ]); + const content = postData.content || ''; + const currentUploads = postData.uploads || []; + // Extract upload file paths from post content let match = searchRegex.exec(content); let uploads = new Set(); @@ -75,14 +77,19 @@ module.exports = function (Posts) { // 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), - ]); + await Posts.uploads.associate(pid, add); + await Posts.uploads.dissociate(pid, remove); }; - Posts.uploads.list = async function (pid) { - return await db.getSortedSetMembers(`post:${pid}:uploads`); + Posts.uploads.list = async function (pids) { + const isArray = Array.isArray(pids); + if (isArray) { + const uploads = await Posts.getPostsFields(pids, ['uploads']); + return uploads.map(p => p.uploads || []); + } + + const uploads = await Posts.getPostField(pids, 'uploads'); + return uploads; }; Posts.uploads.listWithSizes = async function (pid) { @@ -157,33 +164,38 @@ module.exports = function (Posts) { }; 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; } filePaths = await _filterValidPaths(filePaths); // Only process files that exist and are within uploads directory + const currentUploads = await Posts.uploads.list(pid); + filePaths.forEach((path) => { + if (!currentUploads.includes(path)) { + currentUploads.push(path); + } + }); const now = Date.now(); - const scores = filePaths.map((p, i) => now + i); const bulkAdd = filePaths.map(path => [`upload:${md5(path)}:pids`, now, pid]); + await Promise.all([ - db.sortedSetAdd(`post:${pid}:uploads`, scores, filePaths), + db.setObjectField(`post:${pid}`, 'uploads', JSON.stringify(currentUploads)), 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; } - + let currentUploads = await Posts.uploads.list(pid); + currentUploads = currentUploads.filter(upload => !filePaths.includes(upload)); const bulkRemove = filePaths.map(path => [`upload:${md5(path)}:pids`, pid]); const promises = [ - db.sortedSetRemove(`post:${pid}:uploads`, filePaths), + db.setObjectField(`post:${pid}`, 'uploads', JSON.stringify(currentUploads)), db.sortedSetRemoveBulk(bulkRemove), ]; diff --git a/src/routes/api.js b/src/routes/api.js index e374e242a4..4424d9a979 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -36,6 +36,7 @@ module.exports = function (app, middleware, controllers) { ]; router.post('/post/upload', postMiddlewares, helpers.tryRoute(uploadsController.uploadPost)); + router.post('/topic/thumb/upload', postMiddlewares, helpers.tryRoute(uploadsController.uploadThumb)); router.post('/user/:userslug/uploadpicture', [ ...middlewares, ...postMiddlewares, diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index eb56cdaf42..2b159ee3c0 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -41,7 +41,7 @@ module.exports = function () { ...middlewares, ], controllers.write.topics.addThumb); - setupApiRoute(router, 'put', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['tid'])], controllers.write.topics.migrateThumbs); + setupApiRoute(router, 'delete', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['path'])], controllers.write.topics.deleteThumb); setupApiRoute(router, 'put', '/:tid/thumbs/order', [...middlewares, middleware.checkRequired.bind(null, ['path', 'order'])], controllers.write.topics.reorderThumbs); diff --git a/src/topics/create.js b/src/topics/create.js index 352823a202..2f41c822b1 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -41,6 +41,12 @@ module.exports = function (Topics) { topicData.tags = data.tags.join(','); } + if (Array.isArray(data.thumbs) && data.thumbs.length) { + const thumbs = Topics.thumbs.filterThumbs(data.thumbs); + topicData.thumbs = JSON.stringify(thumbs); + topicData.numThumbs = thumbs.length; + } + const result = await plugins.hooks.fire('filter:topic.create', { topic: topicData, data: data }); topicData = result.topic; await db.setObject(`topic:${topicData.tid}`, topicData); diff --git a/src/topics/data.js b/src/topics/data.js index 76c027121d..a5801e0475 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -140,4 +140,12 @@ function modifyTopic(topic, fields) { }; }); } + + if (fields.includes('thumbs') || !fields.length) { + try { + topic.thumbs = topic.thumbs ? JSON.parse(String(topic.thumbs || '[]')) : []; + } catch (e) { + topic.thumbs = []; + } + } } diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index be2916a05d..2c3482664d 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -5,29 +5,26 @@ const _ = require('lodash'); const nconf = require('nconf'); const path = require('path'); const mime = require('mime'); - -const db = require('../database'); -const file = require('../file'); const plugins = require('../plugins'); const posts = require('../posts'); const meta = require('../meta'); -const cache = require('../cache'); const topics = module.parent.exports; const Thumbs = module.exports; -Thumbs.exists = async function (id, path) { - const isDraft = !await topics.exists(id); - const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; +const upload_url = nconf.get('relative_path') + nconf.get('upload_url'); +const upload_path = nconf.get('upload_path'); - return db.isSortedSetMember(set, path); +Thumbs.exists = async function (tid, path) { + const thumbs = await topics.getTopicField(tid, 'thumbs'); + return thumbs.includes(path); }; Thumbs.load = async function (topicData) { const mainPids = topicData.filter(Boolean).map(t => t.mainPid); - let hashes = await posts.getPostsFields(mainPids, ['attachments']); - const hasUploads = await db.exists(mainPids.map(pid => `post:${pid}:uploads`)); - hashes = hashes.map(o => o.attachments); + const mainPostData = await posts.getPostsFields(mainPids, ['attachments', 'uploads']); + const hasUploads = mainPostData.map(p => Array.isArray(p.uploads) && p.uploads.length > 0); + const hashes = mainPostData.map(o => o.attachments); let hasThumbs = topicData.map((t, idx) => t && (parseInt(t.numThumbs, 10) > 0 || !!(hashes[idx] && hashes[idx].length) || @@ -36,11 +33,70 @@ Thumbs.load = async function (topicData) { const topicsWithThumbs = topicData.filter((tid, idx) => hasThumbs[idx]); const tidsWithThumbs = topicsWithThumbs.map(t => t.tid); - const thumbs = await Thumbs.get(tidsWithThumbs); + + const thumbs = await loadFromTopicData(topicsWithThumbs); + const tidToThumbs = _.zipObject(tidsWithThumbs, thumbs); return topicData.map(t => (t && t.tid ? (tidToThumbs[t.tid] || []) : [])); }; +async function loadFromTopicData(topicData, options = {}) { + const tids = topicData.map(t => t.tid); + const thumbs = topicData.map(t => t ? t.thumbs : []); + + if (!options.thumbsOnly) { + const mainPids = topicData.map(t => t.mainPid); + const [mainPidUploads, mainPidAttachments] = await Promise.all([ + posts.uploads.list(mainPids), + posts.attachments.get(mainPids), + ]); + + // Add uploaded media to thumb sets + mainPidUploads.forEach((uploads, idx) => { + uploads = uploads.filter((upload) => { + const type = mime.getType(upload); + return !thumbs[idx].includes(upload) && type && type.startsWith('image/'); + }); + + if (uploads.length) { + thumbs[idx].push(...uploads); + } + }); + + // Add attachments to thumb sets + mainPidAttachments.forEach((attachments, idx) => { + attachments = attachments.filter( + attachment => !thumbs[idx].includes(attachment.url) && (attachment.mediaType && attachment.mediaType.startsWith('image/')) + ); + + if (attachments.length) { + thumbs[idx].push(...attachments.map(attachment => attachment.url)); + } + }); + } + + const hasTimestampPrefix = /^\d+-/; + + let response = thumbs.map((thumbSet, idx) => thumbSet.map(thumb => ({ + id: String(tids[idx]), + name: (() => { + const name = path.basename(thumb); + return hasTimestampPrefix.test(name) ? name.slice(14) : name; + })(), + path: thumb, + url: thumb.startsWith('http') ? + thumb : + path.posix.join(upload_url, thumb.replace(/\\/g, '/')), + }))); + + ({ thumbs: response } = await plugins.hooks.fire('filter:topics.getThumbs', { + tids, + thumbsOnly: options.thumbsOnly, + thumbs: response, + })); + return response; +}; + Thumbs.get = async function (tids, options) { // Allow singular or plural usage let singular = false; @@ -54,118 +110,77 @@ Thumbs.get = async function (tids, options) { thumbsOnly: false, }; } - - const isDraft = (await topics.exists(tids)).map(exists => !exists); - if (!meta.config.allowTopicsThumbnail || !tids.length) { return singular ? [] : tids.map(() => []); } - const hasTimestampPrefix = /^\d+-/; - const upload_url = nconf.get('relative_path') + nconf.get('upload_url'); - const sets = tids.map((tid, idx) => `${isDraft[idx] ? 'draft' : 'topic'}:${tid}:thumbs`); - const thumbs = await Promise.all(sets.map(getThumbs)); - - let mainPids = await topics.getTopicsFields(tids, ['mainPid']); - mainPids = mainPids.map(o => o.mainPid); - - if (!options.thumbsOnly) { - // Add uploaded media to thumb sets - const mainPidUploads = await Promise.all(mainPids.map(posts.uploads.list)); - mainPidUploads.forEach((uploads, idx) => { - uploads = uploads.filter((upload) => { - const type = mime.getType(upload); - return !thumbs[idx].includes(upload) && type && type.startsWith('image/'); - }); - - if (uploads.length) { - thumbs[idx].push(...uploads); - } - }); - - // Add attachments to thumb sets - const mainPidAttachments = await posts.attachments.get(mainPids); - mainPidAttachments.forEach((attachments, idx) => { - attachments = attachments.filter( - attachment => !thumbs[idx].includes(attachment.url) && (attachment.mediaType && attachment.mediaType.startsWith('image/')) - ); - - if (attachments.length) { - thumbs[idx].push(...attachments.map(attachment => attachment.url)); - } - }); - } - - let response = thumbs.map((thumbSet, idx) => thumbSet.map(thumb => ({ - id: tids[idx], - name: (() => { - const name = path.basename(thumb); - return hasTimestampPrefix.test(name) ? name.slice(14) : name; - })(), - path: thumb, - url: thumb.startsWith('http') ? thumb : path.posix.join(upload_url, thumb.replace(/\\/g, '/')), - }))); - - ({ thumbs: response } = await plugins.hooks.fire('filter:topics.getThumbs', { - tids, - thumbsOnly: options.thumbsOnly, - thumbs: response, - })); - return singular ? response.pop() : response; + const topicData = await topics.getTopicsFields(tids, ['tid', 'mainPid', 'thumbs']); + const response = await loadFromTopicData(topicData, options); + return singular ? response[0] : response; }; -async function getThumbs(set) { - const cached = cache.get(set); - if (cached !== undefined) { - return cached.slice(); - } - const thumbs = await db.getSortedSetRange(set, 0, -1); - cache.set(set, thumbs); - return thumbs.slice(); -} Thumbs.associate = async function ({ id, path, score }) { - // Associates a newly uploaded file as a thumb to the passed-in draft or topic - const isDraft = !await topics.exists(id); + // Associates a newly uploaded file as a thumb to the passed-in topic + const topicData = await topics.getTopicData(id); + if (!topicData) { + return; + } const isLocal = !path.startsWith('http'); - const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; - const numThumbs = await db.sortedSetCard(set); // Normalize the path to allow for changes in upload_path (and so upload_url can be appended if needed) if (isLocal) { path = path.replace(nconf.get('relative_path'), ''); path = path.replace(nconf.get('upload_url'), ''); } - await db.sortedSetAdd(set, isFinite(score) ? score : numThumbs, path); - if (!isDraft) { - const numThumbs = await db.sortedSetCard(set); - await topics.setTopicField(id, 'numThumbs', numThumbs); - } - cache.del(set); - // Associate thumbnails with the main pid (only on local upload) - if (!isDraft && isLocal) { - const mainPid = (await topics.getMainPids([id]))[0]; - await posts.uploads.associate(mainPid, path); + if (Array.isArray(topicData.thumbs)) { + const currentIdx = topicData.thumbs.indexOf(path); + const insertIndex = (typeof score === 'number' && score >= 0 && score < topicData.thumbs.length) ? + score : + topicData.thumbs.length; + + if (currentIdx !== -1) { + // Remove from current position + topicData.thumbs.splice(currentIdx, 1); + // Adjust insertIndex if needed + const adjustedIndex = currentIdx < insertIndex ? insertIndex - 1 : insertIndex; + topicData.thumbs.splice(adjustedIndex, 0, path); + } else { + topicData.thumbs.splice(insertIndex, 0, path); + } + + await topics.setTopicFields(id, { + thumbs: JSON.stringify(topicData.thumbs), + numThumbs: topicData.thumbs.length, + }); + // Associate thumbnails with the main pid (only on local upload) + if (isLocal && currentIdx === -1) { + await posts.uploads.associate(topicData.mainPid, path); + } } }; -Thumbs.migrate = async function (uuid, id) { - // Converts the draft thumb zset to the topic zset (combines thumbs if applicable) - const set = `draft:${uuid}:thumbs`; - const thumbs = await db.getSortedSetRangeWithScores(set, 0, -1); - await Promise.all(thumbs.map(async thumb => await Thumbs.associate({ - id, - path: thumb.value, - score: thumb.score, - }))); - await db.delete(set); - cache.del(set); +Thumbs.filterThumbs = function (thumbs) { + if (!Array.isArray(thumbs)) { + return []; + } + thumbs = thumbs.filter((thumb) => { + if (thumb.startsWith('http')) { + return true; + } + // ensure it is in upload path + const fullPath = path.join(upload_path, thumb); + return fullPath.startsWith(upload_path); + }); + return thumbs; }; -Thumbs.delete = async function (id, relativePaths) { - const isDraft = !await topics.exists(id); - const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; +Thumbs.delete = async function (tid, relativePaths) { + const topicData = await topics.getTopicData(tid); + if (!topicData) { + return; + } if (typeof relativePaths === 'string') { relativePaths = [relativePaths]; @@ -173,48 +188,28 @@ Thumbs.delete = async function (id, relativePaths) { throw new Error('[[error:invalid-data]]'); } - const absolutePaths = relativePaths.map(relativePath => path.join(nconf.get('upload_path'), relativePath)); - const [associated, existsOnDisk] = await Promise.all([ - db.isSortedSetMembers(set, relativePaths), - Promise.all(absolutePaths.map(async absolutePath => file.exists(absolutePath))), - ]); + const toRemove = relativePaths.map( + relativePath => topicData.thumbs.includes(relativePath) ? relativePath : null + ).filter(Boolean); - const toRemove = []; - const toDelete = []; - relativePaths.forEach((relativePath, idx) => { - if (associated[idx]) { - toRemove.push(relativePath); - } - - if (existsOnDisk[idx]) { - toDelete.push(absolutePaths[idx]); - } - }); - - await db.sortedSetRemove(set, toRemove); - - if (isDraft && toDelete.length) { // drafts only; post upload dissociation handles disk deletion for topics - await Promise.all(toDelete.map(path => file.delete(path))); - } - - if (toRemove.length && !isDraft) { - const topics = require('.'); - const mainPid = (await topics.getMainPids([id]))[0]; + if (toRemove.length) { + const { mainPid } = topicData.mainPid; + topicData.thumbs = topicData.thumbs.filter(thumb => !toRemove.includes(thumb)); await Promise.all([ - db.incrObjectFieldBy(`topic:${id}`, 'numThumbs', -toRemove.length), + topics.setTopicFields(tid, { + thumbs: JSON.stringify(topicData.thumbs), + numThumbs: topicData.thumbs.length, + }), Promise.all(toRemove.map(async relativePath => posts.uploads.dissociate(mainPid, relativePath))), ]); } - if (toRemove.length) { - cache.del(set); +}; + +Thumbs.deleteAll = async (tid) => { + const topicData = await topics.getTopicData(tid); + if (!topicData) { + return; } -}; - -Thumbs.deleteAll = async (id) => { - const isDraft = !await topics.exists(id); - const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; - - const thumbs = await db.getSortedSetRange(set, 0, -1); - await Thumbs.delete(id, thumbs); + await Thumbs.delete(tid, topicData.thumbs); }; diff --git a/src/upgrades/4.5.0/post-uploads-to-hash.js b/src/upgrades/4.5.0/post-uploads-to-hash.js new file mode 100644 index 0000000000..bef3b72df7 --- /dev/null +++ b/src/upgrades/4.5.0/post-uploads-to-hash.js @@ -0,0 +1,39 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Move post::uploads to post hash', + timestamp: Date.UTC(2025, 6, 5), + method: async function () { + const { progress } = this; + + const postCount = await db.sortedSetCard('posts:pid'); + progress.total = postCount; + + await batch.processSortedSet('posts:pid', async (pids) => { + const keys = pids.map(pid => `post:${pid}:uploads`); + + const postUploadData = await db.getSortedSetsMembersWithScores(keys); + + const bulkSet = []; + postUploadData.forEach((postUploads, idx) => { + const pid = pids[idx]; + if (Array.isArray(postUploads) && postUploads.length > 0) { + bulkSet.push([ + `post:${pid}`, + { uploads: JSON.stringify(postUploads.map(upload => upload.value)) }, + ]); + } + }); + + await db.setObjectBulk(bulkSet); + await db.deleteAll(keys); + + progress.incr(pids.length); + }, { + batch: 500, + }); + }, +}; diff --git a/src/upgrades/4.5.0/topic-thumbs-to-hash.js b/src/upgrades/4.5.0/topic-thumbs-to-hash.js new file mode 100644 index 0000000000..3385052c90 --- /dev/null +++ b/src/upgrades/4.5.0/topic-thumbs-to-hash.js @@ -0,0 +1,39 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Move topic::thumbs to topic hash', + timestamp: Date.UTC(2025, 6, 5), + method: async function () { + const { progress } = this; + + const topicCount = await db.sortedSetCard('topics:tid'); + progress.total = topicCount; + + await batch.processSortedSet('topics:tid', async (tids) => { + const keys = tids.map(tid => `topic:${tid}:thumbs`); + + const topicThumbData = await db.getSortedSetsMembersWithScores(keys); + + const bulkSet = []; + topicThumbData.forEach((topicThumbs, idx) => { + const tid = tids[idx]; + if (Array.isArray(topicThumbs) && topicThumbs.length > 0) { + bulkSet.push([ + `topic:${tid}`, + { thumbs: JSON.stringify(topicThumbs.map(thumb => thumb.value)) }, + ]); + } + }); + + await db.setObjectBulk(bulkSet); + await db.deleteAll(keys); + + progress.incr(tids.length); + }, { + batch: 500, + }); + }, +}; diff --git a/src/views/modals/topic-thumbs.tpl b/src/views/modals/topic-thumbs.tpl index ead862457c..d59f189be1 100644 --- a/src/views/modals/topic-thumbs.tpl +++ b/src/views/modals/topic-thumbs.tpl @@ -3,13 +3,13 @@
[[modules:thumbs.modal.no-thumbs]]
{{{ end }}} {{{ each thumbs }}} -
+
- +

- {./name} + {uploadBasename(@value)}

diff --git a/test/posts/uploads.js b/test/posts/uploads.js index 8f22f3d1f9..45ea10411f 100644 --- a/test/posts/uploads.js +++ b/test/posts/uploads.js @@ -62,13 +62,13 @@ describe('upload methods', () => { }); describe('.sync()', () => { - it('should properly add new images to the post\'s zset', (done) => { + it('should properly add new images to the post\'s hash', (done) => { posts.uploads.sync(pid, (err) => { assert.ifError(err); - db.sortedSetCard(`post:${pid}:uploads`, (err, length) => { + posts.uploads.list(pid, (err, uploads) => { assert.ifError(err); - assert.strictEqual(length, 2); + assert.strictEqual(uploads.length, 2); done(); }); }); @@ -81,8 +81,8 @@ describe('upload methods', () => { content: 'here is an image [alt text](/assets/uploads/files/abracadabra.png)... AND NO MORE!', }); await posts.uploads.sync(pid); - const length = await db.sortedSetCard(`post:${pid}:uploads`); - assert.strictEqual(1, length); + const uploads = await posts.uploads.list(pid); + assert.strictEqual(1, uploads.length); }); }); @@ -345,13 +345,11 @@ describe('post uploads management', () => { reply = replyData; }); - it('should automatically sync uploads on topic create and reply', (done) => { - db.sortedSetsCard([`post:${topic.topicData.mainPid}:uploads`, `post:${reply.pid}:uploads`], (err, lengths) => { - assert.ifError(err); - assert.strictEqual(lengths[0], 1); - assert.strictEqual(lengths[1], 1); - done(); - }); + it('should automatically sync uploads on topic create and reply', async () => { + const uploads1 = await posts.uploads.list(topic.topicData.mainPid); + const uploads2 = await posts.uploads.list(reply.pid); + assert.strictEqual(uploads1.length, 1); + assert.strictEqual(uploads2.length, 1); }); it('should automatically sync uploads on post edit', async () => { diff --git a/test/topics/thumbs.js b/test/topics/thumbs.js index afc13e5b22..b740596c74 100644 --- a/test/topics/thumbs.js +++ b/test/topics/thumbs.js @@ -37,8 +37,6 @@ describe('Topic thumbs', () => { const relativeThumbPaths = thumbPaths.map(path => path.replace(nconf.get('upload_path'), '')); - const uuid = utils.generateUUID(); - function createFiles() { fs.closeSync(fs.openSync(path.resolve(__dirname, '../uploads', thumbPaths[0]), 'w')); fs.closeSync(fs.openSync(path.resolve(__dirname, '../uploads', thumbPaths[1]), 'w')); @@ -70,7 +68,11 @@ describe('Topic thumbs', () => { // Touch a couple files and associate it to a topic createFiles(); - await db.sortedSetAdd(`topic:${topicObj.topicData.tid}:thumbs`, 0, `${relativeThumbPaths[0]}`); + + await topics.setTopicFields(topicObj.topicData.tid, { + numThumbs: 1, + thumbs: JSON.stringify([relativeThumbPaths[0]]), + }); }); it('should return bool for whether a thumb exists', async () => { @@ -80,10 +82,9 @@ describe('Topic thumbs', () => { describe('.get()', () => { it('should return an array of thumbs', async () => { - require('../../src/cache').del(`topic:${topicObj.topicData.tid}:thumbs`); const thumbs = await topics.thumbs.get(topicObj.topicData.tid); assert.deepStrictEqual(thumbs, [{ - id: topicObj.topicData.tid, + id: String(topicObj.topicData.tid), name: 'test.png', path: `${relativeThumbPaths[0]}`, url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, @@ -94,7 +95,7 @@ describe('Topic thumbs', () => { const thumbs = await topics.thumbs.get([topicObj.topicData.tid, topicObj.topicData.tid + 1]); assert.deepStrictEqual(thumbs, [ [{ - id: topicObj.topicData.tid, + id: String(topicObj.topicData.tid), name: 'test.png', path: `${relativeThumbPaths[0]}`, url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, @@ -119,25 +120,13 @@ describe('Topic thumbs', () => { mainPid = topicObj.postData.pid; }); - it('should add an uploaded file to a zset', async () => { + it('should add an uploaded file to the topic hash', async () => { await topics.thumbs.associate({ id: tid, path: relativeThumbPaths[0], }); - - const exists = await db.isSortedSetMember(`topic:${tid}:thumbs`, relativeThumbPaths[0]); - assert(exists); - }); - - it('should also work with UUIDs', async () => { - await topics.thumbs.associate({ - id: uuid, - path: relativeThumbPaths[1], - score: 5, - }); - - const exists = await db.isSortedSetMember(`draft:${uuid}:thumbs`, relativeThumbPaths[1]); - assert(exists); + const topicData = await topics.getTopicData(tid); + assert(topicData.thumbs.includes(relativeThumbPaths[0])); }); it('should also work with a URL', async () => { @@ -145,14 +134,8 @@ describe('Topic thumbs', () => { id: tid, path: relativeThumbPaths[2], }); - - const exists = await db.isSortedSetMember(`topic:${tid}:thumbs`, relativeThumbPaths[2]); - assert(exists); - }); - - it('should have a score equal to the number of thumbs prior to addition', async () => { - const scores = await db.sortedSetScores(`topic:${tid}:thumbs`, [relativeThumbPaths[0], relativeThumbPaths[2]]); - assert.deepStrictEqual(scores, [0, 1]); + const topicData = await topics.getTopicData(tid); + assert(topicData.thumbs.includes(relativeThumbPaths[2])); }); it('should update the relevant topic hash with the number of thumbnails', async () => { @@ -166,23 +149,19 @@ describe('Topic thumbs', () => { path: relativeThumbPaths[0], }); - const score = await db.sortedSetScore(`topic:${tid}:thumbs`, relativeThumbPaths[0]); - - assert(isFinite(score)); // exists in set - assert.strictEqual(score, 2); + const topicData = await topics.getTopicData(tid); + assert.strictEqual(topicData.thumbs.indexOf(relativeThumbPaths[0]), 1); }); - it('should update the score to be passed in as the third argument', async () => { + it('should update the index to be passed in as the third argument', async () => { await topics.thumbs.associate({ id: tid, path: relativeThumbPaths[0], score: 0, }); - const score = await db.sortedSetScore(`topic:${tid}:thumbs`, relativeThumbPaths[0]); - - assert(isFinite(score)); // exists in set - assert.strictEqual(score, 0); + const topicData = await topics.getTopicData(tid); + assert.strictEqual(topicData.thumbs.indexOf(relativeThumbPaths[0]), 0); }); it('should associate the thumbnail with that topic\'s main pid\'s uploads', async () => { @@ -195,33 +174,6 @@ describe('Topic thumbs', () => { const uploads = await posts.uploads.list(mainPid); assert(uploads.includes(relativeThumbPaths[0])); }); - - it('should combine the thumbs uploaded to a UUID zset and combine it with a topic\'s thumb zset', async () => { - await topics.thumbs.migrate(uuid, tid); - - const thumbs = await topics.thumbs.get(tid); - assert.strictEqual(thumbs.length, 3); - assert.deepStrictEqual(thumbs, [ - { - id: tid, - name: 'test.png', - path: relativeThumbPaths[0], - url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, - }, - { - id: tid, - name: 'example.org', - path: 'https://example.org', - url: 'https://example.org', - }, - { - id: tid, - name: 'test2.png', - path: relativeThumbPaths[1], - url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[1]}`, - }, - ]); - }); }); describe(`.delete()`, () => { @@ -231,8 +183,8 @@ describe('Topic thumbs', () => { path: `/files/test.png`, }); await topics.thumbs.delete(1, `/files/test.png`); - - assert.strictEqual(await db.isSortedSetMember('topic:1:thumbs', '/files/test.png'), false); + const thumbs = await topics.getTopicField(1, 'thumbs'); + assert.strictEqual(thumbs.includes(`/files/test.png`), false); }); it('should no longer be associated with that topic\'s main pid\'s uploads', async () => { @@ -241,40 +193,12 @@ describe('Topic thumbs', () => { assert(!uploads.includes(path.basename(relativeThumbPaths[0]))); }); - it('should also work with UUIDs', async () => { - await topics.thumbs.associate({ - id: uuid, - path: `/files/test.png`, - }); - await topics.thumbs.delete(uuid, '/files/test.png'); - - assert.strictEqual(await db.isSortedSetMember(`draft:${uuid}:thumbs`, '/files/test.png'), false); - assert.strictEqual(await file.exists(path.join(`${nconf.get('upload_path')}`, '/files/test.png')), false); - }); - - it('should also work with URLs', async () => { - await topics.thumbs.associate({ - id: uuid, - path: thumbPaths[2], - }); - await topics.thumbs.delete(uuid, relativeThumbPaths[2]); - - assert.strictEqual(await db.isSortedSetMember(`draft:${uuid}:thumbs`, relativeThumbPaths[2]), false); - }); - - it('should not delete the file from disk if not associated with the tid', async () => { - createFiles(); - await topics.thumbs.delete(uuid, thumbPaths[0]); - assert.strictEqual(await file.exists(thumbPaths[0]), true); - }); - it('should have no more thumbs left', async () => { - const associated = await db.isSortedSetMembers(`topic:1:thumbs`, [relativeThumbPaths[0], relativeThumbPaths[1]]); - assert.strictEqual(associated.some(Boolean), false); + const thumbs = await topics.getTopicField(1, 'thumbs'); + assert.strictEqual(thumbs.length, 0); }); it('should decrement numThumbs if dissociated one by one', async () => { - console.log('before', await db.getSortedSetRange(`topic:1:thumbs`, 0, -1)); await topics.thumbs.associate({ id: 1, path: `${nconf.get('relative_path')}${nconf.get('upload_url')}/files/test.png` }); await topics.thumbs.associate({ id: 1, path: `${nconf.get('relative_path')}${nconf.get('upload_url')}/files/test2.png` }); @@ -290,18 +214,14 @@ describe('Topic thumbs', () => { describe('.deleteAll()', () => { before(async () => { - await Promise.all([ - topics.thumbs.associate({ id: 1, path: '/files/test.png' }), - topics.thumbs.associate({ id: 1, path: '/files/test2.png' }), - ]); + await topics.thumbs.associate({ id: 1, path: '/files/test.png' }); + await topics.thumbs.associate({ id: 1, path: '/files/test2.png' }); createFiles(); }); it('should have thumbs prior to tests', async () => { - const associated = await db.isSortedSetMembers( - `topic:1:thumbs`, ['/files/test.png', '/files/test2.png'] - ); - assert.strictEqual(associated.every(Boolean), true); + const thumbs = await topics.getTopicField(1, 'thumbs'); + assert.deepStrictEqual(thumbs, ['/files/test.png', '/files/test2.png']); }); it('should not error out', async () => { @@ -309,14 +229,8 @@ describe('Topic thumbs', () => { }); it('should remove all associated thumbs with that topic', async () => { - const associated = await db.isSortedSetMembers( - `topic:1:thumbs`, ['/files/test.png', '/files/test2.png'] - ); - assert.strictEqual(associated.some(Boolean), false); - }); - - it('should no longer have a :thumbs zset', async () => { - assert.strictEqual(await db.exists('topic:1:thumbs'), false); + const thumbs = await topics.getTopicField(1, 'thumbs'); + assert.deepStrictEqual(thumbs, []); }); }); @@ -330,11 +244,6 @@ describe('Topic thumbs', () => { assert.strictEqual(response.statusCode, 200); }); - it('should succeed with a uuid', async () => { - const { response } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); - assert.strictEqual(response.statusCode, 200); - }); - it('should succeed with uploader plugins', async () => { const hookMethod = async () => ({ name: 'test.png', @@ -346,7 +255,7 @@ describe('Topic thumbs', () => { }); const { response } = await helpers.uploadFile( - `${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, + `${nconf.get('url')}/api/v3/topics/1/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, @@ -375,7 +284,7 @@ describe('Topic thumbs', () => { it('should fail if thumbnails are not enabled', async () => { meta.config.allowTopicsThumbnail = 0; - const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/1/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); assert.strictEqual(response.statusCode, 503); assert(body && body.status); assert.strictEqual(body.status.message, 'Topic thumbnails are disabled.'); @@ -384,7 +293,7 @@ describe('Topic thumbs', () => { it('should fail if file is not image', async () => { meta.config.allowTopicsThumbnail = 1; - const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/503.html'), {}, adminJar, adminCSRF); + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/1/thumbs`, path.join(__dirname, '../files/503.html'), {}, adminJar, adminCSRF); assert.strictEqual(response.statusCode, 500); assert(body && body.status); assert.strictEqual(body.status.message, 'Invalid File'); @@ -402,21 +311,17 @@ describe('Topic thumbs', () => { content: 'The content of test topic', }); - await Promise.all([ - topics.thumbs.associate({ id: topicObj.tid, path: thumbPaths[0] }), - topics.thumbs.associate({ id: topicObj.tid, path: thumbPaths[1] }), - ]); + + await topics.thumbs.associate({ id: topicObj.tid, path: thumbPaths[0] }); + await topics.thumbs.associate({ id: topicObj.tid, path: thumbPaths[1] }); + createFiles(); await topics.purge(topicObj.tid, adminUid); }); - it('should no longer have a :thumbs zset', async () => { - assert.strictEqual(await db.exists(`topic:${topicObj.tid}:thumbs`), false); - }); - it('should not leave post upload associations behind', async () => { - const uploads = await db.getSortedSetMembers(`post:${topicObj.postData.pid}:uploads`); + const uploads = await posts.uploads.list(topicObj.postData.pid); assert.strictEqual(uploads.length, 0); }); });