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 += ` + + ${keys[i]} + ${value} + ${prettyValue} + + ` + } + + document.querySelector('#title').innerText = page.file.name + page.fileinfoContainer.querySelector('.table-container').innerHTML = ` +
+ + + + + + + + + + ${rows} + +
FieldsValues
+
+ ` + + if (page.downloadBtn) { + page.downloadBtn.setAttribute('href', `${page.uploadRoot}/${page.file.name}`) + } + + const isimage = page.file.type.startsWith('image/') + const isvideo = page.file.type.startsWith('video/') + const isaudio = page.file.type.startsWith('audio/') + if (isimage) { + const img = page.fileinfoContainer.querySelector('img') + img.setAttribute('alt', page.file.name || '') + img.src = `${page.uploadRoot}/${page.file.name}` + img.parentNode.classList.remove('is-hidden') + img.onerror = event => event.currentTarget.classList.add('is-hidden') + } else if (isvideo || isaudio) { + page.playerBtn.setAttribute('href', `../v/${page.file.name}`) + page.playerBtn.parentNode.parentNode.classList.remove('is-hidden') + } + + page.fileinfoContainer.classList.remove('is-hidden') + page.messageElement.classList.add('is-hidden') + + if (page.urlParams.has('delete')) { + page.deleteBtn.click() + } + }).catch(error => { + if (typeof error.response !== 'undefined') page.onAxiosError(error) + else page.onError(error) + }) +} + +window.addEventListener('DOMContentLoaded', () => { + // Partial polyfill URLSearchParams.has() + // eslint-disable-next-line compat/compat + window.URLSearchParams = window.URLSearchParams || function (searchString) { + const self = this + self.has = function (name) { + const results = new RegExp('[?&]' + name).exec(self.searchString) + if (results == null) { + return false + } else { + return true + } + } + } + + axios.defaults.headers.common.token = page.token + + const mainScript = document.querySelector('#mainScript') + if (!mainScript || typeof mainScript.dataset.uploadRoot === 'undefined') return + + page.uploadRoot = mainScript.dataset.uploadRoot + page.titleFormat = mainScript.dataset.titleFormat + + let urlPrefix = window.location.protocol + '//' + window.location.host + const match = window.location.pathname.match(/.*\/(.*)$/) + if (!match || !match[1]) { + return page.updateMessageBody('

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() }} +
+
+ +
+ +
+
+

Loading…

+
+
+ + +
+
+ +{% set floatingHomeHref = '..' %} +{% include "_partial/floating-home.njk" %} +{% set noscriptMessage = null %} +{% include "_partial/noscript.njk" %} +{% endblock %}