mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +01:00
feat: server-side routes for handling multiple topic thumbnails
closes #8994, requires 'topic-thumb-refactor' branch of composer-default
This commit is contained in:
@@ -36,6 +36,9 @@ postsAPI.edit = async function (caller, data) {
|
|||||||
data.req = apiHelpers.buildReqObject(caller);
|
data.req = apiHelpers.buildReqObject(caller);
|
||||||
|
|
||||||
const editResult = await posts.edit(data);
|
const editResult = await posts.edit(data);
|
||||||
|
if (editResult.topic.isMainPost) {
|
||||||
|
await topics.thumbs.commit(data.uuid, editResult.topic.tid);
|
||||||
|
}
|
||||||
if (editResult.topic.renamed) {
|
if (editResult.topic.renamed) {
|
||||||
await events.log({
|
await events.log({
|
||||||
type: 'topic-rename',
|
type: 'topic-rename',
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ topicsAPI.create = async function (caller, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await topics.post(payload);
|
const result = await topics.post(payload);
|
||||||
|
await topics.thumbs.commit(data.uuid, result.topicData.tid);
|
||||||
|
|
||||||
socketHelpers.emitToUids('event:new_post', { posts: [result.postData] }, [caller.uid]);
|
socketHelpers.emitToUids('event:new_post', { posts: [result.postData] }, [caller.uid]);
|
||||||
socketHelpers.emitToUids('event:new_topic', result.topicData, [caller.uid]);
|
socketHelpers.emitToUids('event:new_topic', result.topicData, [caller.uid]);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const validator = require('validator');
|
||||||
|
|
||||||
const api = require('../../api');
|
const api = require('../../api');
|
||||||
const topics = require('../../topics');
|
const topics = require('../../topics');
|
||||||
|
|
||||||
const helpers = require('../helpers');
|
const helpers = require('../helpers');
|
||||||
|
const uploadsController = require('../uploads');
|
||||||
|
|
||||||
const Topics = module.exports;
|
const Topics = module.exports;
|
||||||
|
|
||||||
@@ -85,3 +88,33 @@ Topics.deleteTags = async (req, res) => {
|
|||||||
await topics.deleteTopicTags(req.params.tid);
|
await topics.deleteTopicTags(req.params.tid);
|
||||||
helpers.formatApiResponse(200, res);
|
helpers.formatApiResponse(200, res);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Topics.addThumb = async (req, res) => {
|
||||||
|
// 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 id = req.params.tid;
|
||||||
|
const isUUID = validator.isUUID(id);
|
||||||
|
|
||||||
|
// Sanity-check the tid if it's strictly not a uuid
|
||||||
|
if (!isUUID && (isNaN(parseInt(id, 10)) || !await topics.exists(req.params.tid))) {
|
||||||
|
return helpers.formatApiResponse(404, res, new Error('[[error:no-topic]]'));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* todo test:
|
||||||
|
* - uuid
|
||||||
|
* - tid
|
||||||
|
* - number but not tid
|
||||||
|
* - random garbage
|
||||||
|
*/
|
||||||
|
|
||||||
|
const files = await uploadsController.uploadThumb(req, res); // response is handled here, fix this?
|
||||||
|
|
||||||
|
// Add uploaded files to topic zset
|
||||||
|
await Promise.all(files.map(async (fileObj) => {
|
||||||
|
await topics.thumbs.associate(id, fileObj.path, isUUID);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
Topics.deleteThumb = async (req, res) => {
|
||||||
|
await topics.thumbs.delete(req.params.tid, req.query.path);
|
||||||
|
helpers.formatApiResponse(200, res, await topics.thumbs.get(req.params.tid));
|
||||||
|
};
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ Assert.topic = helpers.try(async (req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Assert.topicThumb = helpers.try(async (req, res, next) => {
|
||||||
|
// thumbs are parsed out of req.query
|
||||||
|
if (!await topics.thumbs.exists(req.params.tid, req.query.path)) {
|
||||||
|
return controllerHelpers.formatApiResponse(404, res, new Error('[[error:invalid-file]]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
Assert.post = helpers.try(async (req, res, next) => {
|
Assert.post = helpers.try(async (req, res, next) => {
|
||||||
if (!await posts.exists(req.params.pid)) {
|
if (!await posts.exists(req.params.pid)) {
|
||||||
return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-topic]]'));
|
return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-topic]]'));
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ const setupApiRoute = routeHelpers.setupApiRoute;
|
|||||||
module.exports = function () {
|
module.exports = function () {
|
||||||
const middlewares = [middleware.authenticate];
|
const middlewares = [middleware.authenticate];
|
||||||
|
|
||||||
|
var multipart = require('connect-multiparty');
|
||||||
|
var multipartMiddleware = multipart();
|
||||||
|
|
||||||
setupApiRoute(router, 'post', '/', [middleware.authenticateOrGuest, middleware.checkRequired.bind(null, ['cid', 'title', 'content'])], controllers.write.topics.create);
|
setupApiRoute(router, 'post', '/', [middleware.authenticateOrGuest, middleware.checkRequired.bind(null, ['cid', 'title', 'content'])], controllers.write.topics.create);
|
||||||
setupApiRoute(router, 'post', '/:tid', [middleware.authenticateOrGuest, middleware.checkRequired.bind(null, ['content']), middleware.assert.topic], controllers.write.topics.reply);
|
setupApiRoute(router, 'post', '/:tid', [middleware.authenticateOrGuest, middleware.checkRequired.bind(null, ['content']), middleware.assert.topic], controllers.write.topics.reply);
|
||||||
setupApiRoute(router, 'delete', '/:tid', [...middlewares], controllers.write.topics.purge);
|
setupApiRoute(router, 'delete', '/:tid', [...middlewares], controllers.write.topics.purge);
|
||||||
@@ -31,5 +34,8 @@ module.exports = function () {
|
|||||||
setupApiRoute(router, 'put', '/:tid/tags', [...middlewares, middleware.checkRequired.bind(null, ['tags']), middleware.assert.topic], controllers.write.topics.addTags);
|
setupApiRoute(router, 'put', '/:tid/tags', [...middlewares, middleware.checkRequired.bind(null, ['tags']), middleware.assert.topic], controllers.write.topics.addTags);
|
||||||
setupApiRoute(router, 'delete', '/:tid/tags', [...middlewares, middleware.assert.topic], controllers.write.topics.deleteTags);
|
setupApiRoute(router, 'delete', '/:tid/tags', [...middlewares, middleware.assert.topic], controllers.write.topics.deleteTags);
|
||||||
|
|
||||||
|
setupApiRoute(router, 'post', '/:tid/thumbs', [multipartMiddleware, middleware.validateFiles, ...middlewares], controllers.write.topics.addThumb);
|
||||||
|
setupApiRoute(router, 'delete', '/:tid/thumbs', [...middlewares, middleware.assert.topic, middleware.assert.topicThumb], controllers.write.topics.deleteThumb);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ var mime = require('mime');
|
|||||||
var validator = require('validator');
|
var validator = require('validator');
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
|
|
||||||
|
const db = require('../database');
|
||||||
var meta = require('../meta');
|
var meta = require('../meta');
|
||||||
var image = require('../image');
|
var image = require('../image');
|
||||||
var file = require('../file');
|
var file = require('../file');
|
||||||
@@ -24,6 +25,49 @@ function pipeToFile(source, destination, callback) {
|
|||||||
}
|
}
|
||||||
const pipeToFileAsync = util.promisify(pipeToFile);
|
const pipeToFileAsync = util.promisify(pipeToFile);
|
||||||
|
|
||||||
|
Thumbs.exists = async function (tid, path) {
|
||||||
|
// TODO: tests
|
||||||
|
return db.isSortedSetMember(`topic:${tid}:thumbs`, path);
|
||||||
|
};
|
||||||
|
|
||||||
|
Thumbs.get = async function (tid) {
|
||||||
|
const thumbs = await db.getSortedSetRange(`topic:${tid}:thumbs`, 0, -1);
|
||||||
|
return thumbs.map(thumb => path.join(nconf.get('upload_path'), thumb));
|
||||||
|
};
|
||||||
|
|
||||||
|
Thumbs.associate = async function (id, path, isDraft) {
|
||||||
|
// Associates a newly uploaded file as a thumb to the passed-in tid
|
||||||
|
const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`;
|
||||||
|
const numThumbs = await db.sortedSetCard(set);
|
||||||
|
path = path.replace(nconf.get('upload_path'), '');
|
||||||
|
db.sortedSetAdd(set, numThumbs, path);
|
||||||
|
};
|
||||||
|
|
||||||
|
Thumbs.commit = async function (uuid, tid) {
|
||||||
|
// Converts the draft thumb zset to the topic zset (combines thumbs if applicable)
|
||||||
|
const set = `draft:${uuid}:thumbs`;
|
||||||
|
const thumbs = await db.getSortedSetRange(set, 0, -1);
|
||||||
|
await Promise.all(thumbs.map(async path => await Thumbs.associate(tid, path, false)));
|
||||||
|
await db.delete(set);
|
||||||
|
};
|
||||||
|
|
||||||
|
Thumbs.delete = async function (tid, relativePath) {
|
||||||
|
// TODO: tests
|
||||||
|
const set = `topic:${tid}:thumbs`;
|
||||||
|
const absolutePath = path.join(nconf.get('upload_path'), relativePath);
|
||||||
|
const [associated, existsOnDisk] = await Promise.all([
|
||||||
|
db.isSortedSetMember(set, relativePath),
|
||||||
|
file.exists(absolutePath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (associated) {
|
||||||
|
await db.sortedSetRemove(set, relativePath);
|
||||||
|
}
|
||||||
|
if (existsOnDisk) {
|
||||||
|
await file.delete(absolutePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Thumbs.resizeAndUpload = async function (data) {
|
Thumbs.resizeAndUpload = async function (data) {
|
||||||
const allowedExtensions = file.allowedExtensions();
|
const allowedExtensions = file.allowedExtensions();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user