diff --git a/src/user/uploads.js b/src/user/uploads.js index 14c7a67b34..219b8f94b1 100644 --- a/src/user/uploads.js +++ b/src/user/uploads.js @@ -1,90 +1,94 @@ -'use strict'; - -const path = require('path'); -const nconf = require('nconf'); -const winston = require('winston'); -const crypto = require('crypto'); - -const db = require('../database'); -const posts = require('../posts'); -const file = require('../file'); -const batch = require('../batch'); - -const md5 = filename => crypto.createHash('md5').update(filename).digest('hex'); -const _getFullPath = relativePath => path.resolve(nconf.get('upload_path'), relativePath); -const _validatePath = async (relativePaths) => { - if (typeof relativePaths === 'string') { - relativePaths = [relativePaths]; - } else if (!Array.isArray(relativePaths)) { - throw new Error(`[[error:wrong-parameter-type, relativePaths, ${typeof relativePaths}, array]]`); - } - - const fullPaths = relativePaths.map(path => _getFullPath(path)); - const exists = await Promise.all(fullPaths.map(async fullPath => file.exists(fullPath))); - - if (!fullPaths.every(fullPath => fullPath.startsWith(nconf.get('upload_path'))) || !exists.every(Boolean)) { - throw new Error('[[error:invalid-path]]'); - } +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); }; - -module.exports = function (User) { - User.associateUpload = async (uid, relativePath) => { - await _validatePath(relativePath); - await Promise.all([ - db.sortedSetAdd(`uid:${uid}:uploads`, Date.now(), relativePath), - db.setObjectField(`upload:${md5(relativePath)}`, 'uid', uid), - ]); - }; - - User.deleteUpload = async function (callerUid, uid, uploadNames) { - if (typeof uploadNames === 'string') { - uploadNames = [uploadNames]; - } else if (!Array.isArray(uploadNames)) { - throw new Error(`[[error:wrong-parameter-type, uploadNames, ${typeof uploadNames}, array]]`); - } - - await _validatePath(uploadNames); - - const [isUsersUpload, isAdminOrGlobalMod] = await Promise.all([ - db.isSortedSetMembers(`uid:${callerUid}:uploads`, uploadNames), - User.isAdminOrGlobalMod(callerUid), - ]); - if (!isAdminOrGlobalMod && !isUsersUpload.every(Boolean)) { - throw new Error('[[error:no-privileges]]'); - } - - await batch.processArray(uploadNames, async (uploadNames) => { - const fullPaths = uploadNames.map(path => _getFullPath(path)); - - await Promise.all(fullPaths.map(async (fullPath, idx) => { - winston.verbose(`[user/deleteUpload] Deleting ${uploadNames[idx]}`); - await Promise.all([ - file.delete(fullPath), - file.delete(file.appendToFileName(fullPath, '-resized')), - ]); - await Promise.all([ - db.sortedSetRemove(`uid:${uid}:uploads`, uploadNames[idx]), - db.delete(`upload:${md5(uploadNames[idx])}`), - ]); - })); - - // Dissociate the upload from pids, if any - const pids = await db.getSortedSetsMembers(uploadNames.map(relativePath => `upload:${md5(relativePath)}:pids`)); - await Promise.all(pids.map(async (pids, idx) => Promise.all( - pids.map(async pid => posts.uploads.dissociate(pid, uploadNames[idx])) - ))); - }, { batch: 50 }); - }; - - User.collateUploads = async function (uid, archive) { - await batch.processSortedSet(`uid:${uid}:uploads`, (files, next) => { - files.forEach((file) => { - archive.file(_getFullPath(file), { - name: path.basename(file), - }); - }); - - setImmediate(next); - }, { batch: 100 }); - }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +const path_1 = __importDefault(require("path")); +const nconf_1 = __importDefault(require("nconf")); +const winston_1 = __importDefault(require("winston")); +const crypto_1 = __importDefault(require("crypto")); +const database_1 = __importDefault(require("../database")); +const posts_1 = __importDefault(require("../posts")); +const file_1 = __importDefault(require("../file")); +const batch_1 = __importDefault(require("../batch")); +const md5 = (filename) => crypto_1.default.createHash('md5').update(filename).digest('hex'); +const _getFullPath = (relativePath) => path_1.default.resolve(nconf_1.default.get('upload_path'), relativePath); +const _validatePath = (relativePaths) => __awaiter(void 0, void 0, void 0, function* () { + if (typeof relativePaths === 'string') { + relativePaths = [relativePaths]; + } + else if (!Array.isArray(relativePaths)) { + throw new Error(`[[error:wrong-parameter-type, relativePaths, ${typeof relativePaths}, array]]`); + } + const fullPaths = relativePaths.map(path => _getFullPath(path)); + const exists = yield Promise.all(fullPaths.map((fullPath) => __awaiter(void 0, void 0, void 0, function* () { return file_1.default.exists(fullPath); }))); + if (!fullPaths.every(fullPath => fullPath.startsWith(nconf_1.default.get('upload_path'))) || !exists.every(Boolean)) { + throw new Error('[[error:invalid-path]]'); + } +}); +function default_1(User) { + User.associateUpload = (uid, relativePath) => __awaiter(this, void 0, void 0, function* () { + yield _validatePath(relativePath); + yield Promise.all([ + database_1.default.sortedSetAdd(`uid:${uid}:uploads`, Date.now(), relativePath), + database_1.default.setObjectField(`upload:${md5(relativePath)}`, 'uid', uid), + ]); + }); + User.deleteUpload = function (callerUid, uid, uploadNames) { + return __awaiter(this, void 0, void 0, function* () { + if (typeof uploadNames === 'string') { + uploadNames = [uploadNames]; + } + else if (!Array.isArray(uploadNames)) { + throw new Error(`[[error:wrong-parameter-type, uploadNames, ${typeof uploadNames}, array]]`); + } + yield _validatePath(uploadNames); + const [isUsersUpload, isAdminOrGlobalMod] = yield Promise.all([ + database_1.default.isSortedSetMembers(`uid:${callerUid}:uploads`, uploadNames), + User.isAdminOrGlobalMod(callerUid), + ]); + if (!isAdminOrGlobalMod && !isUsersUpload.every(Boolean)) { + throw new Error('[[error:no-privileges]]'); + } + yield batch_1.default.processArray(uploadNames, (uploadNames) => __awaiter(this, void 0, void 0, function* () { + const fullPaths = uploadNames.map(path => _getFullPath(path)); + yield Promise.all(fullPaths.map((fullPath, idx) => __awaiter(this, void 0, void 0, function* () { + winston_1.default.verbose(`[user/deleteUpload] Deleting ${uploadNames[idx]}`); + yield Promise.all([ + file_1.default.delete(fullPath), + file_1.default.delete(file_1.default.appendToFileName(fullPath, '-resized')), + ]); + yield Promise.all([ + database_1.default.sortedSetRemove(`uid:${uid}:uploads`, uploadNames[idx]), + database_1.default.delete(`upload:${md5(uploadNames[idx])}`), + ]); + }))); + // Dissociate the upload from pids, if any + const pids = yield database_1.default.getSortedSetsMembers(uploadNames.map(relativePath => `upload:${md5(relativePath)}:pids`)); + yield Promise.all(pids.map((pids, idx) => __awaiter(this, void 0, void 0, function* () { return Promise.all(pids.map((pid) => __awaiter(this, void 0, void 0, function* () { return posts_1.default.uploads.dissociate(pid, uploadNames[idx]); }))); }))); + }), { batch: 50 }); + }); + }; + User.collateUploads = function (uid, archive) { + return __awaiter(this, void 0, void 0, function* () { + yield batch_1.default.processSortedSet(`uid:${uid}:uploads`, (files, next) => { + files.forEach((file) => { + archive.file(_getFullPath(file), { + name: path_1.default.basename(file), + }); + }); + setImmediate(() => next()); + }, { batch: 100 }); + }); + }; +} diff --git a/src/user/uploads.ts b/src/user/uploads.ts new file mode 100644 index 0000000000..2df56b1178 --- /dev/null +++ b/src/user/uploads.ts @@ -0,0 +1,89 @@ +import path from 'path'; +import nconf from 'nconf'; +import winston from 'winston'; +import crypto from 'crypto'; + +import db from '../database'; +import posts from '../posts'; +import file from '../file'; +import batch from '../batch'; + +const md5 = (filename: string): string => crypto.createHash('md5').update(filename).digest('hex'); +const _getFullPath = (relativePath: string): string => path.resolve(nconf.get('upload_path'), relativePath); + +const _validatePath = async (relativePaths: string | string[]): Promise => { + if (typeof relativePaths === 'string') { + relativePaths = [relativePaths]; + } else if (!Array.isArray(relativePaths)) { + throw new Error(`[[error:wrong-parameter-type, relativePaths, ${typeof relativePaths}, array]]`); + } + + const fullPaths = relativePaths.map(path => _getFullPath(path)); + const exists = await Promise.all(fullPaths.map(async (fullPath) => file.exists(fullPath))); + + if (!fullPaths.every(fullPath => fullPath.startsWith(nconf.get('upload_path'))) || !exists.every(Boolean)) { + throw new Error('[[error:invalid-path]]'); + } +}; + +export default function (User: Record) { + + User.associateUpload = async (uid: number, relativePath: string): Promise => { + await _validatePath(relativePath); + await Promise.all([ + db.sortedSetAdd(`uid:${uid}:uploads`, Date.now(), relativePath), + db.setObjectField(`upload:${md5(relativePath)}`, 'uid', uid), + ]); + }; + + User.deleteUpload = async function (callerUid: number, uid: number, uploadNames: string | string[]): Promise { + if (typeof uploadNames === 'string') { + uploadNames = [uploadNames]; + } else if (!Array.isArray(uploadNames)) { + throw new Error(`[[error:wrong-parameter-type, uploadNames, ${typeof uploadNames}, array]]`); + } + + await _validatePath(uploadNames); + + const [isUsersUpload, isAdminOrGlobalMod] = await Promise.all([ + db.isSortedSetMembers(`uid:${callerUid}:uploads`, uploadNames), + User.isAdminOrGlobalMod(callerUid), + ]); + + if (!isAdminOrGlobalMod && !isUsersUpload.every(Boolean)) { + throw new Error('[[error:no-privileges]]'); + } + + await batch.processArray(uploadNames, async (uploadNames: string[]) => { + const fullPaths = uploadNames.map(path => _getFullPath(path)); + + await Promise.all(fullPaths.map(async (fullPath, idx) => { + winston.verbose(`[user/deleteUpload] Deleting ${uploadNames[idx]}`); + await Promise.all([ + file.delete(fullPath), + file.delete(file.appendToFileName(fullPath, '-resized')), + ]); + await Promise.all([ + db.sortedSetRemove(`uid:${uid}:uploads`, uploadNames[idx]), + db.delete(`upload:${md5(uploadNames[idx])}`), + ]); + })); + + // Dissociate the upload from pids, if any + const pids = await db.getSortedSetsMembers(uploadNames.map(relativePath => `upload:${md5(relativePath)}:pids`)); + await Promise.all(pids.map(async (pids, idx) => + Promise.all(pids.map(async (pid) => posts.uploads.dissociate(pid, uploadNames[idx]))))); + }, { batch: 50 }); + }; + + User.collateUploads = async function (uid: number, archive: { file: (path: string, options: { name: string }) => void }): Promise { + await batch.processSortedSet(`uid:${uid}:uploads`, (files: string[], next: () => void) => { + files.forEach((file: string) => { + archive.file(_getFullPath(file), { + name: path.basename(file), + }); + }); + setImmediate(() => next()); + }, { batch: 100 }); + }; +}