mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-02-04 13:49:56 +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",
|
"orphaned": "Orphaned",
|
||||||
"size/filecount": "Size / Filecount",
|
"size/filecount": "Size / Filecount",
|
||||||
"confirm-delete": "Do you really want to delete this file?",
|
"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",
|
"invalid-event": "Invalid event: %1",
|
||||||
"local-login-disabled": "Local login system has been disabled for non-privileged accounts.",
|
"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",
|
"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",
|
"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'
|
$ref: 'write/admin/analytics/set.yaml'
|
||||||
/files/:
|
/files/:
|
||||||
$ref: 'write/files.yaml'
|
$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';
|
'use strict';
|
||||||
|
|
||||||
|
define('admin/manage/uploads', ['api', 'bootbox', 'uploader'], function (api, bootbox, uploader) {
|
||||||
define('admin/manage/uploads', ['uploader', 'api'], function (uploader, api) {
|
|
||||||
var Uploads = {};
|
var Uploads = {};
|
||||||
|
|
||||||
Uploads.init = function () {
|
Uploads.init = function () {
|
||||||
@@ -29,6 +28,21 @@ define('admin/manage/uploads', ['uploader', 'api'], function (uploader, api) {
|
|||||||
}).catch(app.alertError);
|
}).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;
|
return Uploads;
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ Files.delete = async (req, res) => {
|
|||||||
await fs.unlink(res.locals.cleanedPath);
|
await fs.unlink(res.locals.cleanedPath);
|
||||||
helpers.formatApiResponse(200, res);
|
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 groups = require('../groups');
|
||||||
const topics = require('../topics');
|
const topics = require('../topics');
|
||||||
const posts = require('../posts');
|
const posts = require('../posts');
|
||||||
|
const slugify = require('../slugify');
|
||||||
|
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
const controllerHelpers = require('../controllers/helpers');
|
const controllerHelpers = require('../controllers/helpers');
|
||||||
@@ -86,3 +87,20 @@ Assert.path = helpers.try(async (req, res, next) => {
|
|||||||
|
|
||||||
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;
|
const { setupApiRoute } = routeHelpers;
|
||||||
|
|
||||||
module.exports = function () {
|
module.exports = function () {
|
||||||
const middlewares = [middleware.ensureLoggedIn];
|
const middlewares = [middleware.ensureLoggedIn, middleware.admin.checkPrivileges];
|
||||||
|
|
||||||
// setupApiRoute(router, 'put', '/', [
|
// setupApiRoute(router, 'put', '/', [
|
||||||
// ...middlewares,
|
// ...middlewares,
|
||||||
@@ -21,5 +21,13 @@ module.exports = function () {
|
|||||||
middleware.assert.path,
|
middleware.assert.path,
|
||||||
], controllers.write.files.delete);
|
], 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;
|
return router;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
<!-- IMPORT partials/breadcrumbs.tpl -->
|
<!-- IMPORT partials/breadcrumbs.tpl -->
|
||||||
<div class="clearfix">
|
<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>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
|
|||||||
@@ -183,4 +183,20 @@ helpers.invite = async function (body, uid, jar, csrf_token) {
|
|||||||
return { res, body };
|
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);
|
require('../../src/promisify')(helpers);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const request = require('request');
|
const request = require('request');
|
||||||
|
const requestAsync = require('request-promise-native');
|
||||||
|
|
||||||
const db = require('./mocks/databasemock');
|
const db = require('./mocks/databasemock');
|
||||||
const categories = require('../src/categories');
|
const categories = require('../src/categories');
|
||||||
@@ -372,14 +373,28 @@ describe('Upload Controllers', () => {
|
|||||||
describe('admin uploads', () => {
|
describe('admin uploads', () => {
|
||||||
let jar;
|
let jar;
|
||||||
let csrf_token;
|
let csrf_token;
|
||||||
|
let regularJar;
|
||||||
|
let regular_csrf_token;
|
||||||
|
|
||||||
before((done) => {
|
before((done) => {
|
||||||
helpers.loginUser('admin', 'barbar', (err, _jar, _csrf_token) => {
|
async.parallel([
|
||||||
assert.ifError(err);
|
function (next) {
|
||||||
jar = _jar;
|
helpers.loginUser('admin', 'barbar', (err, _jar, _csrf_token) => {
|
||||||
csrf_token = _csrf_token;
|
assert.ifError(err);
|
||||||
done();
|
jar = _jar;
|
||||||
});
|
csrf_token = _csrf_token;
|
||||||
|
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) => {
|
it('should upload site logo', (done) => {
|
||||||
@@ -490,5 +505,67 @@ describe('Upload Controllers', () => {
|
|||||||
done();
|
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