diff --git a/controllers/albumsController.js b/controllers/albumsController.js index ddcfb7b..dc9b660 100644 --- a/controllers/albumsController.js +++ b/controllers/albumsController.js @@ -101,6 +101,7 @@ albumsController.create = async (req, res, next) => { public: (req.body.public === false || req.body.public === 0) ? 0 : 1, description: utils.escape(req.body.description) || '' }) + utils.invalidateStatsCache('albums') return res.json({ success: true, id: ids[0] }) } @@ -156,6 +157,7 @@ albumsController.delete = async (req, res, next) => { userid: user.id }) .update('enabled', 0) + utils.invalidateStatsCache('albums') const identifier = await db.table('albums') .select('identifier') @@ -216,6 +218,7 @@ albumsController.edit = async (req, res, next) => { public: Boolean(req.body.public), description: utils.escape(req.body.description) || '' }) + utils.invalidateStatsCache('albums') if (req.body.requestLink) { const oldIdentifier = await db.table('albums') @@ -416,6 +419,7 @@ albumsController.generateZip = async (req, res, next) => { const fileName = `${album.name}.zip` albumsController.zipEmitters.get(identifier).emit('done', filePath, fileName) + utils.invalidateStatsCache('albums') return download(filePath, fileName) }) }) diff --git a/controllers/authController.js b/controllers/authController.js index 23224a6..0a5e8c2 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -63,6 +63,7 @@ authController.register = async (req, res, next) => { enabled: 1, permission: perms.permissions.user }) + utils.invalidateStatsCache('users') return res.json({ success: true, token }) }) } @@ -179,6 +180,7 @@ authController.editUser = async (req, res, next) => { await db.table('users') .where('id', id) .update(update) + utils.invalidateStatsCache('users') if (!req.body.resetPassword) return res.json({ success: true, update }) diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 8fcb4d8..f8beeb6 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -550,6 +550,7 @@ uploadsController.formatInfoMap = (req, res, user, infoMap) => { userid: user !== undefined ? user.id : null, timestamp: Math.floor(Date.now() / 1000) }) + utils.invalidateStatsCache('uploads') } else { utils.deleteFile(info.data.filename, req.app.get('uploads-set')).catch(console.error) existingFiles.push(dbFile) @@ -669,6 +670,7 @@ uploadsController.bulkDelete = async (req, res) => { return res.json({ success: false, description: 'No array of files specified.' }) const failed = await utils.bulkDeleteFiles(field, values, user, req.app.get('uploads-set')) + utils.invalidateStatsCache('uploads') if (failed.length < values.length) return res.json({ success: true, failed }) diff --git a/controllers/utilsController.js b/controllers/utilsController.js index 8d0d3a3..7300e95 100644 --- a/controllers/utilsController.js +++ b/controllers/utilsController.js @@ -10,6 +10,24 @@ const perms = require('./permissionController') const sharp = require('sharp') const utilsController = {} +const _stats = { + system: { + cache: null, + timestamp: 0 + }, + albums: { + cache: null, + valid: false + }, + users: { + cache: null, + valid: false + }, + uploads: { + cache: null, + valid: false + } +} const uploadsDir = path.join(__dirname, '..', config.uploads.folder) const thumbsDir = path.join(uploadsDir, 'thumbs') @@ -405,7 +423,8 @@ utilsController.purgeCloudflareCache = async (names, uploads, thumbs) => { return results } -utilsController.getLinuxMemoryUsage = () => { +utilsController.getMemoryUsage = () => { + // For now this is linux-only. Not sure if darwin has this too. return new Promise((resolve, reject) => { const prc = spawn('free', ['-b']) prc.stdout.setEncoding('utf8') @@ -432,6 +451,12 @@ utilsController.getLinuxMemoryUsage = () => { }) } +utilsController.invalidateStatsCache = type => { + if (!['albums', 'users', 'uploads'].includes(type)) return + _stats[type].cache = null + _stats[type].valid = false +} + utilsController.stats = async (req, res, next) => { const user = await utilsController.authorize(req, res) if (!user) return @@ -439,66 +464,149 @@ utilsController.stats = async (req, res, next) => { const isadmin = perms.is(user, 'admin') if (!isadmin) return res.status(403).end() - const platform = os.platform() - const system = { platform: `${platform}-${os.arch()}` } - if (platform === 'linux') { - const memoryUsage = await utilsController.getLinuxMemoryUsage() - system.memory = { - used: memoryUsage.mem.used, - total: memoryUsage.mem.total + const stats = {} + + // Re-use system cache for only 1000ms + if (Date.now() - _stats.system.timestamp <= 1000) { + stats.system = _stats.system.cache + } else { + const platform = os.platform() + stats.system = { + platform: `${platform}-${os.arch()}`, + systemMemory: null, + nodeVersion: `${process.versions.node}`, + memoryUsage: process.memoryUsage().rss } - } - system['node.js'] = `${process.versions.node}` - system['memory usage'] = process.memoryUsage().rss - - if (platform !== 'win32') - system.loadavg = `${os.loadavg().map(load => load.toFixed(2)).join(', ')}` - - const stats = { - uploads: { - count: 0, - size: 0, - types: { - images: 0, - videos: 0, - others: 0 + if (platform === 'linux') { + const memoryUsage = await utilsController.getMemoryUsage() + stats.system.systemMemory = { + used: memoryUsage.mem.used, + total: memoryUsage.mem.total } - }, - users: { - count: 0, - disabled: 0, - permissions: {} + } else { + delete stats.system.systemMemory + } + + if (platform !== 'win32') + stats.system.loadAverage = `${os.loadavg().map(load => load.toFixed(2)).join(', ')}` + + // Cache + _stats.system = { + cache: stats.system, + timestamp: Date.now() } } - Object.keys(perms.permissions).forEach(p => { - stats.users.permissions[p] = 0 - }) + // Re-use albums, users, and uploads caches as long as they are still valid - const uploads = await db.table('files') - stats.uploads.count = uploads.length - for (const upload of uploads) { - stats.uploads.size += parseInt(upload.size) - const extname = utilsController.extname(upload.name) - if (utilsController.imageExtensions.includes(extname)) - stats.uploads.types.images++ - else if (utilsController.videoExtensions.includes(extname)) - stats.uploads.types.videos++ - else - stats.uploads.types.others++ + if (_stats.albums.valid) { + stats.albums = _stats.albums.cache + } else { + stats.albums = { + total: 0, + active: 0, + downloadable: 0, + public: 0, + zips: 0 + } + + const albums = await db.table('albums') + stats.albums.total = albums.length + const identifiers = [] + for (const album of albums) + if (album.enabled) { + stats.albums.active++ + if (album.download) stats.albums.downloadable++ + if (album.public) stats.albums.public++ + if (album.zipGeneratedAt) identifiers.push(album.identifier) + } + + const zipsDir = path.join(uploadsDir, '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) + }) + }) + })) + + // Cache + _stats.albums = { + cache: stats.albums, + valid: true + } } - const users = await db.table('users') - stats.users.count = users.length - for (const user of users) { - if (user.enabled === false || user.enabled === 0) stats.users.disabled++ - user.permission = user.permission || 0 - for (const p of Object.keys(stats.users.permissions)) - if (user.permission === perms.permissions[p]) stats.users.permissions[p]++ + if (_stats.users.valid) { + stats.users = _stats.users.cache + } else { + 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 + } + } + + // Cache + _stats.users = { + cache: stats.users, + valid: true + } } - return res.json({ success: true, system, stats }) + if (_stats.uploads.valid) { + stats.uploads = _stats.uploads.cache + } else { + 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 = utilsController.extname(upload.name) + if (utilsController.imageExtensions.includes(extname)) + stats.uploads.images++ + else if (utilsController.videoExtensions.includes(extname)) + stats.uploads.videos++ + else + stats.uploads.others++ + } + + // Cache + _stats.uploads = { + cache: stats.uploads, + valid: true + } + } + + return res.json({ success: true, stats }) } module.exports = utilsController diff --git a/public/js/dashboard.js b/public/js/dashboard.js index d6d1c79..37aa71d 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -1988,77 +1988,45 @@ page.getServerStats = function (element) { return swal('An error occurred!', response.data.description, 'error') } - const system = response.data.system - let systemRows = '' - for (const s of Object.keys(system)) { - let value - if (s === 'memory') { - const mem = system[s] - value = `${page.getPrettyBytes(mem.used)} / ${page.getPrettyBytes(mem.total)} (${Math.round((mem.used / mem.total) * 100)}%)` - } else if (s === 'memory usage') { - value = page.getPrettyBytes(system[s]) - } else { - value = system[s].toLocaleString() + let content = '' + + for (const key of Object.keys(response.data.stats)) { + let rows = '' + for (const valKey of Object.keys(response.data.stats[key])) { + let value = response.data.stats[key][valKey] + if (['albums', 'users'].includes(key)) + value = value.toLocaleString() + if (['memoryUsage', 'size'].includes(valKey)) + value = page.getPrettyBytes(value) + if (valKey === 'systemMemory') + value = `${page.getPrettyBytes(value.used)} / ${page.getPrettyBytes(value.total)} (${Math.round(value.used / value.total * 100)}%)` + rows += ` + + ${valKey.replace(/([A-Z])/g, ' $1').toUpperCase()} + ${value} + + ` } - systemRows += ` - - ${s.toUpperCase()} - ${value} - + content += ` +
+ + + + + + + + + ${rows} + +
${key.toUpperCase()}
+
` } - const types = response.data.stats.uploads.types - let typesRows = '' - for (const t of Object.keys(types)) - typesRows += ` - - ${t.toUpperCase()} - ${types[t].toLocaleString()} - - ` - - const permissions = response.data.stats.users.permissions - let permissionsRows = '' - for (const p of Object.keys(permissions)) - permissionsRows += ` - - ${p.toUpperCase()} - ${permissions[p].toLocaleString()} - - ` - page.dom.innerHTML = `

Statistics

-
- - - ${systemRows} - - - - - - - - - - - - - ${typesRows} - - - - - ${permissionsRows} - - - - - -
DISK USAGE${page.getPrettyBytes(response.data.stats.uploads.size)}
IN BYTES${response.data.stats.uploads.size.toLocaleString()} B
TOTAL UPLOADS${response.data.stats.uploads.count.toLocaleString()}
TOTAL USERS${response.data.stats.users.count.toLocaleString()}
DISABLED${response.data.stats.users.disabled.toLocaleString()}
-
+ ${content} ` page.fadeIn() diff --git a/views/_globals.njk b/views/_globals.njk index 4da7fa1..22c57e9 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 = "xz10E2pAFd" %} +{% set v1 = "MYXYdOtS8u" %} {% set v2 = "hiboQUzAzp" %} {% set v3 = "hiboQUzAzp" %} {% set v4 = "dplQUZqTnf" %}