Updates (breaking changes!)

* Updated API route: /upload/bulkdelete.
It now accepts an additional property named "field". In it you can now enter either "id" or "name", which will set whether it will bulk delete by ids or names respectively. It also no longer accepts property named "ids", instead it has to be named "values" (which of course is an array of either ids or names). So yeah, now the API route can be used to bulk delete by ids and names.
In the future this will be expanded to bulk deleting files by username (only accessible by root of course).

* Added a form to bulk delete files by names for the hardcore user, like me (https://i.fiery.me/AHph.png).

* Some design update. Mainly forms restructuring aimed at tight screens.

* Changing file name length, requesting new token and setting new password will no longer reload the dashboard page on success. Instead it will simply silently reload the form.

* utils.bulkDeleteFilesByIds() replaced by utils.bulkDeleteFiles() which now can either by ids or names. This will be the one that will eventually be extended for deleting by username.

* Various other code improvements.
This commit is contained in:
Bobby Wibowo 2018-05-06 02:44:58 +07:00
parent ee2ce394b1
commit 08410faa9a
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
6 changed files with 221 additions and 98 deletions

View File

@ -129,7 +129,7 @@ albumsController.delete = async (req, res, next) => {
}
let ids = []
let failedids = []
let failed = []
if (purge) {
const files = await db.table('files')
.where({
@ -138,9 +138,9 @@ albumsController.delete = async (req, res, next) => {
})
ids = files.map(file => file.id)
failedids = await utils.bulkDeleteFilesByIds(ids, user)
failed = await utils.bulkDeleteFiles('id', ids, user)
if (failedids.length === ids.length) {
if (failed.length === ids.length) {
return res.json({ success: false, description: 'Could not delete any of the files associated with the album.' })
}
}
@ -166,9 +166,9 @@ albumsController.delete = async (req, res, next) => {
fs.unlink(zipPath, error => {
if (error && error.code !== 'ENOENT') {
console.log(error)
return res.json({ success: false, description: error.toString(), failedids })
return res.json({ success: false, description: error.toString(), failed })
}
res.json({ success: true, failedids })
res.json({ success: true, failed })
})
}
@ -443,7 +443,7 @@ albumsController.addFiles = async (req, res, next) => {
}
})
const failedids = ids.filter(id => !files.find(file => file.id === id))
const failed = ids.filter(id => !files.find(file => file.id === id))
await Promise.all(files.map(file => {
if (file.albumid && !albumids.includes(file.albumid)) {
@ -455,23 +455,23 @@ albumsController.addFiles = async (req, res, next) => {
.update('albumid', albumid)
.catch(error => {
console.error(error)
failedids.push(file.id)
failed.push(file.id)
})
}))
if (failedids.length < ids.length) {
if (failed.length < ids.length) {
await Promise.all(albumids.map(albumid => {
return db.table('albums')
.where('id', albumid)
.update('editedAt', Math.floor(Date.now() / 1000))
}))
return res.json({ success: true, failedids })
return res.json({ success: true, failed })
}
return res.json({
success: false,
description: `Could not ${albumid === null ? 'add' : 'remove'} any of the selected files ${albumid === null ? 'to' : 'from'} the album.`
description: `Could not ${albumid === null ? 'add' : 'remove'} any files ${albumid === null ? 'to' : 'from'} the album.`
})
}

View File

@ -446,7 +446,7 @@ uploadsController.processFilesForDisplay = async (req, res, files, existingFiles
}
uploadsController.delete = async (req, res) => {
// TODO: Wrap utils.bulkDeleteFilesByIds() instead
// TODO: Wrap utils.bulkDeleteFiles() instead
const user = await utils.authorize(req, res)
if (!user) { return }
@ -483,17 +483,19 @@ uploadsController.delete = async (req, res) => {
uploadsController.bulkDelete = async (req, res) => {
const user = await utils.authorize(req, res)
if (!user) { return }
const ids = req.body.ids
if (ids === undefined || !ids.length) {
const field = req.body.field || 'id'
const values = req.body.values
if (values === undefined || !values.length) {
return res.json({ success: false, description: 'No files specified.' })
}
const failedids = await utils.bulkDeleteFilesByIds(ids, user)
if (failedids.length < ids.length) {
return res.json({ success: true, failedids })
const failed = await utils.bulkDeleteFiles(field, values, user)
if (failed.length < values.length) {
return res.json({ success: true, failed })
}
return res.json({ success: false, description: 'Could not delete any of the selected files.' })
return res.json({ success: false, description: 'Could not delete any files.' })
}
uploadsController.list = async (req, res) => {

View File

@ -112,18 +112,29 @@ utilsController.deleteFile = file => {
})
}
// This will return an array of IDs that could not be deleted
utilsController.bulkDeleteFilesByIds = async (ids, user) => {
/**
* Delete files by matching whether the specified field contains any value
* in the array of values. This will return an array of values that could
* not be deleted. At the moment it's hard-coded to only accept either
* "id" or "name" field.
*
* @param {string} field
* @param {any} values
* @param {user} user
* @return {any[]} failed
*/
utilsController.bulkDeleteFiles = async (field, values, user) => {
if (!user) { return }
if (!['id', 'name'].includes(field)) { return }
const files = await db.table('files')
.whereIn('id', ids)
.whereIn(field, values)
.where(function () {
if (user.username !== 'root') {
this.where('userid', user.id)
}
})
const failedids = ids.filter(id => !files.find(file => file.id === id))
const failed = values.filter(value => !files.find(file => file[field] === value))
const albumids = []
// Delete all files
@ -132,12 +143,12 @@ utilsController.bulkDeleteFilesByIds = async (ids, user) => {
const deleteFile = await utilsController.deleteFile(file.name)
.catch(error => {
console.log(error)
failedids.push(file.id)
failed.push(file[field])
})
if (!deleteFile) { return resolve() }
await db.table('files')
.where('id', file.id)
.where(field, file[field])
.del()
.then(() => {
if (file.albumid && !albumids.includes(file.albumid)) {
@ -146,7 +157,7 @@ utilsController.bulkDeleteFilesByIds = async (ids, user) => {
})
.catch(error => {
console.error(error)
failedids.push(file.id)
failed.push(file[field])
})
return resolve()
@ -162,7 +173,7 @@ utilsController.bulkDeleteFilesByIds = async (ids, user) => {
}))
}
return failedids
return failed
}
module.exports = utilsController

View File

@ -19,11 +19,12 @@ const page = {
checkboxes: [],
lastSelected: null,
// select album dom, for 'add to album' dialog
// select album dom for dialogs/modals
selectAlbumContainer: null,
// cache of albums data, for 'edit album' dialog
albums: [],
// cache of files and albums data for dialogs/modals
files: new Map(),
albums: new Map(),
clipboardJS: null,
lazyLoad: null
@ -37,11 +38,7 @@ page.preparePage = () => {
page.verifyToken(page.token, true)
}
page.verifyToken = async (token, reloadOnError) => {
if (reloadOnError === undefined) {
reloadOnError = false
}
page.verifyToken = async (token, reloadOnError = false) => {
const response = await axios.post('api/tokens/verify', { token })
.catch(error => {
console.log(error)
@ -50,7 +47,7 @@ page.verifyToken = async (token, reloadOnError) => {
if (!response) { return }
if (response.data.success === false) {
swal({
return swal({
title: 'An error occurred!',
text: response.data.description,
icon: 'error'
@ -60,7 +57,6 @@ page.verifyToken = async (token, reloadOnError) => {
location.location = 'auth'
}
})
return
}
axios.defaults.headers.common.token = token
@ -79,6 +75,10 @@ page.prepareDashboard = () => {
page.setActiveMenu(this)
})
document.getElementById('itemDeleteByNames').addEventListener('click', function () {
page.setActiveMenu(this)
})
document.getElementById('itemManageGallery').addEventListener('click', function () {
page.setActiveMenu(this)
})
@ -127,6 +127,8 @@ page.getUploads = (album, pageNum, element) => {
}
}
page.files.clear()
let prevPage = 0
let nextPage = pageNum + 1
@ -270,6 +272,11 @@ page.getUploads = (album, pageNum, element) => {
const selected = page.selectedFiles.includes(file.id)
if (!selected && allFilesSelected) { allFilesSelected = false }
page.files.set(file.id, {
name: file.name,
thumb: file.thumb
})
const tr = document.createElement('tr')
let displayAlbumOrUser = file.album
@ -286,7 +293,7 @@ page.getUploads = (album, pageNum, element) => {
<td>${file.size}</td>
<td>${file.date}</td>
<td style="text-align: right">
<a class="button is-small is-primary" title="View thumbnail" onclick="page.displayThumbnail(${file.thumb ? `'${file.thumb}'` : null}, '${file.name}')"${file.thumb ? '' : ' disabled'}>
<a class="button is-small is-primary" title="View thumbnail" onclick="page.displayThumbnail(${file.id})"${file.thumb ? '' : ' disabled'}>
<span class="icon">
<i class="icon-picture-1"></i>
</span>
@ -334,13 +341,14 @@ page.setFilesView = (view, element) => {
page.getUploads(page.currentView.album, page.currentView.pageNum, element)
}
page.displayThumbnail = (src, text) => {
if (!src) { return }
page.displayThumbnail = id => {
const file = page.files.get(id)
if (!file.thumb) { return }
swal({
text,
text: file.name,
content: {
element: 'img',
attributes: { src }
attributes: { src: file.thumb }
},
button: true
})
@ -509,7 +517,8 @@ page.deleteSelectedFiles = async () => {
if (!proceed) { return }
const bulkdelete = await axios.post('api/upload/bulkdelete', {
ids: page.selectedFiles
field: 'id',
values: page.selectedFiles
})
.catch(error => {
console.log(error)
@ -526,9 +535,9 @@ page.deleteSelectedFiles = async () => {
}
let deleted = count
if (bulkdelete.data.failedids && bulkdelete.data.failedids.length) {
deleted -= bulkdelete.data.failedids.length
page.selectedFiles = page.selectedFiles.filter(id => bulkdelete.data.failedids.includes(id))
if (bulkdelete.data.failed && bulkdelete.data.failed.length) {
deleted -= bulkdelete.data.failed.length
page.selectedFiles = page.selectedFiles.filter(id => bulkdelete.data.failed.includes(id))
} else {
page.selectedFiles = []
}
@ -539,16 +548,91 @@ page.deleteSelectedFiles = async () => {
return page.getUploads(page.currentView.album, page.currentView.pageNum)
}
page.deleteByNames = () => {
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.</p>
</div>
<div class="field">
<div class="control">
<a class="button is-danger is-fullwidth" onclick="page.deleteFileByNames()">
<span class="icon">
<i class="icon-trash"></i>
</span>
<span>Bulk delete</span>
</a>
</div>
</div>
`
}
page.deleteFileByNames = async () => {
const names = document.getElementById('names').value.split(/\r?\n/).filter(n => 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'}`
const proceed = await 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
}
}
})
if (!proceed) { return }
const bulkdelete = await axios.post('api/upload/bulkdelete', {
field: 'name',
values: names
})
.catch(error => {
console.log(error)
swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
})
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')
}
}
let deleted = count
if (bulkdelete.data.failed && bulkdelete.data.failed.length) {
deleted -= bulkdelete.data.failed.length
document.getElementById('names').value = bulkdelete.data.failed.join('\n')
}
swal('Deleted!', `${deleted} file${deleted === 1 ? ' has' : 's have'} been deleted.`, 'success')
}
page.addSelectedFilesToAlbum = async () => {
const count = page.selectedFiles.length
if (!count) {
return swal('An error occurred!', 'You have not selected any files.', 'error')
}
const failedids = await page.addFilesToAlbum(page.selectedFiles)
if (!failedids) { return }
if (failedids.length) {
page.selectedFiles = page.selectedFiles.filter(id => failedids.includes(id))
const failed = await page.addFilesToAlbum(page.selectedFiles)
if (!failed) { return }
if (failed.length) {
page.selectedFiles = page.selectedFiles.filter(id => failed.includes(id))
} else {
page.selectedFiles = []
}
@ -557,8 +641,8 @@ page.addSelectedFilesToAlbum = async () => {
}
page.addSingleFileToAlbum = async id => {
const failedids = await page.addFilesToAlbum([id])
if (!failedids) { return }
const failed = await page.addFilesToAlbum([id])
if (!failed) { return }
page.getUploads(page.currentView.album, page.currentView.pageNum)
}
@ -652,8 +736,8 @@ page.addFilesToAlbum = async ids => {
}
let added = ids.length
if (add.data.failedids && add.data.failedids.length) {
added -= add.data.failedids.length
if (add.data.failed && add.data.failed.length) {
added -= add.data.failed.length
}
const suffix = `file${ids.length === 1 ? '' : 's'}`
@ -663,7 +747,7 @@ page.addFilesToAlbum = async ids => {
}
swal('Woohoo!', `Successfully ${albumid < 0 ? 'removed' : 'added'} ${added} ${suffix} ${albumid < 0 ? 'from' : 'to'} the album.`, 'success')
return add.data.failedids
return add.data.failed
}
page.getAlbums = () => {
@ -676,23 +760,30 @@ page.getAlbums = () => {
}
}
page.albums.clear()
page.dom.innerHTML = `
<h2 class="subtitle">Create new album</h2>
<div class="field has-addons has-addons-centered">
<div class="control is-expanded">
<div class="field">
<div class="control">
<input id="albumName" class="input" type="text" placeholder="Name">
</div>
</div>
<div class="field">
<div class="control">
<a id="submitAlbum" class="button is-breeze">
<a id="submitAlbum" class="button is-breeze is-fullwidth">
<span class="icon">
<i class="icon-paper-plane-empty"></i>
</span>
<span>Submit</span>
<span>Create</span>
</a>
</div>
</div>
<hr>
<h2 class="subtitle">List of albums</h2>
<div class="table-container">
@ -713,13 +804,18 @@ page.getAlbums = () => {
</div>
`
page.albums = response.data.albums
const homeDomain = response.data.homeDomain
const table = document.getElementById('table')
for (const album of response.data.albums) {
const albumUrl = `${homeDomain}/a/${album.identifier}`
page.albums.set(album.id, {
name: album.name,
download: album.download,
public: album.public
})
const tr = document.createElement('tr')
tr.innerHTML = `
<tr>
@ -767,10 +863,8 @@ page.getAlbums = () => {
}
page.editAlbum = async id => {
const album = page.albums.find(a => a.id === id)
if (!album) {
return swal('An error occurred!', 'Album with that ID could not be found.', 'error')
}
const album = page.albums.get(id)
if (!album) { return }
const div = document.createElement('div')
div.innerHTML = `
@ -978,21 +1072,24 @@ page.changeFileLength = () => {
<h2 class="subtitle">File name length</h2>
<div class="field">
<label class="label">Your current file name length:</label>
<div class="field has-addons">
<div class="control is-expanded">
<div class="field">
<label class="label">Your current file name length:</label>
<div class="control">
<input id="fileLength" class="input" type="text" placeholder="Your file length" value="${response.data.fileLength ? Math.min(Math.max(response.data.fileLength, response.data.config.min), response.data.config.max) : response.data.config.default}">
</div>
<p class="help">Default file name length is <b>${response.data.config.default}</b> characters. ${response.data.config.userChangeable ? `Range allowed for user is <b>${response.data.config.min}</b> to <b>${response.data.config.max}</b> characters.` : 'Changing file name length is disabled at the moment.'}</p>
</div>
<div class="field">
<div class="control">
<a id="setFileLength" class="button is-breeze">
<a id="setFileLength" class="button is-breeze is-fullwidth">
<span class="icon">
<i class="icon-paper-plane-empty"></i>
</span>
<span>Set file name length</span>
</a>
</div>
</div>
<p class="help">Default file name length is <b>${response.data.config.default}</b> characters. ${response.data.config.userChangeable ? `Range allowed for user is <b>${response.data.config.min}</b> to <b>${response.data.config.max}</b> characters.` : 'Changing file name length is disabled at the moment.'}</p>
<div>
</div>
`
@ -1024,7 +1121,8 @@ page.setFileLength = (fileLength, element) => {
text: 'Your file length was successfully changed.',
icon: 'success'
}).then(() => {
location.reload()
// location.reload()
page.changeFileLength()
})
})
.catch(error => {
@ -1050,18 +1148,21 @@ page.changeToken = () => {
<div class="field">
<label class="label">Your current token:</label>
<div class="field has-addons">
<div class="control is-expanded">
<div class="field">
<div class="control">
<input id="token" readonly class="input" type="text" placeholder="Your token" value="${response.data.token}">
</div>
<div class="control">
<a id="getNewToken" class="button is-breeze">
<span class="icon">
<i class="icon-arrows-cw"></i>
</span>
<span>Request new token</span>
</a>
</div>
</div>
</div>
<div class="field">
<div class="control">
<a id="getNewToken" class="button is-breeze is-fullwidth">
<span class="icon">
<i class="icon-arrows-cw"></i>
</span>
<span>Request new token</span>
</a>
</div>
</div>
`
@ -1094,8 +1195,11 @@ page.getNewToken = element => {
text: 'Your token was successfully changed.',
icon: 'success'
}).then(() => {
axios.defaults.headers.common.token = response.data.token
localStorage.token = response.data.token
location.reload()
page.token = response.data.token
// location.reload()
page.changeToken()
})
})
.catch(error => {
@ -1112,23 +1216,25 @@ page.changePassword = () => {
<div class="field">
<label class="label">New password:</label>
<div class="control">
<input id="password" class="input" type="password" placeholder="Your new password">
<input id="password" class="input" type="password">
</div>
</div>
<div class="field">
<label class="label">Confirm password:</label>
<div class="field has-addons">
<div class="control is-expanded">
<input id="passwordConfirm" class="input is-expanded" type="password" placeholder="Verify your new password">
</div>
<div class="control">
<a id="sendChangePassword" class="button is-breeze">
<span class="icon">
<i class="icon-paper-plane-empty"></i>
</span>
<span>Set new password</span>
</a>
</div>
<label class="label">Re-type new password:</label>
<div class="control">
<input id="passwordConfirm" class="input" type="password">
</div>
</div>
<div class="field">
<div class="control">
<a id="sendChangePassword" class="button is-breeze is-fullwidth">
<span class="icon">
<i class="icon-paper-plane-empty"></i>
</span>
<span>Set new password</span>
</a>
</div>
</div>
`
@ -1142,7 +1248,7 @@ page.changePassword = () => {
text: 'Your passwords do not match, please try again.',
icon: 'error'
}).then(() => {
page.changePassword()
// page.changePassword()
})
}
})
@ -1166,7 +1272,8 @@ page.sendNewPassword = (pass, element) => {
text: 'Your password was successfully changed.',
icon: 'success'
}).then(() => {
location.reload()
// location.reload()
page.changePassword()
})
})
.catch(error => {

View File

@ -12,7 +12,7 @@
v1: CSS and JS files.
v2: Images and config files (manifest.json, browserconfig.xml, etcetera).
#}
{% set v1 = "J0YdFIdpEH" %}
{% set v1 = "TsCPSS21gd" %}
{% set v2 = "MSEpgpfFIQ" %}
{#

View File

@ -55,6 +55,9 @@
<li>
<a id="itemUploads" onclick="page.getUploads()">Uploads</a>
</li>
<li>
<a id="itemDeleteByNames" onclick="page.deleteByNames()">Delete by names</a>
</li>
</ul>
<p class="menu-label">Albums</p>
<ul class="menu-list">