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.
This commit is contained in:
Bobby Wibowo 2019-09-10 23:31:27 +07:00
parent df8cac0f0b
commit 264bd88e88
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
10 changed files with 657 additions and 537 deletions

View File

@ -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 })

View File

@ -16,7 +16,8 @@ const fsFuncs = [
'rename',
'rmdir',
'symlink',
'unlink'
'unlink',
'writeFile'
]
for (const fsFunc of fsFuncs)

View File

@ -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({

View File

@ -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

View File

@ -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,

View File

@ -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
</span>
</a>
${all ? '' : `
<a class="button is-small is-warning" title="Add selected uploads to album" data-action="add-selected-files-to-album">
<a class="button is-small is-warning" title="Bulk add to album" data-action="add-selected-uploads-to-album">
<span class="icon">
<i class="icon-plus"></i>
</span>
</a>`}
<a class="button is-small is-danger" title="Bulk delete" data-action="bulk-delete">
<a class="button is-small is-danger" title="Bulk delete" data-action="bulk-delete-uploads">
<span class="icon">
<i class="icon-trash"></i>
</span>
@ -472,8 +461,8 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
</div>
`
// 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 = `<a class="image" href="${upload.file}" target="_blank" rel="noopener"><h1 class="title">${upload.extname || 'N/A'}</h1></a>`
div.innerHTML += `
<input type="checkbox" class="checkbox" title="Select this file" data-action="select"${upload.selected ? ' checked' : ''}>
<input type="checkbox" class="checkbox" title="Select" data-action="select"${upload.selected ? ' checked' : ''}>
<div class="controls">
<a class="button is-small is-primary" title="View thumbnail" data-action="display-thumbnail"${upload.thumb ? '' : ' disabled'}>
<span class="icon">
@ -556,7 +545,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
<i class="icon-plus"></i>
</span>
</a>
<a class="button is-small is-danger" title="Delete file" data-action="delete-file">
<a class="button is-small is-danger" title="Delete" data-action="delete-upload">
<span class="icon">
<i class="icon-trash"></i>
</span>
@ -581,7 +570,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
<table class="table is-narrow is-fullwidth is-hoverable">
<thead>
<tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all uploads" data-action="select-all"></th>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
<th style="width: 20%">File</th>
<th>${all ? 'User' : 'Album'}</th>
<th>Size</th>
@ -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 = `
<td class="controls"><input type="checkbox" class="checkbox" title="Select this file" data-action="select"${upload.selected ? ' checked' : ''}></td>
<td class="controls"><input type="checkbox" class="checkbox" title="Select" data-action="select"${upload.selected ? ' checked' : ''}></td>
<th><a href="${upload.file}" target="_blank" rel="noopener" title="${upload.file}">${upload.name}</a></th>
<th>${upload.appendix}</th>
<td>${upload.prettyBytes}</td>
@ -630,7 +619,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
<i class="icon-plus"></i>
</span>
</a>`}
<a class="button is-small is-danger" title="Delete file" data-action="delete-file">
<a class="button is-small is-danger" title="Delete" data-action="delete-upload">
<span class="icon">
<i class="icon-trash"></i>
</span>
@ -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 <b>user</b> (username), <b>ip</b> and <b>name</b> (file name).
This supports 3 filter keys, namely <b>user</b> (username), <b>ip</b> and <b>name</b> (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 <b>-user</b> and <b>-ip</b>, 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":
<code>user:John\\ Doe user:demo</code>
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":
<code>ip:127.0.0.1 name:*.rar name:*.zip</code>
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 = '<br>As a staff, you can use this feature to delete uploads from other users.'
page.dom.innerHTML = `
<form class="prevent-default">
<div class="field">
<label class="label">Upload names:</label>
<div class="control">
<textarea id="bulkDeleteNames" class="textarea"></textarea>
</div>
<p class="help">Separate each entry with a new line.${appendix}</p>
</div>
<div class="field">
<div class="control">
<button type="submit" id="submitBulkDelete" class="button is-danger is-fullwidth">
<span class="icon">
<i class="icon-trash"></i>
</span>
<span>Bulk delete</span>
</button>
</div>
</div>
</form>
`
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 = '<br>As a staff, you can use this feature to delete uploads by other users.'
page.dom.innerHTML = `
<h2 class="subtitle">Delete by names</h2>
<div class="field">
<label class="label">File names:</label>
<div class="control">
<textarea id="names" class="textarea"></textarea>
</div>
<p class="help">Separate each entry with a new line.${appendix}</p>
</div>
<div class="field">
<div class="control">
<a class="button is-danger is-fullwidth" data-action="delete-file-by-names">
<span class="icon">
<i class="icon-trash"></i>
</span>
<span>Bulk delete</span>
</a>
</div>
</div>
`
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 = `
<div class="field has-text-centered">
<p>You are about to add <b>${count}</b> file${count === 1 ? '' : 's'} to an album.</p>
<p><b>If a file is already in an album, it will be moved.</b></p>
<p>You are about to add <b>${count}</b> upload${count === 1 ? '' : 's'} to an album.</p>
<p><b>If an upload is already in an album, it will be moved.</b></p>
</div>
<div class="field">
<div class="control">
@ -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 = `
<h2 class="subtitle">Manage your token</h2>
<div class="field">
<label class="label">Your current token:</label>
<div class="field">
@ -1567,7 +1552,6 @@ page.getNewToken = function (element) {
page.changePassword = function () {
page.dom.innerHTML = `
<h2 class="subtitle">Change your password</h2>
<form class="prevent-default">
<div class="field">
<label class="label">New password:</label>
@ -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) {
</div>
`
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) {
<table class="table is-narrow is-fullwidth is-hoverable">
<thead>
<tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all users" data-action="select-all"></th>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
<th>ID</th>
<th style="width: 20%">Username</th>
<th>Uploads</th>
@ -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 = `
<td class="controls"><input type="checkbox" class="checkbox" title="Select this user" data-action="select"${selected ? ' checked' : ''}></td>
<td class="controls"><input type="checkbox" class="checkbox" title="Select" data-action="select"${selected ? ' checked' : ''}></td>
<th>${user.id}</th>
<th${enabled ? '' : ' class="is-linethrough"'}>${user.username}</td>
<th>${user.uploadsCount}</th>
<td>${page.getPrettyBytes(user.diskUsage)}</td>
<th>${user.uploads}</th>
<td>${page.getPrettyBytes(user.usage)}</td>
<td>${displayGroup}</td>
<td class="controls" style="text-align: right">
<a class="button is-small is-primary" title="Edit user" data-action="edit-user">
@ -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
<progress class="progress is-breeze" max="100" style="margin-top: 10px"></progress>
`
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 += `
<tr>
<th>${valKeys[j].replace(/([A-Z])/g, ' $1').toUpperCase()}</th>
<td>${value}</td>
<th>${string}</th>
<td>${parsed}</td>
</tr>
`
}
@ -2084,11 +2079,16 @@ page.getServerStats = function (element) {
`
}
page.dom.innerHTML = `
<h2 class="subtitle">Statistics</h2>
${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 = `<p>${description}</p>`
page.fadeAndScroll()
return swal('An error occurred!', description, 'error')
})
}

View File

@ -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)
}
}

