mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-02 20:16:04 +01:00
* feat: create folders in ACP uploads #9638 * fix: openapi * test: missing tests * fix: eslint * fix: tests
This commit is contained in:
@@ -5,5 +5,7 @@
|
||||
"orphaned": "Orphaned",
|
||||
"size/filecount": "Size / Filecount",
|
||||
"confirm-delete": "Do you really want to delete this file?",
|
||||
"filecount": "%1 files"
|
||||
"filecount": "%1 files",
|
||||
"new-folder": "New Folder",
|
||||
"name-new-folder": "Enter a name for new the folder"
|
||||
}
|
||||
@@ -28,6 +28,8 @@
|
||||
"invalid-event": "Invalid event: %1",
|
||||
"local-login-disabled": "Local login system has been disabled for non-privileged accounts.",
|
||||
"csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again",
|
||||
"invalid-path": "Invalid path",
|
||||
"folder-exists": "Folder exists",
|
||||
|
||||
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
|
||||
|
||||
|
||||
@@ -144,3 +144,5 @@ paths:
|
||||
$ref: 'write/admin/analytics/set.yaml'
|
||||
/files/:
|
||||
$ref: 'write/files.yaml'
|
||||
/files/folder:
|
||||
$ref: 'write/files/folder.yaml'
|
||||
36
public/openapi/write/files/folder.yaml
Normal file
36
public/openapi/write/files/folder.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
put:
|
||||
tags:
|
||||
- files
|
||||
summary: create a new folder
|
||||
description: This operation creates a new folder inside upload path
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Path to the file (relative to the configured `upload_path`)
|
||||
example: /files
|
||||
folderName:
|
||||
type: string
|
||||
description: New folder name
|
||||
example: myfiles
|
||||
required:
|
||||
- path
|
||||
- folderName
|
||||
responses:
|
||||
'200':
|
||||
description: Folder created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties: {}
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
define('admin/manage/uploads', ['uploader', 'api'], function (uploader, api) {
|
||||
define('admin/manage/uploads', ['api', 'bootbox', 'uploader'], function (api, bootbox, uploader) {
|
||||
var Uploads = {};
|
||||
|
||||
Uploads.init = function () {
|
||||
@@ -29,6 +28,21 @@ define('admin/manage/uploads', ['uploader', 'api'], function (uploader, api) {
|
||||
}).catch(app.alertError);
|
||||
});
|
||||
});
|
||||
|
||||
$('#new-folder').on('click', async function () {
|
||||
bootbox.prompt('[[admin/manage/uploads:name-new-folder]]', (newFolderName) => {
|
||||
if (!newFolderName || !newFolderName.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.put('/files/folder', {
|
||||
path: ajaxify.data.currentFolder,
|
||||
folderName: newFolderName,
|
||||
}).then(() => {
|
||||
ajaxify.refresh();
|
||||
}).catch(app.alertError);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return Uploads;
|
||||
|
||||
@@ -9,3 +9,8 @@ Files.delete = async (req, res) => {
|
||||
await fs.unlink(res.locals.cleanedPath);
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Files.createFolder = async (req, res) => {
|
||||
await fs.mkdir(res.locals.folderPath);
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ const user = require('../user');
|
||||
const groups = require('../groups');
|
||||
const topics = require('../topics');
|
||||
const posts = require('../posts');
|
||||
const slugify = require('../slugify');
|
||||
|
||||
const helpers = require('./helpers');
|
||||
const controllerHelpers = require('../controllers/helpers');
|
||||
@@ -86,3 +87,20 @@ Assert.path = helpers.try(async (req, res, next) => {
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
Assert.folderName = helpers.try(async (req, res, next) => {
|
||||
const folderName = slugify(path.basename(req.body.folderName.trim()));
|
||||
const folderPath = path.join(res.locals.cleanedPath, folderName);
|
||||
|
||||
// slugify removes invalid characters, folderName may become empty
|
||||
if (!folderName) {
|
||||
return controllerHelpers.formatApiResponse(403, res, new Error('[[error:invalid-path]]'));
|
||||
}
|
||||
if (await file.exists(folderPath)) {
|
||||
return controllerHelpers.formatApiResponse(403, res, new Error('[[error:folder-exists]]'));
|
||||
}
|
||||
|
||||
res.locals.folderPath = folderPath;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ const routeHelpers = require('../helpers');
|
||||
const { setupApiRoute } = routeHelpers;
|
||||
|
||||
module.exports = function () {
|
||||
const middlewares = [middleware.ensureLoggedIn];
|
||||
const middlewares = [middleware.ensureLoggedIn, middleware.admin.checkPrivileges];
|
||||
|
||||
// setupApiRoute(router, 'put', '/', [
|
||||
// ...middlewares,
|
||||
@@ -21,5 +21,13 @@ module.exports = function () {
|
||||
middleware.assert.path,
|
||||
], controllers.write.files.delete);
|
||||
|
||||
setupApiRoute(router, 'put', '/folder', [
|
||||
...middlewares,
|
||||
middleware.checkRequired.bind(null, ['path', 'folderName']),
|
||||
middleware.assert.path,
|
||||
// Should come after assert.path
|
||||
middleware.assert.folderName,
|
||||
], controllers.write.files.createFolder);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<!-- IMPORT partials/breadcrumbs.tpl -->
|
||||
<div class="clearfix">
|
||||
<button id="upload" class="btn-success pull-right"><i class="fa fa-upload"></i> [[global:upload]]</button>
|
||||
<div class="pull-right">
|
||||
<div class="btn-group">
|
||||
<button id="new-folder" class="btn-primary"><i class="fa fa-folder"></i> [[admin/manage/uploads:new-folder]]</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button id="upload" class="btn-success"><i class="fa fa-upload"></i> [[global:upload]]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -183,4 +183,20 @@ helpers.invite = async function (body, uid, jar, csrf_token) {
|
||||
return { res, body };
|
||||
};
|
||||
|
||||
helpers.createFolder = function (path, folderName, jar, csrf_token) {
|
||||
return requestAsync.put(`${nconf.get('url')}/api/v3/files/folder`, {
|
||||
jar,
|
||||
body: {
|
||||
path,
|
||||
folderName,
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'x-csrf-token': csrf_token,
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
};
|
||||
|
||||
require('../../src/promisify')(helpers);
|
||||
|
||||
@@ -5,6 +5,7 @@ const assert = require('assert');
|
||||
const nconf = require('nconf');
|
||||
const path = require('path');
|
||||
const request = require('request');
|
||||
const requestAsync = require('request-promise-native');
|
||||
|
||||
const db = require('./mocks/databasemock');
|
||||
const categories = require('../src/categories');
|
||||
@@ -372,14 +373,28 @@ describe('Upload Controllers', () => {
|
||||
describe('admin uploads', () => {
|
||||
let jar;
|
||||
let csrf_token;
|
||||
let regularJar;
|
||||
let regular_csrf_token;
|
||||
|
||||
before((done) => {
|
||||
async.parallel([
|
||||
function (next) {
|
||||
helpers.loginUser('admin', 'barbar', (err, _jar, _csrf_token) => {
|
||||
assert.ifError(err);
|
||||
jar = _jar;
|
||||
csrf_token = _csrf_token;
|
||||
done();
|
||||
next();
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
helpers.loginUser('regular', 'zugzug', (err, _jar, _csrf_token) => {
|
||||
assert.ifError(err);
|
||||
regularJar = _jar;
|
||||
regular_csrf_token = _csrf_token;
|
||||
next();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
it('should upload site logo', (done) => {
|
||||
@@ -490,5 +505,67 @@ describe('Upload Controllers', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ACP uploads screen', () => {
|
||||
it('should create a folder', async () => {
|
||||
const res = await helpers.createFolder('', 'myfolder', jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
assert(file.existsSync(path.join(nconf.get('upload_path'), 'myfolder')));
|
||||
});
|
||||
|
||||
it('should fail to create a folder if it already exists', async () => {
|
||||
const res = await helpers.createFolder('', 'myfolder', jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.deepStrictEqual(res.body.status, {
|
||||
code: 'forbidden',
|
||||
message: 'Folder exists',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create a folder as a non-admin', async () => {
|
||||
const res = await helpers.createFolder('', 'hisfolder', regularJar, regular_csrf_token);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.deepStrictEqual(res.body.status, {
|
||||
code: 'forbidden',
|
||||
message: 'You are not authorised to make this call',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create a folder in wrong directory', async () => {
|
||||
const res = await helpers.createFolder('../traversing', 'unexpectedfolder', jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.deepStrictEqual(res.body.status, {
|
||||
code: 'forbidden',
|
||||
message: 'Invalid path',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use basename of given folderName to create new folder', async () => {
|
||||
const res = await helpers.createFolder('/myfolder', '../another folder', jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
const slugifiedName = 'another-folder';
|
||||
assert(file.existsSync(path.join(nconf.get('upload_path'), 'myfolder', slugifiedName)));
|
||||
});
|
||||
|
||||
it('should fail to delete a file as a non-admin', async () => {
|
||||
const res = await requestAsync.delete(`${nconf.get('url')}/api/v3/files`, {
|
||||
body: {
|
||||
path: '/system/test.png',
|
||||
},
|
||||
jar: regularJar,
|
||||
json: true,
|
||||
headers: {
|
||||
'x-csrf-token': regular_csrf_token,
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.deepStrictEqual(res.body.status, {
|
||||
code: 'forbidden',
|
||||
message: 'You are not authorised to make this call',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user