diff --git a/src/api/users.js b/src/api/users.js index febcf290e6..da9ea1a489 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -18,8 +18,7 @@ const plugins = require('../plugins'); const events = require('../events'); const translator = require('../translator'); const sockets = require('../socket.io'); - -// const api = require('.'); +const utils = require('../utils'); const usersAPI = module.exports; @@ -686,6 +685,9 @@ usersAPI.generateExport = async (caller, { uid, type }) => { if (!validTypes.includes(type)) { throw new Error('[[error:invalid-data]]'); } + if (!utils.isNumber(uid) || !(parseInt(uid, 10) > 0)) { + throw new Error('[[error:invalid-uid]]'); + } const count = await db.incrObjectField('locks', `export:${uid}${type}`); if (count > 1) { throw new Error('[[error:already-exporting]]'); diff --git a/src/upgrades/3.8.0/user-upload-folders.js b/src/upgrades/3.8.0/user-upload-folders.js new file mode 100644 index 0000000000..826bf62c32 --- /dev/null +++ b/src/upgrades/3.8.0/user-upload-folders.js @@ -0,0 +1,86 @@ +'use strict'; + + +const fs = require('fs'); +const nconf = require('nconf'); +const path = require('path'); +const { mkdirp } = require('mkdirp'); + +const db = require('../../database'); +const batch = require('../../batch'); + + +module.exports = { + name: 'Create user upload folders', + timestamp: Date.UTC(2024, 4, 28), + method: async function () { + const { progress } = this; + + const folder = path.join(nconf.get('upload_path'), 'profile'); + + const userPicRegex = /^\d+-profile/; + const files = (await fs.promises.readdir(folder, { withFileTypes: true })) + .filter(item => !item.isDirectory() && String(item.name).match(userPicRegex)) + .map(item => item.name); + + progress.total = files.length; + await batch.processArray(files, async (files) => { + progress.incr(files.length); + await Promise.all(files.map(async (file) => { + const uid = file.split('-')[0]; + if (parseInt(uid, 10) > 0) { + await mkdirp(path.join(folder, `uid-${uid}`)); + await fs.promises.rename( + path.join(folder, file), + path.join(folder, `uid-${uid}`, file), + ); + } + })); + }, { + batch: 500, + }); + + await batch.processSortedSet('users:joindate', async (uids) => { + progress.incr(uids.length); + const usersData = await db.getObjects(uids.map(uid => `user:${uid}`)); + const bulkSet = []; + usersData.forEach((userData) => { + const setObj = {}; + if (userData && userData.picture && + userData.picture.includes(`/uploads/profile/${userData.uid}-`) && + !userData.picture.includes(`/uploads/profile/uid-${userData.uid}/${userData.uid}-`)) { + setObj.picture = userData.picture.replace( + `/uploads/profile/${userData.uid}-`, + `/uploads/profile/uid-${userData.uid}/${userData.uid}-` + ); + } + + if (userData && userData.uploadedpicture && + userData.uploadedpicture.includes(`/uploads/profile/${userData.uid}-`) && + !userData.uploadedpicture.includes(`/uploads/profile/uid-${userData.uid}/${userData.uid}-`)) { + setObj.uploadedpicture = userData.uploadedpicture.replace( + `/uploads/profile/${userData.uid}-`, + `/uploads/profile/uid-${userData.uid}/${userData.uid}-` + ); + } + + if (userData && userData['cover:url'] && + userData['cover:url'].includes(`/uploads/profile/${userData.uid}-`) && + !userData['cover:url'].includes(`/uploads/profile/uid-${userData.uid}/${userData.uid}-`)) { + setObj['cover:url'] = userData['cover:url'].replace( + `/uploads/profile/${userData.uid}-`, + `/uploads/profile/uid-${userData.uid}/${userData.uid}-` + ); + } + + if (Object.keys(setObj).length) { + bulkSet.push([`user:${userData.uid}`, setObj]); + } + }); + await db.setObjectBulk(bulkSet); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/user/delete.js b/src/user/delete.js index 681eabeec1..9efd8802ae 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -227,7 +227,7 @@ module.exports = function (User) { } async function deleteImages(uid) { - const folder = path.join(nconf.get('upload_path'), 'profile'); - await rimraf(`${uid}-profile{avatar,cover}*`, { glob: { cwd: folder } }); + const folder = path.join(nconf.get('upload_path'), 'profile', `uid-${uid}`); + await rimraf(folder); } }; diff --git a/src/user/jobs/export-uploads.js b/src/user/jobs/export-uploads.js index a3bc097a49..89c623211e 100644 --- a/src/user/jobs/export-uploads.js +++ b/src/user/jobs/export-uploads.js @@ -74,14 +74,8 @@ process.on('message', async (msg) => { winston.verbose(`[user/export/uploads] Collating uploads for uid ${targetUid}`); await user.collateUploads(targetUid, archive); - const uploadedPicture = await user.getUserField(targetUid, 'uploadedpicture'); - if (uploadedPicture) { - const filePath = uploadedPicture.replace(nconf.get('upload_url'), ''); - archive.file(path.join(nconf.get('upload_path'), filePath), { - name: path.basename(filePath), - }); - } - + const profileUploadPath = path.join(nconf.get('upload_path'), `profile/uid-${targetUid}`); + archive.directory(profileUploadPath, 'profile'); archive.finalize(); } }); diff --git a/src/user/picture.js b/src/user/picture.js index d3d2a22f68..fbff7fd225 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -50,7 +50,7 @@ module.exports = function (User) { const extension = file.typeToExtension(image.mimeFromBase64(data.imageData)); const filename = `${data.uid}-profilecover-${Date.now()}${extension}`; - const uploadData = await image.uploadImage(filename, 'profile', picture); + const uploadData = await image.uploadImage(filename, `profile/uid-${data.uid}`, picture); await deleteCurrentPicture(data.uid, 'cover:url'); await User.setUserField(data.uid, 'cover:url', uploadData.url); @@ -96,7 +96,7 @@ module.exports = function (User) { }); const filename = generateProfileImageFilename(data.uid, extension); - const uploadedImage = await image.uploadImage(filename, 'profile', { + const uploadedImage = await image.uploadImage(filename, `profile/uid-${data.uid}`, { uid: data.uid, path: newPath, name: 'profileAvatar', @@ -140,7 +140,7 @@ module.exports = function (User) { }); const filename = generateProfileImageFilename(data.uid, extension); - const uploadedImage = await image.uploadImage(filename, 'profile', picture); + const uploadedImage = await image.uploadImage(filename, `profile/uid-${data.uid}`, picture); await deleteCurrentPicture(data.uid, 'uploadedpicture'); await User.updateProfile(data.callerUid, { @@ -224,10 +224,10 @@ module.exports = function (User) { async function getPicturePath(uid, field) { const value = await User.getUserField(uid, field); - if (!value || !value.startsWith(`${nconf.get('relative_path')}/assets/uploads/profile/`)) { + if (!value || !value.startsWith(`${nconf.get('relative_path')}/assets/uploads/profile/uid-${uid}`)) { return false; } const filename = value.split('/').pop(); - return path.join(nconf.get('upload_path'), 'profile', filename); + return path.join(nconf.get('upload_path'), `profile/uid-${uid}`, filename); } };