View File

@ -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" %}

View File

@ -20,7 +20,7 @@
{% block content %}
{{ super() }}
<section id="dashboard" class="section">
<section id="dashboard" class="section is-hidden">
<div id="panel" class="container">
<h1 class="title">
Dashboard
@ -42,28 +42,28 @@
<a id="itemUploads">Uploads</a>
</li>
<li>
<a id="itemDeleteByNames">Delete by names</a>
<a id="itemDeleteUploadsByNames">Delete uploads by names</a>
</li>
</ul>
<p class="menu-label">Albums</p>
<ul class="menu-list">
<li>
<a id="itemManageGallery">Manage your albums</a>
<a id="itemManageAlbums">Manage your albums</a>
</li>
<li>
<ul id="albumsContainer"></ul>
</li>
</ul>
<p id="itemLabelAdmin" class="menu-label" style="display: none">Administration</p>
<ul id="itemListAdmin" class="menu-list" style="display: none">
<p id="itemLabelAdmin" class="menu-label is-hidden">Administration</p>
<ul id="itemListAdmin" class="menu-list is-hidden">
<li>
<a id="itemServerStats">Statistics</a>
<a id="itemStatistics" class="is-hidden">Statistics</a>
</li>
<li>
<a id="itemManageUploads">Manage uploads</a>
<a id="itemManageUploads" class="is-hidden">Manage uploads</a>
</li>
<li>
<a id="itemManageUsers">Manage users</a>
<a id="itemManageUsers" class="is-hidden">Manage users</a>
</li>
</ul>
<p class="menu-label">Configuration</p>
@ -72,10 +72,10 @@
<a id="ShareX">ShareX user profile</a>
</li>
<li>
<a id="itemTokens">Manage your token</a>
<a id="itemManageToken">Manage your token</a>
</li>
<li>
<a id="itemPassword">Change your password</a>
<a id="itemChangePassword">Change your password</a>
</li>
<li>
<a id="itemLogout">Logout</a>

