From 264bd88e88b9c5596c41cd2f095426e9b3d8ce06 Mon Sep 17 00:00:00 2001 From: Bobby Wibowo Date: Tue, 10 Sep 2019 23:31:27 +0700 Subject: [PATCH] Updated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improved performance of /api/users/:id (admin's manage users). Promisify fs.writeFile. Improved performance of /api/stats. By a lot in Linux, cause uploads size will be deferred to "du" binary. In addition, total usage of whichever disk uploads path resides on will also be queried using "df" binary. Non-Linux will have to rely on manual calculation by querying DB for each upload's size. But logics related to uploads stats were also improved to be almost twice as fast as before. Improved parsing of /api/stats results on dashboard.js. This allows ease of extending server's response by not having to update dashboard.js by much, if at all. Improved codes relating to item menus in dashboard's sidebar. Finally much cleaner now 👍 No longer use /api/upload/delete API route from dashboard. Single file deletion and bulk files deletion, both from uploads list or by names, will now properly use a single function that will use /api/upload/bulkdelete API route. /api/upload/delete will still be kept indefinitely for backward support. Fixed oddities with Select all checkbox. Replaced all instances of modifying HTML element's style attribute with adding/removing is-hidden CSS helper class. Rephrased all instances of "files" to "uploads" in any display strings. Fixed notice message when server is on private mode. A few other improvements. --- controllers/authController.js | 27 +- controllers/pathsController.js | 3 +- controllers/uploadController.js | 7 +- controllers/utilsController.js | 401 +++++++++++++------- public/css/dashboard.css | 6 - public/js/dashboard.js | 648 ++++++++++++++++---------------- public/js/home.js | 54 +-- views/_globals.njk | 2 +- views/dashboard.njk | 20 +- views/home.njk | 26 +- 10 files changed, 657 insertions(+), 537 deletions(-) diff --git a/controllers/authController.js b/controllers/authController.js index e99c128..ec2dff0 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -205,33 +205,22 @@ self.listUsers = async (req, res, next) => { .offset(25 * offset) .select('id', 'username', 'enabled', 'permission') - const userids = [] - + const pointers = {} for (const user of users) { user.groups = perms.mapPermissions(user) delete user.permission - - userids.push(user.id) - user.uploadsCount = 0 - user.diskUsage = 0 + user.uploads = 0 + user.usage = 0 + pointers[user.id] = user } - const maps = {} const uploads = await db.table('files') - .whereIn('userid', userids) + .whereIn('userid', Object.keys(pointers)) + .select('userid', 'size') for (const upload of uploads) { - if (maps[upload.userid] === undefined) - maps[upload.userid] = { count: 0, size: 0 } - - maps[upload.userid].count++ - maps[upload.userid].size += parseInt(upload.size) - } - - for (const user of users) { - if (!maps[user.id]) continue - user.uploadsCount = maps[user.id].count - user.diskUsage = maps[user.id].size + pointers[upload.userid].uploads++ + pointers[upload.userid].usage += parseInt(upload.size) } return res.json({ success: true, users, count }) diff --git a/controllers/pathsController.js b/controllers/pathsController.js index cd0eb75..700e669 100644 --- a/controllers/pathsController.js +++ b/controllers/pathsController.js @@ -16,7 +16,8 @@ const fsFuncs = [ 'rename', 'rmdir', 'symlink', - 'unlink' + 'unlink', + 'writeFile' ] for (const fsFunc of fsFuncs) diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 1ad846e..9073270 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -312,12 +312,7 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => { const name = await self.getUniqueRandomName(length, extname) const destination = path.join(paths.uploads, name) - await new Promise((resolve, reject) => { - fs.writeFile(destination, file, error => { - if (error) return reject(error) - return resolve() - }) - }) + await paths.writeFile(destination, file) downloaded.push(destination) infoMap.push({ diff --git a/controllers/utilsController.js b/controllers/utilsController.js index 0d55701..775c02e 100644 --- a/controllers/utilsController.js +++ b/controllers/utilsController.js @@ -1,4 +1,5 @@ const { promisify } = require('util') +const { spawn } = require('child_process') const config = require('./../config') const db = require('knex')(config.database) const fetch = require('node-fetch') @@ -33,6 +34,10 @@ const statsCache = { cache: null, generating: false }, + disk: { + cache: null, + generating: false + }, albums: { cache: null, generating: false, @@ -500,153 +505,287 @@ self.stats = async (req, res, next) => { const isadmin = perms.is(user, 'admin') if (!isadmin) return res.status(403).end() - const stats = {} - - // Re-use caches as long as they are still valid - - if (!statsCache.system.cache && statsCache.system.generating) { - stats.system = false - } else if (statsCache.system.generating) { - stats.system = statsCache.system.cache - } else { - statsCache.system.generating = true - + try { + const stats = {} const os = await si.osInfo() - const currentLoad = await si.currentLoad() - const mem = await si.mem() - stats.system = { - platform: `${os.platform} ${os.arch}`, - distro: `${os.distro} ${os.release}`, - kernel: os.kernel, - cpuLoad: `${currentLoad.currentload.toFixed(1)}%`, - cpusLoad: currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '), - systemMemory: { - used: mem.active, - total: mem.total - }, - memoryUsage: process.memoryUsage().rss, - nodeVersion: `${process.versions.node}` + // System info + if (!statsCache.system.cache && statsCache.system.generating) { + stats.system = false + } else if (statsCache.system.generating) { + stats.system = statsCache.system.cache + } else { + statsCache.system.generating = true + + const currentLoad = await si.currentLoad() + const mem = await si.mem() + + stats.system = { + _types: { + byte: ['memoryUsage'], + byteUsage: ['systemMemory'] + }, + platform: `${os.platform} ${os.arch}`, + distro: `${os.distro} ${os.release}`, + kernel: os.kernel, + cpuLoad: `${currentLoad.currentload.toFixed(1)}%`, + cpusLoad: currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '), + systemMemory: { + used: mem.active, + total: mem.total + }, + memoryUsage: process.memoryUsage().rss, + nodeVersion: `${process.versions.node}` + } + + // Update cache + statsCache.system.cache = stats.system + statsCache.system.generating = false } - // Update cache - statsCache.system.cache = stats.system - statsCache.system.generating = false - } + // Disk usage, only for Linux platform + if (os.platform === 'linux') + if (!statsCache.disk.cache && statsCache.disk.generating) { + stats.disk = false + } else if (statsCache.disk.generating) { + stats.disk = statsCache.disk.cache + } else { + statsCache.disk.generating = true - if (!statsCache.albums.cache && statsCache.albums.generating) { - stats.albums = false - } else if ((statsCache.albums.invalidatedAt < statsCache.albums.generatedAt) || statsCache.albums.generating) { - stats.albums = statsCache.albums.cache - } else { - statsCache.albums.generating = true - stats.albums = { - total: 0, - active: 0, - downloadable: 0, - public: 0, - zips: 0 + // We pre-assign the keys below to guarantee their order + stats.disk = { + _types: { + byte: ['uploads', 'thumbs', 'zips', 'chunks'], + byteUsage: ['drive'] + }, + drive: null, + uploads: 0, + thumbs: 0, + zips: 0, + chunks: 0 + } + + // Get size of directories in uploads path + await new Promise((resolve, reject) => { + const proc = spawn('du', [ + '--apparent-size', + '--block-size=1', + '--dereference', + '--separate-dirs', + paths.uploads + ]) + + proc.stdout.on('data', data => { + const formatted = String(data) + .trim() + .split(/\s+/) + if (formatted.length !== 2) return + + const basename = path.basename(formatted[1]) + stats.disk[basename] = parseInt(formatted[0]) + + // Add to types if necessary + if (!stats.disk._types.byte.includes(basename)) + stats.disk._types.byte.push(basename) + }) + + const stderr = [] + proc.stderr.on('data', data => stderr.push(data)) + + proc.on('exit', code => { + if (code !== 0) return reject(stderr) + resolve() + }) + }) + + // Get disk usage of whichever disk uploads path resides on + await new Promise((resolve, reject) => { + const proc = spawn('df', [ + '--block-size=1', + '--output=used,size', + paths.uploads + ]) + + proc.stdout.on('data', data => { + // Only use the first valid line + if (stats.disk.drive !== null) return + + const lines = String(data) + .trim() + .split('\n') + if (lines.length !== 2) return + + for (const line of lines) { + const columns = line.split(/\s+/) + // Skip lines that have non-number chars + if (columns.some(w => !/^\d+$/.test(w))) continue + + stats.disk.drive = { + used: parseInt(columns[0]), + total: parseInt(columns[1]) + } + } + }) + + const stderr = [] + proc.stderr.on('data', data => stderr.push(data)) + + proc.on('exit', code => { + if (code !== 0) return reject(stderr) + resolve() + }) + }) + + // Update cache + statsCache.disk.cache = stats.system + statsCache.disk.generating = false + } + + // Uploads + if (!statsCache.uploads.cache && statsCache.uploads.generating) { + stats.uploads = false + } else if ((statsCache.uploads.invalidatedAt < statsCache.uploads.generatedAt) || statsCache.uploads.generating) { + stats.uploads = statsCache.uploads.cache + } else { + statsCache.uploads.generating = true + stats.uploads = { + _types: { + number: ['total', 'images', 'videos', 'others'] + }, + total: 0, + images: 0, + videos: 0, + others: 0 + } + + if (os.platform !== 'linux') { + // If not Linux platform, rely on DB for total size + const uploads = await db.table('files') + .select('size') + stats.uploads.total = uploads.length + stats.uploads.sizeInDb = uploads.reduce((acc, upload) => acc + parseInt(upload.size), 0) + // Add type information for the new column + if (!Array.isArray(stats.uploads._types.byte)) + stats.uploads._types.byte = [] + stats.uploads._types.byte.push('sizeInDb') + } else { + stats.uploads.total = await db.table('files') + .count('id as count') + .then(rows => rows[0].count) + } + + stats.uploads.images = await db.table('files') + .whereRaw(self.imageExts.map(ext => `\`name\` like '%${ext}'`).join(' or ')) + .count('id as count') + .then(rows => rows[0].count) + + stats.uploads.videos = await db.table('files') + .whereRaw(self.videoExts.map(ext => `\`name\` like '%${ext}'`).join(' or ')) + .count('id as count') + .then(rows => rows[0].count) + + stats.uploads.others = stats.uploads.total - stats.uploads.images - stats.uploads.videos + + // Update cache + statsCache.uploads.cache = stats.uploads + statsCache.uploads.generatedAt = Date.now() + statsCache.uploads.generating = false } - const albums = await db.table('albums') - stats.albums.total = albums.length - const identifiers = [] - for (const album of albums) - if (album.enabled) { - stats.albums.active++ + // Users + if (!statsCache.users.cache && statsCache.users.generating) { + stats.users = false + } else if ((statsCache.users.invalidatedAt < statsCache.users.generatedAt) || statsCache.users.generating) { + stats.users = statsCache.users.cache + } else { + statsCache.users.generating = true + stats.users = { + _types: { + number: ['total', 'disabled'] + }, + total: 0, + disabled: 0 + } + + const permissionKeys = Object.keys(perms.permissions).reverse() + permissionKeys.forEach(p => { + stats.users[p] = 0 + stats.users._types.number.push(p) + }) + + const users = await db.table('users') + stats.users.total = users.length + for (const user of users) { + if (user.enabled === false || user.enabled === 0) + stats.users.disabled++ + + // This may be inaccurate on installations with customized permissions + user.permission = user.permission || 0 + for (const p of permissionKeys) + if (user.permission === perms.permissions[p]) { + stats.users[p]++ + break + } + } + + // Update cache + statsCache.users.cache = stats.users + statsCache.users.generatedAt = Date.now() + statsCache.users.generating = false + } + + // Albums + if (!statsCache.albums.cache && statsCache.albums.generating) { + stats.albums = false + } else if ((statsCache.albums.invalidatedAt < statsCache.albums.generatedAt) || statsCache.albums.generating) { + stats.albums = statsCache.albums.cache + } else { + statsCache.albums.generating = true + stats.albums = { + _types: { + number: ['total', 'active', 'downloadable', 'public', 'generatedZip'] + }, + total: 0, + disabled: 0, + public: 0, + downloadable: 0, + zipGenerated: 0 + } + + const albums = await db.table('albums') + stats.albums.total = albums.length + const identifiers = [] + for (const album of albums) { + if (!album.enabled) { + stats.albums.disabled++ + continue + } if (album.download) stats.albums.downloadable++ if (album.public) stats.albums.public++ if (album.zipGeneratedAt) identifiers.push(album.identifier) } - const zipsDir = path.join(paths.uploads, 'zips') - await Promise.all(identifiers.map(identifier => { - return new Promise(resolve => { - const filePath = path.join(zipsDir, `${identifier}.zip`) - fs.access(filePath, error => { - if (!error) stats.albums.zips++ - resolve(true) - }) - }) - })) - - // Update cache - statsCache.albums.cache = stats.albums - statsCache.albums.generatedAt = Date.now() - statsCache.albums.generating = false - } - - if (!statsCache.users.cache && statsCache.users.generating) { - stats.users = false - } else if ((statsCache.users.invalidatedAt < statsCache.users.generatedAt) || statsCache.users.generating) { - stats.users = statsCache.users.cache - } else { - statsCache.users.generating = true - stats.users = { - total: 0, - disabled: 0 - } - - const permissionKeys = Object.keys(perms.permissions) - permissionKeys.forEach(p => { - stats.users[p] = 0 - }) - - const users = await db.table('users') - stats.users.total = users.length - for (const user of users) { - if (user.enabled === false || user.enabled === 0) - stats.users.disabled++ - - // This may be inaccurate on installations with customized permissions - user.permission = user.permission || 0 - for (const p of permissionKeys) - if (user.permission === perms.permissions[p]) { - stats.users[p]++ - break + for (const identifier of identifiers) + try { + await paths.access(path.join(paths.zips, `${identifier}.zip`)) + stats.albums.zipGenerated++ + } catch (error) { + // Re-throw error + if (error.code !== 'ENOENT') + throw error } + + // Update cache + statsCache.albums.cache = stats.albums + statsCache.albums.generatedAt = Date.now() + statsCache.albums.generating = false } - // Update cache - statsCache.users.cache = stats.users - statsCache.users.generatedAt = Date.now() - statsCache.users.generating = false + return res.json({ success: true, stats }) + } catch (error) { + logger.error(error) + return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' }) } - - if (!statsCache.uploads.cache && statsCache.uploads.generating) { - stats.uploads = false - } else if ((statsCache.uploads.invalidatedAt < statsCache.uploads.generatedAt) || statsCache.uploads.generating) { - stats.uploads = statsCache.uploads.cache - } else { - statsCache.uploads.generating = true - stats.uploads = { - total: 0, - size: 0, - images: 0, - videos: 0, - others: 0 - } - - const uploads = await db.table('files') - stats.uploads.total = uploads.length - for (const upload of uploads) { - stats.uploads.size += parseInt(upload.size) - const extname = self.extname(upload.name) - if (self.imageExts.includes(extname)) - stats.uploads.images++ - else if (self.videoExts.includes(extname)) - stats.uploads.videos++ - else - stats.uploads.others++ - } - - // Update cache - statsCache.uploads.cache = stats.uploads - statsCache.uploads.generatedAt = Date.now() - statsCache.uploads.generating = false - } - - return res.json({ success: true, stats }) } module.exports = self diff --git a/public/css/dashboard.css b/public/css/dashboard.css index 21e3bb9..8868066 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -3,9 +3,7 @@ body { animation: none; } -#auth, #dashboard { - display: none; -webkit-animation: fadeInOpacity .5s; animation: fadeInOpacity .5s; } @@ -102,15 +100,11 @@ li[data-action="page-ellipsis"] { .no-touch .image-container .checkbox { opacity: .5; - -webkit-transition: opacity .25s; - transition: opacity .25s; } .no-touch .image-container .controls, .no-touch .image-container .details { opacity: 0; - -webkit-transition: opacity .25s; - transition: opacity .25s; } .no-touch .image-container:hover .checkbox, diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 5ca8d21..e517a29 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -24,6 +24,9 @@ const page = { username: null, permissions: null, + // sidebar menus + menus: [], + currentView: null, views: { // config of uploads view @@ -75,7 +78,8 @@ const page = { clipboardJS: null, lazyLoad: null, - imageExtensions: ['.webp', '.jpg', '.jpeg', '.bmp', '.gif', '.png'], + imageExts: ['.webp', '.jpg', '.jpeg', '.gif', '.png', '.tiff', '.tif', '.svg'], + videoExts: ['.webm', '.mp4', '.wmv', '.avi', '.mov', '.mkv'], fadingIn: null } @@ -115,77 +119,64 @@ page.verifyToken = function (token, reloadOnError) { page.prepareDashboard = function () { page.dom = document.querySelector('#page') + + // Capture all click events page.dom.addEventListener('click', page.domClick, true) + // Capture all submit events page.dom.addEventListener('submit', function (event) { - const element = event.target - if (element && element.classList.contains('prevent-default')) + // Prevent default if necessary + if (event.target && event.target.classList.contains('prevent-default')) return event.preventDefault() }, true) - document.querySelector('#dashboard').style.display = 'block' + // All item menus in the sidebar + const itemMenus = [ + { selector: '#itemUploads', onclick: page.getUploads }, + { selector: '#itemDeleteUploadsByNames', onclick: page.deleteUploadsByNames }, + { selector: '#itemManageAlbums', onclick: page.getAlbums }, + { selector: '#itemManageToken', onclick: page.changeToken }, + { selector: '#itemChangePassword', onclick: page.changePassword }, + { selector: '#itemLogout', onclick: page.logout, inactive: true }, + { selector: '#itemManageUploads', onclick: page.getUploads, params: [{ all: true }], group: 'moderator' }, + { selector: '#itemStatistics', onclick: page.getStatistics, group: 'admin' }, + { selector: '#itemManageUsers', onclick: page.getUsers, group: 'admin' } + ] + for (let i = 0; i < itemMenus.length; i++) { + // Skip item menu if not enough permission + if (itemMenus[i].group && !page.permissions[itemMenus[i].group]) + continue + + // Add onclick event listener + const item = document.querySelector(itemMenus[i].selector) + item.addEventListener('click', function () { + itemMenus[i].onclick.apply(null, itemMenus[i].params) + if (!itemMenus[i].inactive) + page.setActiveMenu(this) + }) + + item.classList.remove('is-hidden') + page.menus.push(item) + } + + // If at least a moderator, show administration section if (page.permissions.moderator) { - document.querySelector('#itemLabelAdmin').style.display = 'block' - document.querySelector('#itemListAdmin').style.display = 'block' - const itemManageUploads = document.querySelector('#itemManageUploads') - itemManageUploads.addEventListener('click', function () { - page.setActiveMenu(this) - page.getUploads({ all: true }) - }) + document.querySelector('#itemLabelAdmin').classList.remove('is-hidden') + document.querySelector('#itemListAdmin').classList.remove('is-hidden') } - if (page.permissions.admin) { - const itemServerStats = document.querySelector('#itemServerStats') - itemServerStats.addEventListener('click', function () { - page.setActiveMenu(this) - page.getServerStats() - }) + // Update text of logout button + document.querySelector('#itemLogout').innerHTML = `Logout ( ${page.username} )` - const itemManageUsers = document.querySelector('#itemManageUsers') - itemManageUsers.addEventListener('click', function () { - page.setActiveMenu(this) - page.getUsers() - }) - } else { - document.querySelector('#itemServerStats').style.display = 'none' - document.querySelector('#itemManageUsers').style.display = 'none' - } - - document.querySelector('#itemUploads').addEventListener('click', function () { - page.setActiveMenu(this) - page.getUploads({ all: false }) - }) - - document.querySelector('#itemDeleteByNames').addEventListener('click', function () { - page.setActiveMenu(this) - page.deleteByNames() - }) - - document.querySelector('#itemManageGallery').addEventListener('click', function () { - page.setActiveMenu(this) - page.getAlbums() - }) - - document.querySelector('#itemTokens').addEventListener('click', function () { - page.setActiveMenu(this) - page.changeToken() - }) - - document.querySelector('#itemPassword').addEventListener('click', function () { - page.setActiveMenu(this) - page.changePassword() - }) - - const logoutBtn = document.querySelector('#itemLogout') - logoutBtn.addEventListener('click', function () { - page.logout() - }) - logoutBtn.innerHTML = `Logout ( ${page.username} )` + // Finally display dashboard + document.querySelector('#dashboard').classList.remove('is-hidden') + // Load albums sidebar page.getAlbumsSidebar() - if (typeof page.prepareShareX === 'function') page.prepareShareX() + if (typeof page.prepareShareX === 'function') + page.prepareShareX() } page.logout = function () { @@ -234,22 +225,20 @@ page.domClick = function (event) { return page.setUploadsView('thumbs', element) case 'clear-selection': return page.clearSelection() - case 'add-selected-files-to-album': - return page.addSelectedFilesToAlbum() - case 'bulk-delete': - return page.deleteSelectedFiles() + case 'add-selected-uploads-to-album': + return page.addSelectedUploadsToAlbum() case 'select': return page.select(element, event) - case 'add-to-album': - return page.addSingleFileToAlbum(id) - case 'delete-file': - return page.deleteFile(id) case 'select-all': return page.selectAll(element) + case 'add-to-album': + return page.addToAlbum(id) + case 'delete-upload': + return page.deleteUpload(id) + case 'bulk-delete-uploads': + return page.bulkDeleteUploads() case 'display-thumbnail': return page.displayThumbnail(id) - case 'delete-file-by-names': - return page.deleteFileByNames() case 'submit-album': return page.submitAlbum(element) case 'edit-album': @@ -457,12 +446,12 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele ${all ? '' : ` - + `} - + @@ -472,8 +461,8 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele ` - // Set to true to tick "all files" checkbox in list view - let allSelected = true + // Whether there are any unselected items + let unselected = false const hasExpiryDateColumn = files.some(file => file.expirydate !== undefined) @@ -501,7 +490,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele // Update selected status files[i].selected = page.selected[page.currentView].includes(files[i].id) - if (allSelected && !files[i].selected) allSelected = false + if (!files[i].selected) unselected = true // Appendix (display album or user) if (all) @@ -539,7 +528,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele div.innerHTML = `

