From c7506d77b07c24d0d232828b7a3739d8b27d4268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Fri, 26 Jan 2018 18:56:17 -0500 Subject: [PATCH] closes #6247 --- .../language/en-GB/admin/manage/uploads.json | 7 ++ public/language/en-GB/admin/menu.json | 1 + public/src/admin/manage/uploads.js | 35 ++++++ src/controllers/admin/uploads.js | 113 ++++++++++++++++++ src/file.js | 2 +- src/pagination.js | 5 +- src/routes/admin.js | 3 + src/socket.io/admin.js | 13 ++ src/views/admin/manage/uploads.tpl | 39 ++++++ src/views/admin/partials/menu.tpl | 2 + 10 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 public/language/en-GB/admin/manage/uploads.json create mode 100644 public/src/admin/manage/uploads.js create mode 100644 src/views/admin/manage/uploads.tpl diff --git a/public/language/en-GB/admin/manage/uploads.json b/public/language/en-GB/admin/manage/uploads.json new file mode 100644 index 0000000000..13e69cafa7 --- /dev/null +++ b/public/language/en-GB/admin/manage/uploads.json @@ -0,0 +1,7 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json index 8f44bcd304..51099e9af4 100644 --- a/public/language/en-GB/admin/menu.json +++ b/public/language/en-GB/admin/menu.json @@ -17,6 +17,7 @@ "manage/post-queue": "Post Queue", "manage/groups": "Groups", "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", "section-settings": "Settings", "settings/general": "General", diff --git a/public/src/admin/manage/uploads.js b/public/src/admin/manage/uploads.js new file mode 100644 index 0000000000..98da2ef557 --- /dev/null +++ b/public/src/admin/manage/uploads.js @@ -0,0 +1,35 @@ +'use strict'; + + +define('admin/manage/uploads', ['uploader'], function (uploader) { + var Uploads = {}; + + Uploads.init = function () { + $('#upload').on('click', function () { + uploader.show({ + title: '[[admin/manage/uploads:upload-file]]', + route: config.relative_path + '/api/admin/upload/file', + params: { folder: ajaxify.data.currentFolder }, + }, function () { + ajaxify.refresh(); + }); + }); + + $('.delete').on('click', function () { + var file = $(this).parents('[data-path]'); + bootbox.confirm('[[admin/manage/uploads:confirm-delete]]', function (ok) { + if (!ok) { + return; + } + socket.emit('admin.uploads.delete', file.attr('data-path'), function (err) { + if (err) { + return app.alertError(err.message); + } + file.remove(); + }); + }); + }); + }; + + return Uploads; +}); diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index dc5bd7ce37..db26284205 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -4,16 +4,109 @@ var path = require('path'); var async = require('async'); var nconf = require('nconf'); var mime = require('mime'); +var fs = require('fs'); var meta = require('../../meta'); var file = require('../../file'); var image = require('../../image'); var plugins = require('../../plugins'); +var pagination = require('../../pagination'); var allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml']; var uploadsController = module.exports; +uploadsController.get = function (req, res, next) { + var currentFolder = path.join(nconf.get('upload_path'), req.query.dir || ''); + if (!currentFolder.startsWith(nconf.get('upload_path'))) { + return next(new Error('[[error:invalid-path]]')); + } + var itemsPerPage = 20; + var itemCount = 0; + var page = parseInt(req.query.page, 10) || 1; + async.waterfall([ + function (next) { + fs.readdir(currentFolder, next); + }, + function (files, next) { + files = files.filter(function (filename) { + return filename !== '.gitignore'; + }); + + itemCount = files.length; + var start = Math.max(0, (page - 1) * itemsPerPage); + var stop = start + itemsPerPage; + files = files.slice(start, stop); + + filesToData(currentFolder, files, next); + }, + function (files) { + files.sort(function (a, b) { + if (a.isDirectory && !b.isDirectory) { + return -1; + } else if (!a.isDirectory && b.isDirectory) { + return 1; + } + return 0; + }); + res.render('admin/manage/uploads', { + currentFolder: currentFolder.replace(nconf.get('upload_path'), ''), + files: files, + breadcrumbs: buildBreadcrumbs(currentFolder), + pagination: pagination.create(page, Math.ceil(itemCount / itemsPerPage), req.query), + }); + }, + ], next); +}; + +function buildBreadcrumbs(currentFolder) { + var crumbs = []; + var parts = currentFolder.replace(nconf.get('upload_path'), '').split(path.sep); + var currentPath = ''; + parts.forEach(function (part) { + var dir = path.join(currentPath, part); + crumbs.push({ + text: part || 'Uploads', + url: part ? '/admin/manage/uploads?dir=' + dir : '/admin/manage/uploads', + }); + currentPath = dir; + }); + + return crumbs; +} + +function filesToData(currentDir, files, callback) { + async.map(files, function (file, next) { + var stat; + async.waterfall([ + function (next) { + fs.stat(path.join(currentDir, file), next); + }, + function (_stat, next) { + stat = _stat; + if (stat.isDirectory()) { + fs.readdir(path.join(currentDir, file), next); + } else { + next(null, []); + } + }, + function (filesInDir, next) { + var url = nconf.get('upload_url') + currentDir.replace(nconf.get('upload_path'), '') + '/' + file; + next(null, { + name: file, + path: path.join(currentDir, file).replace(nconf.get('upload_path'), ''), + url: url, + fileCount: filesInDir.length - 1, // ignore .gitignore + size: stat.size, + sizeHumanReadable: (stat.size / 1024).toFixed(1) + 'KiB', + isDirectory: stat.isDirectory(), + isFile: stat.isFile(), + }); + }, + ], next); + }, callback); +} + uploadsController.uploadCategoryPicture = function (req, res, next) { var uploadedFile = req.files.files[0]; var params = null; @@ -110,6 +203,25 @@ uploadsController.uploadSound = function (req, res, next) { }); }; +uploadsController.uploadFile = function (req, res, next) { + var uploadedFile = req.files.files[0]; + var params; + try { + params = JSON.parse(req.body.params); + } catch (e) { + file.delete(uploadedFile.path); + return next(new Error('[[error:invalid-json]]')); + } + + file.saveFileToLocal(uploadedFile.name, params.folder, uploadedFile.path, function (err, data) { + file.delete(uploadedFile.path); + if (err) { + return next(err); + } + res.json([{ url: data.url }]); + }); +}; + uploadsController.uploadDefaultAvatar = function (req, res, next) { upload('avatar-default', req, res, next); }; @@ -173,3 +285,4 @@ function uploadImage(filename, folder, uploadedFile, req, res, next) { res.json([{ name: uploadedFile.name, url: image.url.startsWith('http') ? image.url : nconf.get('relative_path') + image.url }]); }); } + diff --git a/src/file.js b/src/file.js index 8027f7ad77..e31ae18399 100644 --- a/src/file.js +++ b/src/file.js @@ -81,7 +81,7 @@ file.saveFileToLocal = function (filename, folder, tempPath, callback) { } callback(null, { - url: '/assets/uploads/' + folder + '/' + filename, + url: '/assets/uploads/' + (folder ? folder + '/' : '') + filename, path: uploadPath, }); }); diff --git a/src/pagination.js b/src/pagination.js index 8405fafd50..5789573b92 100644 --- a/src/pagination.js +++ b/src/pagination.js @@ -3,7 +3,7 @@ var qs = require('querystring'); var _ = require('lodash'); -var pagination = {}; +var pagination = module.exports; pagination.create = function (currentPage, pageCount, queryObj) { if (pageCount <= 1) { @@ -76,6 +76,3 @@ pagination.create = function (currentPage, pageCount, queryObj) { } return data; }; - - -module.exports = pagination; diff --git a/src/routes/admin.js b/src/routes/admin.js index db0ce7798c..98008cfeec 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -17,6 +17,7 @@ function apiRoutes(router, middleware, controllers) { router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo); router.post('/uploadOgImage', middlewares, controllers.admin.uploads.uploadOgImage); router.post('/upload/sound', middlewares, controllers.admin.uploads.uploadSound); + router.post('/upload/file', middlewares, controllers.admin.uploads.uploadFile); router.post('/uploadDefaultAvatar', middlewares, controllers.admin.uploads.uploadDefaultAvatar); } @@ -77,6 +78,8 @@ function addRoutes(router, middleware, controllers) { router.get('/manage/groups', middlewares, controllers.admin.groups.list); router.get('/manage/groups/:name', middlewares, controllers.admin.groups.get); + router.get('/manage/uploads', middlewares, controllers.admin.uploads.get); + router.get('/settings/:term?', middlewares, controllers.admin.settings.get); router.get('/appearance/:term?', middlewares, controllers.admin.appearance.get); diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 628bbd97da..668dea0dbc 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -2,6 +2,9 @@ var async = require('async'); var winston = require('winston'); +var fs = require('fs'); +var path = require('path'); +var nconf = require('nconf'); var meta = require('../meta'); var plugins = require('../plugins'); @@ -37,6 +40,7 @@ var SocketAdmin = { analytics: {}, logs: {}, errors: {}, + uploads: {}, }; SocketAdmin.before = function (socket, method, data, next) { @@ -336,4 +340,13 @@ SocketAdmin.reloadAllSessions = function (socket, data, callback) { callback(); }; +SocketAdmin.uploads.delete = function (socket, pathToFile, callback) { + pathToFile = path.join(nconf.get('upload_path'), pathToFile); + if (!pathToFile.startsWith(nconf.get('upload_path'))) { + return callback(new Error('[[error:invalid-path]]')); + } + + fs.unlink(pathToFile, callback); +}; + module.exports = SocketAdmin; diff --git a/src/views/admin/manage/uploads.tpl b/src/views/admin/manage/uploads.tpl new file mode 100644 index 0000000000..0af5bd43c8 --- /dev/null +++ b/src/views/admin/manage/uploads.tpl @@ -0,0 +1,39 @@ +<!-- 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> + +<div class="table-responsive"> + <table class="table table-striped users-table"> + <thead> + <tr> + <th>[[admin/manage/uploads:filename]]</th> + <th class="text-right">[[admin/manage/uploads:size/filecount]]</th> + <th></th> + </tr> + </thead> + <tbody> + <!-- BEGIN files --> + <tr data-path="{files.path}"> + <!-- IF files.isDirectory --> + <td class="col-md-9" role="button"> + <i class="fa fa-fw fa-folder-o"></i> <a href="{config.relative}/admin/manage/uploads?dir={files.path}">{files.name}</a> + </td> + <!-- ENDIF files.isDirectory --> + + <!-- IF files.isFile --> + <td class="col-md-9"> + <i class="fa fa-fw fa-file-text-o"></i> <a href="{config.relative_path}{files.url}" target="_blank">{files.name}</a> + </td> + <!-- ENDIF files.isFile --> + + <td class="col-md-2 text-right"><!-- IF files.size -->{files.sizeHumanReadable}<!-- ELSE -->[[admin/manage/uploads:filecount, {files.fileCount}]]<!-- ENDIF files.size --></td> + + <td role="button" class="col-md-1 text-right"><i class="delete fa fa-fw fa-trash-o <!-- IF !files.isFile --> hidden<!-- ENDIF !files.isFile -->"></i></td> + </tr> + <!-- END files --> + </tbody> + </table> +</div> + +<!-- IMPORT partials/paginator.tpl --> \ No newline at end of file diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 87d9bfeecb..24fe0becb4 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -23,6 +23,7 @@ <li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li> <li><a href="{relative_path}/admin/manage/post-queue">[[admin/menu:manage/post-queue]]</a></li> <li><a href="{relative_path}/admin/manage/ip-blacklist">[[admin/menu:manage/ip-blacklist]]</a></li> + <li><a href="{relative_path}/admin/manage/uploads">[[admin/menu:manage/uploads]]</a></li> </ul> </section> @@ -198,6 +199,7 @@ <li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li> <li><a href="{relative_path}/admin/manage/post-queue">[[admin/menu:manage/post-queue]]</a></li> <li><a href="{relative_path}/admin/manage/ip-blacklist">[[admin/menu:manage/ip-blacklist]]</a></li> + <li><a href="{relative_path}/admin/manage/uploads">[[admin/menu:manage/uploads]]</a></li> </ul> </li> <li class="dropdown menu-item">