View File

@ -43,8 +43,8 @@
<div class="columns is-gapless">
<div class="column is-hidden-mobile"></div>
<div class="column">
<a id="loginToUpload" class="button is-danger is-loading" style="display: flex"></a>
<div id="albumDiv" class="field has-addons" style="display: none">
<button id="loginToUpload" class="button is-danger is-fullwidth is-loading"></button>
<div id="albumDiv" class="field has-addons is-hidden">
<div class="control is-expanded">
<div class="select is-fullwidth">
<select id="albumSelect"></select>
@ -56,7 +56,7 @@
</a>
</div>
</div>
<div id="tabs" class="tabs is-centered is-boxed" style="display: none">
<div id="tabs" class="tabs is-centered is-boxed is-hidden">
<ul>
<li data-id="tab-files" class="is-active">
<a>
@ -80,12 +80,12 @@
</li>
</ul>
</div>
<div id="tab-files" class="tab-content" style="display: none">
<div id="tab-files" class="tab-content is-hidden">
<div class="field dz-container"></div>
<div class="field uploads"></div>
</div>
{% if urlMaxSize -%}
<div id="tab-urls" class="tab-content" style="display: none">
<div id="tab-urls" class="tab-content is-hidden">
<div class="field">
<div class="control">
<textarea id="urls" class="textarea" rows="2"></textarea>
@ -119,7 +119,7 @@
<div class="field uploads"></div>
</div>
{%- endif %}
<div id="tab-config" class="tab-content" style="display: none">
<div id="tab-config" class="tab-content is-hidden">
<form>
<div class="field">
<label class="label">File size display</label>
@ -133,7 +133,7 @@
</div>
<p class="help">This will be used in our homepage, dashboard, and album public pages.</p>
</div>
<div id="fileLengthDiv" class="field" style="display: none">
<div id="fileLengthDiv" class="field is-hidden">
<label class="label">File identifier length</label>
<div class="control is-expanded">
<input id="fileLength" class="input is-fullwidth" type="number" min="0">
@ -141,7 +141,7 @@
<p class="help"></p>
</div>
{%- if temporaryUploadAges %}
<div id="uploadAgeDiv" class="field" style="display: none">
<div id="uploadAgeDiv" class="field is-hidden">
<label class="label">Upload age</label>
<div class="control is-expanded">
<div class="select is-fullwidth">
@ -185,18 +185,18 @@
<div class="column is-hidden-mobile"></div>
</div>
<div id="tpl" style="display: none">
<div id="tpl" class="is-hidden">
<div class="field">
<i class="icon" style="display: none"></i>
<img class="is-unselectable" style="display: none">
<i class="icon is-hidden"></i>
<img class="is-unselectable is-hidden">
<p class="name is-unselectable"></p>
<progress class="progress is-small is-danger" max="100" value="0"></progress>
<p class="error"></p>
<p class="link">
<a target="_blank" rel="noopener"></a>
</p>
<p class="help expiry-date" style="display: none"></p>
<p class="clipboard-mobile" style="display: none">
<p class="help expiry-date is-hidden"></p>
<p class="clipboard-mobile is-hidden">
<a class="button is-small is-info is-outlined clipboard-js" style="display: flex">
<span class="icon">
<i class="icon-clipboard-1"></i>