diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 2b9f3b1..79c1d59 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -953,13 +953,9 @@ self.sendUploadResponse = async (req, res, user, result) => { // If uploaded by user, add delete URL (intended for ShareX and its derivatives) // Homepage uploader will not use this (use dashboard instead) - // REVISION: I wasn't aware ShareX wouldn't do a basic GET request to this API, - // which I hoped would then use the token header in the downloadable ShareX config file. - // At its current state, this isn't really usable. - /* - if (user) - map.deleteUrl = `${config.homeDomain}/api/upload/delete/${file.name}` - */ + if (user) { + map.deleteUrl = `${config.homeDomain}/file/${file.name}?delete` + } return map }) @@ -978,14 +974,7 @@ self.delete = async (req, res, next) => { field: 'id', values: isNaN(id) ? undefined : [id] } - } /* else if (req.method === 'GET') { - // ShareX-compatible API (or other clients that require basic GET-based API) - const name = req.params.name - body = { - field: 'name', - values: name ? [name] : undefined - } - } */ + } req.body = body return self.bulkDelete(req, res, next) diff --git a/lolisafe.js b/lolisafe.js index 2e2545e..9bd46e1 100644 --- a/lolisafe.js +++ b/lolisafe.js @@ -46,6 +46,7 @@ const utils = require('./controllers/utilsController') const album = require('./routes/album') const api = require('./routes/api') +const file = require('./routes/file') const nojs = require('./routes/nojs') const player = require('./routes/player') @@ -243,6 +244,7 @@ safe.use('/', express.static(paths.public, { setHeaders })) safe.use('/', express.static(paths.dist, { setHeaders })) safe.use('/', album) +safe.use('/', file) safe.use('/', nojs) safe.use('/', player) safe.use('/api', api) diff --git a/routes/file.js b/routes/file.js new file mode 100644 index 0000000..7d8bba1 --- /dev/null +++ b/routes/file.js @@ -0,0 +1,15 @@ +const routes = require('express').Router() +const utils = require('../controllers/utilsController') +const config = require('../config') + +routes.get([ + '/file/:identifier' +], async (req, res, next) => { + // Uploads identifiers parsing, etc., are strictly handled by client-side JS at src/js/file.js + return res.render('file', { + config, + versions: utils.versionStrings + }) +}) + +module.exports = routes diff --git a/src/js/file.js b/src/js/file.js new file mode 100644 index 0000000..2202376 --- /dev/null +++ b/src/js/file.js @@ -0,0 +1,233 @@ +/* global swal, axios */ + +const lsKeys = { + token: 'token' +} + +const page = { + // user token + token: localStorage[lsKeys.token], + + urlPrefix: null, + urlIdentifier: null, + + messageElement: document.querySelector('#message'), + fileinfoContainer: document.querySelector('#fileinfo'), + + downloadBtn: document.querySelector('#downloadBtn'), + playerBtn: document.querySelector('#playerBtn'), + deleteBtn: document.querySelector('#deleteBtn'), + uploadRoot: null, + titleFormat: null, + + file: null +} + +page.updateMessageBody = content => { + page.messageElement.querySelector('.message-body').innerHTML = content + page.messageElement.classList.remove('is-hidden') +} + +// Handler for regular JS errors +page.onError = error => { + console.error(error) + page.updateMessageBody(` +
An error occurred!
+${error.toString()}
Please check your console for more information.
+ `) +} + +// Handler for Axios errors +page.onAxiosError = error => { + // Better Cloudflare errors + const cloudflareErrors = { + 520: 'Unknown Error', + 521: 'Web Server Is Down', + 522: 'Connection Timed Out', + 523: 'Origin Is Unreachable', + 524: 'A Timeout Occurred', + 525: 'SSL Handshake Failed', + 526: 'Invalid SSL Certificate', + 527: 'Railgun Error', + 530: 'Origin DNS Error' + } + + const statusText = cloudflareErrors[error.response.status] || error.response.statusText + + const description = error.response.data && error.response.data.description + ? error.response.data.description + : '' + page.updateMessageBody(` +${error.response.status} ${statusText}
+${description}
+ `) +} + +page.deleteFile = () => { + if (!page.file) return + + const content = document.createElement('div') + content.innerHTML = 'You won\'t be able to recover this file!
' + + swal({ + title: 'Are you sure?', + content, + icon: 'warning', + dangerMode: true, + buttons: { + cancel: true, + confirm: { + text: 'Yes, nuke it!', + closeModal: false + } + } + }).then(proceed => { + if (!proceed) return + + axios.post('../api/upload/delete', { + id: page.file.id + }).then(response => { + if (!response) return + + if (response.data.success === false) { + return swal('An error occurred!', response.data.description, 'error') + } + + const failed = Array.isArray(response.data.failed) ? response.data.failed : [] + if (failed.length) { + swal('An error occurred!', 'Unable to delete this file.', 'error') + } else { + swal('Deleted!', 'This file has been deleted.', 'success', { + buttons: false + }) + } + }).catch(page.onAxiosError) + }) +} + +page.loadFileinfo = () => { + if (!page.urlIdentifier) return + + axios.get(`../api/upload/get/${page.urlIdentifier}`).then(response => { + if (![200, 304].includes(response.status)) { + return page.onAxiosError(response) + } + + page.file = response.data.file + + if (page.titleFormat) { + document.title = page.titleFormat.replace(/%identifier%/g, page.file.name) + } + + let rows = '' + const keys = Object.keys(page.file) + for (let i = 0; i < keys.length; i++) { + const value = page.file[keys[i]] + + let prettyValue = '' + if (value) { + if (['size'].includes(keys[i])) { + prettyValue = page.getPrettyBytes(value) + } else if (['timestamp', 'expirydate'].includes(keys[i])) { + prettyValue = page.getPrettyDate(new Date(value * 1000)) + } + } + + rows += ` +Fields | +Values | ++ |
---|
Failed to parse upload identifier from URL.
') + } + + page.urlIdentifier = match[1] + urlPrefix += window.location.pathname.substring(0, window.location.pathname.indexOf(match[1])) + page.urlPrefix = urlPrefix + + // eslint-disable-next-line compat/compat + page.urlParams = new URLSearchParams(window.location.search) + + page.deleteBtn.addEventListener('click', page.deleteFile) + + page.loadFileinfo() +}) diff --git a/src/js/misc/utils.js b/src/js/misc/utils.js index cfc024e..83a2250 100644 --- a/src/js/misc/utils.js +++ b/src/js/misc/utils.js @@ -35,10 +35,9 @@ page.prepareShareX = () => { ThumbnailURL: '$json:files[0].url$' } - /* - if (page.token) + if (page.token) { sharexConfObj.DeletionURL = '$json:files[0].deleteUrl$' - */ + } const sharexConfStr = JSON.stringify(sharexConfObj, null, 2) const sharexBlob = new Blob([sharexConfStr], { type: 'application/octet-binary' }) diff --git a/views/file.njk b/views/file.njk new file mode 100644 index 0000000..174177d --- /dev/null +++ b/views/file.njk @@ -0,0 +1,98 @@ +{%- import '_globals.njk' as globals -%} + +{% set metaTitle = "File" %} + +{% set uploadRoot = config.domain %} +{% set titleFormat = '%identifier% | ' + globals.name %} + +{% extends "_layout.njk" %} + +{% block stylesheets %} + + + + + +{% endblock %} + +{% block scripts %} + + + + +{# We assign an ID for this so that the script can find out proper root URL of uploaded files #} + + +{% endblock %} + +{% set noscriptRefreshUrl = null %} +{% block endmeta %} +{% include "_partial/noscript-refresh.njk" %} +{% endblock %} + +{% block content %} +{{ super() }} +