${upload.extname || 'N/A'}

` div.innerHTML += ` - +
@@ -556,7 +545,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele - + @@ -581,7 +570,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele - + @@ -606,7 +595,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele const tr = document.createElement('tr') tr.dataset.id = upload.id tr.innerHTML = ` - + @@ -630,7 +619,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele `} - + @@ -642,13 +631,15 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele page.checkboxes[page.currentView] = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) } } - page.fadeAndScroll() - if (allSelected && files.length) { - const selectAll = document.querySelector('#selectAll') - if (selectAll) selectAll.checked = true + const selectAll = document.querySelector('#selectAll') + if (selectAll && !unselected) { + selectAll.checked = true + selectAll.title = 'Unselect all' } + page.fadeAndScroll() + if (page.currentView === 'uploads') page.views.uploads.album = album if (page.currentView === 'uploadsAll') page.views.uploadsAll.filters = filters page.views[page.currentView].pageNum = files.length ? pageNum : 0 @@ -693,27 +684,28 @@ page.displayThumbnail = function (id) { const thumb = div.querySelector('#swalThumb') const exec = /.[\w]+(\?|$)/.exec(original) - const isimage = exec && exec[0] && page.imageExtensions.includes(exec[0].toLowerCase()) + if (!exec || !exec[0]) return - if (isimage) { + const extname = exec[0].toLowerCase() + if (page.imageExts.includes(extname)) { thumb.src = file.original thumb.onload = function () { - button.style.display = 'none' + button.classList.add('is-hidden') document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded') } thumb.onerror = function () { button.className = 'button is-danger' button.innerHTML = 'Unable to load original' } - } else { - thumb.style.display = 'none' + } else if (page.videoExts.includes(extname)) { + thumb.classList.add('is-hidden') const video = document.createElement('video') video.id = 'swalVideo' video.controls = true video.src = file.original thumb.insertAdjacentElement('afterend', video) - button.style.display = 'none' + button.classList.add('is-hidden') document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded') } }) @@ -732,68 +724,76 @@ page.displayThumbnail = function (id) { } page.selectAll = function (element) { - const checkboxes = page.checkboxes[page.currentView] - const selected = page.selected[page.currentView] - - for (let i = 0; i < checkboxes.length; i++) { - const id = page.getItemID(checkboxes[i]) + for (let i = 0; i < page.checkboxes[page.currentView].length; i++) { + const id = page.getItemID(page.checkboxes[page.currentView][i]) if (isNaN(id)) continue - if (checkboxes[i].checked !== element.checked) { - checkboxes[i].checked = element.checked - if (checkboxes[i].checked) - selected.push(id) + if (page.checkboxes[page.currentView][i].checked !== element.checked) { + page.checkboxes[page.currentView][i].checked = element.checked + if (page.checkboxes[page.currentView][i].checked) + page.selected[page.currentView].push(id) else - selected.splice(selected.indexOf(id), 1) + page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1) } } - localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(selected) - page.selected[page.currentView] = selected - element.title = element.checked ? 'Unselect all uploads' : 'Select all uploads' + if (page.selected[page.currentView].length) + localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView]) + else + delete localStorage[lsKeys.selected[page.currentView]] + + element.title = element.checked ? 'Unselect all' : 'Select all' } page.selectInBetween = function (element, lastElement) { - if (!element || !lastElement) return - if (element === lastElement) return + if (!element || !lastElement || element === lastElement) + return - const checkboxes = page.checkboxes[page.currentView] - if (!checkboxes || !checkboxes.length) return + if (!Array.isArray(page.checkboxes[page.currentView]) || !page.checkboxes[page.currentView].length) + return - const thisIndex = checkboxes.indexOf(element) - const lastIndex = checkboxes.indexOf(lastElement) + const thisIndex = page.checkboxes[page.currentView].indexOf(element) + const lastIndex = page.checkboxes[page.currentView].indexOf(lastElement) const distance = thisIndex - lastIndex - if (distance >= -1 && distance <= 1) return + if (distance >= -1 && distance <= 1) + return - for (let i = 0; i < checkboxes.length; i++) + for (let i = 0; i < page.checkboxes[page.currentView].length; i++) if ((thisIndex > lastIndex && i > lastIndex && i < thisIndex) || (thisIndex < lastIndex && i > thisIndex && i < lastIndex)) { - checkboxes[i].checked = true - page.selected[page.currentView].push(page.getItemID(checkboxes[i])) + // Check or uncheck depending on the state of the initial checkbox + page.checkboxes[page.currentView][i].checked = lastElement.checked + const id = page.getItemID(page.checkboxes[page.currentView][i]) + if (!page.selected[page.currentView].includes(id) && page.checkboxes[page.currentView][i].checked) + page.selected[page.currentView].push(id) + else if (page.selected[page.currentView].includes(id) && !page.checkboxes[page.currentView][i].checked) + page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1) } - - localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView]) - page.checkboxes[page.currentView] = checkboxes } page.select = function (element, event) { - const lastSelected = page.lastSelected[page.currentView] - if (event.shiftKey && lastSelected) - page.selectInBetween(element, lastSelected) - else - page.lastSelected[page.currentView] = element - const id = page.getItemID(element) if (isNaN(id)) return - const selected = page.selected[page.currentView] - if (!selected.includes(id) && element.checked) - selected.push(id) - else if (selected.includes(id) && !element.checked) - selected.splice(selected.indexOf(id), 1) + const lastSelected = page.lastSelected[page.currentView] + if (event.shiftKey && lastSelected) { + page.selectInBetween(element, lastSelected) + // Check or uncheck depending on the state of the initial checkbox + element.checked = lastSelected.checked + } else { + page.lastSelected[page.currentView] = element + } - localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(selected) - page.selected[page.currentView] = selected + if (!page.selected[page.currentView].includes(id) && element.checked) + page.selected[page.currentView].push(id) + else if (page.selected[page.currentView].includes(id) && !element.checked) + page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1) + + // Update local storage + if (page.selected[page.currentView].length) + localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView]) + else + delete localStorage[lsKeys.selected[page.currentView]] } page.clearSelection = function () { @@ -830,7 +830,7 @@ page.filtersHelp = function (element) { const content = document.createElement('div') content.style = 'text-align: left' content.innerHTML = ` - This supports 3 filter keys, namely user (username), ip and name (file name). + This supports 3 filter keys, namely user (username), ip and name (upload name). Each key can be specified more than once. Backlashes should be used if the usernames have spaces. There are also 2 additional flags, namely -user and -ip, which will match uploads by non-registered users and have no IPs respectively. @@ -847,7 +847,7 @@ page.filtersHelp = function (element) { Uploads from users with username either "John Doe" OR "demo": user:John\\ Doe user:demo - Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip": + Uploads from IP "127.0.0.1" AND which upload names match "*.rar" OR "*.zip": ip:127.0.0.1 name:*.rar name:*.zip Uploads from user with username "test" OR from non-registered users: @@ -864,56 +864,133 @@ page.filterUploads = function (element) { page.viewUserUploads = function (id) { const user = page.cache.users[id] if (!user) return - page.setActiveMenu(document.querySelector('#itemManageUploads')) page.getUploads({ all: true, filters: `user:${user.username.replace(/ /g, '\\ ')}` }) + page.setActiveMenu(document.querySelector('#itemManageUploads')) } -page.deleteFile = function (id) { - // TODO: Share function with bulk delete, just like 'add selected uploads to album' and 'add single file to album' - swal({ - title: 'Are you sure?', - text: 'You won\'t be able to recover the file!', - icon: 'warning', - dangerMode: true, - buttons: { - cancel: true, - confirm: { - text: 'Yes, delete it!', - closeModal: false - } - } - }).then(function (proceed) { - if (!proceed) return +page.deleteUpload = function (id) { + page.postBulkDeleteUploads({ + field: 'id', + values: [id], + cb (failed) { + // Remove from remembered checkboxes if necessary + if (!failed.length && page.selected[page.currentView].includes(id)) + page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1) - axios.post('api/upload/delete', { id }).then(function (response) { - if (!response) return - - if (response.data.success === false) - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } - - swal('Deleted!', 'The file has been deleted.', 'success') + // Update local storage + if (page.selected[page.currentView].length) + localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView]) + else + delete localStorage[lsKeys.selected[page.currentView]] + // Reload upload list const views = Object.assign({}, page.views[page.currentView]) views.autoPage = true page.getUploads(views) - }).catch(function (error) { - console.error(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + } }) } -page.deleteSelectedFiles = function () { +page.bulkDeleteUploads = function () { const count = page.selected[page.currentView].length if (!count) return swal('An error occurred!', 'You have not selected any uploads.', 'error') - const suffix = `upload${count === 1 ? '' : 's'}` - let text = `You won't be able to recover ${count} ${suffix}!` + page.postBulkDeleteUploads({ + field: 'id', + values: page.selected[page.currentView], + cb (failed) { + // Update state of checkboxes + if (failed.length) + page.selected[page.currentView] = page.selected[page.currentView] + .filter(function (id) { + return failed.includes(id) + }) + else + page.selected[page.currentView] = [] + + // Update local storage + if (page.selected[page.currentView].length) + localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView]) + else + delete localStorage[lsKeys.selected[page.currentView]] + + // Reload uploads list + const views = Object.assign({}, page.views[page.currentView]) + views.autoPage = true + page.getUploads(views) + } + }) +} + +page.deleteUploadsByNames = function () { + let appendix = '' + if (page.permissions.moderator) + appendix = '
As a staff, you can use this feature to delete uploads from other users.' + + page.dom.innerHTML = ` +
+
+ +
+ +
+

Separate each entry with a new line.${appendix}

+
+
+
+ +
+
+ + ` + page.fadeAndScroll() + + document.querySelector('#submitBulkDelete').addEventListener('click', function () { + const textArea = document.querySelector('#bulkDeleteNames') + + // Clean up + const seen = {} + const names = textArea.value + .split(/\r?\n/) + .map(function (name) { + const trimmed = name.trim() + return /^[^\s]+$/.test(trimmed) + ? trimmed + : '' + }) + .filter(function (name) { + // Filter out invalid and duplicate names + return (!name || Object.prototype.hasOwnProperty.call(seen, name)) + ? false + : (seen[name] = true) + }) + + // Update textarea with cleaned names + textArea.value = names.join('\n') + + if (!names.length) + return swal('An error occurred!', 'You have not entered any upload names.', 'error') + + page.postBulkDeleteUploads({ + field: 'name', + values: names, + cb (failed) { + textArea.value = failed.join('\n') + } + }) + }) +} + +page.postBulkDeleteUploads = function ({ field, values, cb } = {}) { + const count = values.length + const objective = `${values.length} upload${count === 1 ? '' : 's'}` + let text = `You won't be able to recover ${objective}!` if (page.currentView === 'uploadsAll') text += '\nBe aware, you may be nuking uploads by other users!' @@ -925,41 +1002,32 @@ page.deleteSelectedFiles = function () { buttons: { cancel: true, confirm: { - text: `Yes, nuke the ${suffix}!`, + text: `Yes, nuke ${values.length === 1 ? 'it' : 'them'}!`, closeModal: false } } }).then(function (proceed) { if (!proceed) return - axios.post('api/upload/bulkdelete', { - field: 'id', - values: page.selected[page.currentView] - }).then(function (bulkdelete) { - if (!bulkdelete) return + axios.post('api/upload/bulkdelete', { field, values }).then(function (response) { + if (!response) return - if (bulkdelete.data.success === false) - if (bulkdelete.data.description === 'No token provided') { + if (response.data.success === false) + if (response.data.description === 'No token provided') { return page.verifyToken(page.token) } else { - return swal('An error occurred!', bulkdelete.data.description, 'error') + return swal('An error occurred!', response.data.description, 'error') } - if (Array.isArray(bulkdelete.data.failed) && bulkdelete.data.failed.length) { - page.selected[page.currentView] = page.selected[page.currentView].filter(function (id) { - return bulkdelete.data.failed.includes(id) - }) - localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView]) - swal('An error ocurrred!', `From ${count} ${suffix}, unable to delete ${bulkdelete.data.failed.length} of them.`, 'error') - } else { - page.selected[page.currentView] = [] - delete localStorage[lsKeys.selected[page.currentView]] - swal('Deleted!', `${count} ${suffix} ${count === 1 ? 'has' : 'have'} been deleted.`, 'success') - } + const failed = Array.isArray(response.data.failed) ? response.data.failed : [] + if (failed.length === values.length) + swal('An error occurred!', `Unable to delete any of the ${objective}.`, 'error') + else if (failed.length && failed.length < values.length) + swal('Warning!', `From ${objective}, unable to delete ${failed.length} of them.`, 'warning') + else + swal('Deleted!', `${objective} ${count === 1 ? 'has' : 'have'} been deleted.`, 'success') - const views = Object.assign({}, page.views[page.currentView]) - views.autoPage = true - page.getUploads(views) + if (typeof cb === 'function') cb(failed) }).catch(function (error) { console.error(error) swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') @@ -967,94 +1035,7 @@ page.deleteSelectedFiles = function () { }) } -page.deleteByNames = function () { - let appendix = '' - if (page.permissions.moderator) - appendix = '
As a staff, you can use this feature to delete uploads by other users.' - - page.dom.innerHTML = ` -

Delete by names

-
- -
- -
-

Separate each entry with a new line.${appendix}

-
-
- ` - page.fadeAndScroll() -} - -page.deleteFileByNames = function () { - const names = document.querySelector('#names').value - .split(/\r?\n/) - .filter(function (n) { - return n.trim().length - }) - const count = names.length - if (!count) - return swal('An error occurred!', 'You have not entered any file names.', 'error') - - const suffix = `file${count === 1 ? '' : 's'}` - swal({ - title: 'Are you sure?', - text: `You won't be able to recover ${count} ${suffix}!`, - icon: 'warning', - dangerMode: true, - buttons: { - cancel: true, - confirm: { - text: `Yes, nuke the ${suffix}!`, - closeModal: false - } - } - }).then(function (proceed) { - if (!proceed) return - - axios.post('api/upload/bulkdelete', { - field: 'name', - values: names - }).then(function (bulkdelete) { - if (!bulkdelete) return - - if (bulkdelete.data.success === false) - if (bulkdelete.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', bulkdelete.data.description, 'error') - } - - if (Array.isArray(bulkdelete.data.failed) && bulkdelete.data.failed.length) { - page.selected[page.currentView] = page.selected[page.currentView].filter(function (id) { - return bulkdelete.data.failed.includes(id) - }) - localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView]) - swal('An error ocurrred!', `From ${count} ${suffix}, unable to delete ${bulkdelete.data.failed.length} of them.`, 'error') - } else { - page.selected[page.currentView] = [] - delete localStorage[lsKeys.selected[page.currentView]] - swal('Deleted!', `${count} ${suffix} ${count === 1 ? 'has' : 'have'} been deleted.`, 'success') - } - - document.querySelector('#names').value = bulkdelete.data.failed.join('\n') - }).catch(function (error) { - console.error(error) - swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) - }) -} - -page.addSelectedFilesToAlbum = function () { +page.addSelectedUploadsToAlbum = function () { if (page.currentView !== 'uploads') return @@ -1062,7 +1043,7 @@ page.addSelectedFilesToAlbum = function () { if (!count) return swal('An error occurred!', 'You have not selected any uploads.', 'error') - page.addFilesToAlbum(page.selected[page.currentView], function (failed) { + page.addUploadsToAlbum(page.selected[page.currentView], function (failed) { if (!failed) return if (failed.length) page.selected[page.currentView] = page.selected[page.currentView].filter(function (id) { @@ -1076,21 +1057,21 @@ page.addSelectedFilesToAlbum = function () { }) } -page.addSingleFileToAlbum = function (id) { - page.addFilesToAlbum([id], function (failed) { +page.addToAlbum = function (id) { + page.addUploadsToAlbum([id], function (failed) { if (!failed) return page.getUploads(page.views[page.currentView]) }) } -page.addFilesToAlbum = function (ids, callback) { +page.addUploadsToAlbum = function (ids, callback) { const count = ids.length const content = document.createElement('div') content.innerHTML = `
-

You are about to add ${count} file${count === 1 ? '' : 's'} to an album.

-

If a file is already in an album, it will be moved.

+

You are about to add ${count} upload${count === 1 ? '' : 's'} to an album.

+

If an upload is already in an album, it will be moved.

@@ -1140,7 +1121,7 @@ page.addFilesToAlbum = function (ids, callback) { if (add.data.failed && add.data.failed.length) added -= add.data.failed.length - const suffix = `file${ids.length === 1 ? '' : 's'}` + const suffix = `upload${ids.length === 1 ? '' : 's'}` if (!added) return swal('An error occurred!', `Could not add the ${suffix} to the album.`, 'error') @@ -1469,9 +1450,17 @@ page.getAlbumsSidebar = function () { } const albumsContainer = document.querySelector('#albumsContainer') - albumsContainer.innerHTML = '' - if (response.data.albums === undefined) return + // Clear albums sidebar if necessary + const oldAlbums = albumsContainer.querySelectorAll('li > a') + if (oldAlbums.length) { + for (let i = 0; i < oldAlbums.length; i++) + page.menus.splice(page.menus.indexOf(oldAlbums[i]), 1) + albumsContainer.innerHTML = '' + } + + if (response.data.albums === undefined) + return for (let i = 0; i < response.data.albums.length; i++) { const album = response.data.albums[i] @@ -1481,8 +1470,10 @@ page.getAlbumsSidebar = function () { a.innerHTML = album.name a.addEventListener('click', function () { - page.getAlbum(this) + page.getUploads({ album: this.id }) + page.setActiveMenu(this) }) + page.menus.push(a) li.appendChild(a) albumsContainer.appendChild(li) @@ -1493,11 +1484,6 @@ page.getAlbumsSidebar = function () { }) } -page.getAlbum = function (album) { - page.setActiveMenu(album) - page.getUploads({ album: album.id }) -} - page.changeToken = function () { axios.get('api/tokens').then(function (response) { if (response.data.success === false) @@ -1508,7 +1494,6 @@ page.changeToken = function () { } page.dom.innerHTML = ` -

Manage your token

@@ -1567,7 +1552,6 @@ page.getNewToken = function (element) { page.changePassword = function () { page.dom.innerHTML = ` -

Change your password

@@ -1634,13 +1618,11 @@ page.sendNewPassword = function (pass, element) { }) } -page.setActiveMenu = function (activeItem) { - const menu = document.querySelector('#menu') - const items = menu.getElementsByTagName('a') - for (let i = 0; i < items.length; i++) - items[i].classList.remove('is-active') +page.setActiveMenu = function (element) { + for (let i = 0; i < page.menus.length; i++) + page.menus[i].classList.remove('is-active') - activeItem.classList.add('is-active') + element.classList.add('is-active') } page.getUsers = function ({ pageNum } = {}, element) { @@ -1717,7 +1699,8 @@ page.getUsers = function ({ pageNum } = {}, element) {
` - let allSelected = true + // Whether there are any unselected items + let unselected = false page.dom.innerHTML = ` ${pagination} @@ -1727,7 +1710,7 @@ page.getUsers = function ({ pageNum } = {}, element) {
File ${all ? 'User' : 'Album'} Size ${upload.name} ${upload.appendix} ${upload.prettyBytes}
- + @@ -1749,7 +1732,7 @@ page.getUsers = function ({ pageNum } = {}, element) { for (let i = 0; i < response.data.users.length; i++) { const user = response.data.users[i] const selected = page.selected.users.includes(user.id) - if (!selected && allSelected) allSelected = false + if (!selected) unselected = true let displayGroup = null const groups = Object.keys(user.groups) @@ -1770,11 +1753,11 @@ page.getUsers = function ({ pageNum } = {}, element) { const tr = document.createElement('tr') tr.dataset.id = user.id tr.innerHTML = ` - + ${user.username} - - + + - - + + ` } @@ -2084,11 +2079,16 @@ page.getServerStats = function (element) { ` } - page.dom.innerHTML = ` -

Statistics

- ${content} - ` + page.dom.innerHTML = content page.fadeAndScroll() + }).catch(function (error) { + console.error(error) + const description = error.response.data && error.response.data.description + ? error.response.data && error.response.data.description + : 'There was an error with the request, please check the console for more information.' + page.dom.innerHTML = `

${description}

` + page.fadeAndScroll() + return swal('An error occurred!', description, 'error') }) } diff --git a/public/js/home.js b/public/js/home.js index 06809a5..1ef55f8 100644 --- a/public/js/home.js +++ b/public/js/home.js @@ -55,11 +55,12 @@ page.checkIfPublic = function () { page.preparePage() }).catch(function (error) { console.error(error) - document.querySelector('#albumDiv').style.display = 'none' - document.querySelector('#tabs').style.display = 'none' + document.querySelector('#albumDiv').classList.add('is-hidden') + document.querySelector('#tabs').classList.add('is-hidden') const button = document.querySelector('#loginToUpload') - button.classList.remove('is-loading') button.innerText = 'Error occurred. Reload the page?' + button.classList.remove('is-loading') + button.classList.remove('is-hidden') return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } @@ -74,9 +75,9 @@ page.preparePage = function () { button.classList.remove('is-loading') if (page.enableUserAccounts) - button.innerText = 'Anonymous upload is disabled. Log in to page.' + button.innerText = 'Anonymous upload is disabled. Log in to upload.' else - button.innerText = 'Running in private mode. Log in to page.' + button.innerText = 'Running in private mode. Log in to upload.' } else return page.prepareUpload() @@ -120,13 +121,13 @@ page.prepareUpload = function () { page.prepareAlbums() // Display the album selection - document.querySelector('#albumDiv').style.display = 'flex' + document.querySelector('#albumDiv').classList.remove('is-hidden') } page.prepareUploadConfig() document.querySelector('#maxSize').innerHTML = `Maximum upload size per file is ${page.getPrettyBytes(page.maxSizeBytes)}` - document.querySelector('#loginToUpload').style.display = 'none' + document.querySelector('#loginToUpload').classList.add('is-hidden') if (!page.token && page.enableUserAccounts) document.querySelector('#loginLinkText').innerHTML = 'Create an account and keep track of your uploads' @@ -158,7 +159,7 @@ page.prepareUpload = function () { page.setActiveTab(this.dataset.id) }) page.setActiveTab('tab-files') - tabs.style.display = 'flex' + tabs.classList.remove('is-hidden') } page.prepareAlbums = function () { @@ -203,10 +204,10 @@ page.setActiveTab = function (tabId) { const id = page.tabs[i].dataset.id if (id === tabId) { page.tabs[i].classList.add('is-active') - document.querySelector(`#${id}`).style.display = 'block' + document.querySelector(`#${id}`).classList.remove('is-hidden') } else { page.tabs[i].classList.remove('is-active') - document.querySelector(`#${id}`).style.display = 'none' + document.querySelector(`#${id}`).classList.add('is-hidden') } } page.activeTab = tabId @@ -268,7 +269,7 @@ page.prepareDropzone = function () { } } }).then(function (response) { - file.previewElement.querySelector('.progress').style.display = 'none' + file.previewElement.querySelector('.progress').classList.add('is-hidden') if (response.data.success === false) file.previewElement.querySelector('.error').innerHTML = response.data.description @@ -285,7 +286,7 @@ page.prepareDropzone = function () { // Set active tab to file uploads page.setActiveTab('tab-files') // Add file entry - tabDiv.querySelector('.uploads').style.display = 'block' + tabDiv.querySelector('.uploads').classList.remove('is-hidden') file.previewElement.querySelector('.name').innerHTML = file.name }) @@ -308,7 +309,7 @@ page.prepareDropzone = function () { page.dropzone.on('success', function (file, response) { if (!response) return - file.previewElement.querySelector('.progress').style.display = 'none' + file.previewElement.querySelector('.progress').classList.add('is-hidden') if (response.success === false) file.previewElement.querySelector('.error').innerHTML = response.description @@ -324,7 +325,7 @@ page.prepareDropzone = function () { error = `File too large (${page.getPrettyBytes(file.size)}).` page.updateTemplateIcon(file.previewElement, 'icon-block') - file.previewElement.querySelector('.progress').style.display = 'none' + file.previewElement.querySelector('.progress').classList.add('is-hidden') file.previewElement.querySelector('.name').innerHTML = file.name file.previewElement.querySelector('.error').innerHTML = error.description || error }) @@ -362,7 +363,7 @@ page.uploadUrls = function (button) { // eslint-disable-next-line prefer-promise-reject-errors return done('You have not entered any URLs.') - tabDiv.querySelector('.uploads').style.display = 'block' + tabDiv.querySelector('.uploads').classList.remove('is-hidden') const files = urls.map(function (url) { const previewTemplate = document.createElement('template') previewTemplate.innerHTML = page.previewTemplate.trim() @@ -377,7 +378,7 @@ page.uploadUrls = function (button) { return done() function posted (result) { - files[i].previewElement.querySelector('.progress').style.display = 'none' + files[i].previewElement.querySelector('.progress').classList.add('is-hidden') if (result.success) { page.updateTemplate(files[i], result.files[0]) } else { @@ -408,7 +409,7 @@ page.updateTemplateIcon = function (templateElement, iconClass) { const iconElement = templateElement.querySelector('.icon') if (!iconElement) return iconElement.classList.add(iconClass) - iconElement.style.display = '' + iconElement.classList.remove('is-hidden') } page.updateTemplate = function (file, response) { @@ -417,19 +418,19 @@ page.updateTemplate = function (file, response) { const a = file.previewElement.querySelector('.link > a') const clipboard = file.previewElement.querySelector('.clipboard-mobile > .clipboard-js') a.href = a.innerHTML = clipboard.dataset.clipboardText = response.url - clipboard.parentElement.style.display = 'block' + clipboard.parentElement.classList.remove('is-hidden') const exec = /.[\w]+(\?|$)/.exec(response.url) if (exec && exec[0] && page.imageExtensions.includes(exec[0].toLowerCase())) { const img = file.previewElement.querySelector('img') img.setAttribute('alt', response.name || '') img.dataset.src = response.url - img.style.display = '' + img.classList.remove('is-hidden') img.onerror = function () { // Hide image elements that fail to load // Consequently include WEBP in browsers that do not have WEBP support (Firefox/IE) - this.style.display = 'none' - file.previewElement.querySelector('.icon').style.display = '' + this.classList.add('is-hidden') + file.previewElement.querySelector('.icon').classList.remove('is-hidden') } page.lazyLoad.update(file.previewElement.querySelectorAll('img')) } else { @@ -439,7 +440,7 @@ page.updateTemplate = function (file, response) { if (response.expirydate) { const expiryDate = file.previewElement.querySelector('.expiry-date') expiryDate.innerHTML = `Expiry date: ${page.getPrettyDate(new Date(response.expirydate * 1000))}` - expiryDate.style.display = 'block' + expiryDate.classList.remove('is-hidden') } } @@ -572,7 +573,7 @@ page.prepareUploadConfig = function () { page.fileLength = stored } - fileLengthDiv.style.display = 'block' + fileLengthDiv.classList.remove('is-hidden') fileLengthDiv.querySelector('.help').innerHTML = helpText } @@ -597,7 +598,7 @@ page.prepareUploadConfig = function () { page.uploadAge = stored } } - uploadAgeDiv.style.display = 'block' + uploadAgeDiv.classList.remove('is-hidden') } const tabContent = document.querySelector('#tab-config') @@ -664,8 +665,9 @@ window.addEventListener('paste', function (event) { const item = items[index[i]] if (item.kind === 'file') { const blob = item.getAsFile() - const file = new File([blob], `pasted-image.${blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]}`) - file.type = blob.type + const file = new File([blob], `pasted-image.${blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]}`, { + type: blob.type + }) page.dropzone.addFile(file) } } diff --git a/views/_globals.njk b/views/_globals.njk index 9a5d675..18f2da6 100644 --- a/views/_globals.njk +++ b/views/_globals.njk @@ -16,7 +16,7 @@ v3: CSS and JS files (libs such as bulma, lazyload, etc). v4: Renders in /public/render/* directories (to be used by render.js). #} -{% set v1 = "o59f6p3y1F" %} +{% set v1 = "1QupbESWeT" %} {% set v2 = "hiboQUzAzp" %} {% set v3 = "tWLiAlAX5i" %} {% set v4 = "S3TAWpPeFS" %} diff --git a/views/dashboard.njk b/views/dashboard.njk index 0dfc7bd..64171e7 100644 --- a/views/dashboard.njk +++ b/views/dashboard.njk @@ -20,7 +20,7 @@ {% block content %} {{ super() }} -
+
ID Username Uploads ${user.id} ${user.uploadsCount}${page.getPrettyBytes(user.diskUsage)}${user.uploads}${page.getPrettyBytes(user.usage)} ${displayGroup} @@ -1803,13 +1786,15 @@ page.getUsers = function ({ pageNum } = {}, element) { table.appendChild(tr) page.checkboxes.users = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) } - page.fadeAndScroll() - if (allSelected && response.data.users.length) { - const selectAll = document.querySelector('#selectAll') - if (selectAll) selectAll.checked = true + const selectAll = document.querySelector('#selectAll') + if (selectAll && !unselected) { + selectAll.checked = true + selectAll.title = 'Unselect all' } + page.fadeAndScroll() + page.views.users.pageNum = response.data.users.length ? pageNum : 0 }).catch(function (error) { if (element) page.isLoading(element, false) @@ -2008,7 +1993,7 @@ page.paginate = function (totalItems, itemsPerPage, currentPage) { ` } -page.getServerStats = function (element) { +page.getStatistics = function (element) { if (!page.permissions.admin) return swal('An error occurred!', 'You can not do this!', 'error') @@ -2016,7 +2001,6 @@ page.getServerStats = function (element) { Please wait, this may take a while\u2026 ` - page.fadeAndScroll() const url = 'api/stats' axios.get(url).then(function (response) { @@ -2040,20 +2024,31 @@ page.getServerStats = function (element) { ` else try { + const types = response.data.stats[keys[i]]._types || {} const valKeys = Object.keys(response.data.stats[keys[i]]) for (let j = 0; j < valKeys.length; j++) { - const _value = response.data.stats[keys[i]][valKeys[j]] - let value = _value - if (['albums', 'users', 'uploads'].includes(keys[i])) - value = _value.toLocaleString() - if (['memoryUsage', 'size'].includes(valKeys[j])) - value = page.getPrettyBytes(_value) - if (valKeys[j] === 'systemMemory') - value = `${page.getPrettyBytes(_value.used)} / ${page.getPrettyBytes(_value.total)} (${Math.round(_value.used / _value.total * 100)}%)` + // Skip keys that starts with an underscore + if (/^_/.test(valKeys[j])) + continue + + const value = response.data.stats[keys[i]][valKeys[j]] + let parsed = value + + // Parse values with some preset formatting + if ((types.number || []).includes(valKeys[j])) + parsed = value.toLocaleString() + if ((types.byte || []).includes(valKeys[j])) + parsed = page.getPrettyBytes(value) + if ((types.byteUsage || []).includes(valKeys[j])) + parsed = `${page.getPrettyBytes(value.used)} / ${page.getPrettyBytes(value.total)} (${Math.round(value.used / value.total * 100)}%)` + + const string = valKeys[j] + .replace(/([A-Z])/g, ' $1') + .toUpperCase() rows += `
${valKeys[j].replace(/([A-Z])/g, ' $1').toUpperCase()}${value}${string}${parsed}