Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 92 additions & 88 deletions src/user/uploads.js
Original file line number Diff line number Diff line change
@@ -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 });
});
};
}
89 changes: 89 additions & 0 deletions src/user/uploads.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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<string, any>) {

User.associateUpload = async (uid: number, relativePath: string): Promise<void> => {
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<void> {
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<void> {
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 });
};
}