Added deletion URL for ShareX or derivatives

For registered users only!
This requires adding a basic GET API for file deletion, so that I did.
Configs which guests download will not include pattern for delete URL,
so they won't get notified of unusable delete URL or anything like that.

dev: Improved logger.debug() to support specifying options for node's
Util.inspect() if an object is set as its last param
(assuming >1 params).
Default options now also includes enabling colors.

src/js/utils.js: Simplified dynamic ShareX config generator.
Among other things, it will now use JSON.stringify().
I don't even remember why we didn't use that in the first place..

Some logic improvements in src/js/home.js.

Bumped v1 version string and rebuilt client assets.
This commit is contained in:
Bobby Wibowo 2020-06-20 01:28:23 +07:00
parent e552017bfb
commit 51c8df71bc
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
10 changed files with 79 additions and 48 deletions

View File

@ -320,7 +320,7 @@ self.actuallyUploadFiles = async (req, res, user, albumid, age) => {
await self.stripTags(req, infoMap)
const result = await self.storeFilesToDb(req, res, user, infoMap)
await self.sendUploadResponse(req, res, result)
await self.sendUploadResponse(req, res, user, result)
}
self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
@ -418,7 +418,7 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
}
const result = await self.storeFilesToDb(req, res, user, infoMap)
await self.sendUploadResponse(req, res, result)
await self.sendUploadResponse(req, res, user, result)
} catch (error) {
// Unlink all downloaded files when at least one file threw an error from the for-loop
// Should continue even when encountering errors
@ -544,7 +544,7 @@ self.actuallyFinishChunks = async (req, res, user) => {
await self.stripTags(req, infoMap)
const result = await self.storeFilesToDb(req, res, user, infoMap)
await self.sendUploadResponse(req, res, result)
await self.sendUploadResponse(req, res, user, result)
} catch (error) {
// Dispose unfinished hasher and clean up leftover chunks
// Should continue even when encountering errors
@ -733,7 +733,7 @@ self.storeFilesToDb = async (req, res, user, infoMap) => {
return files.concat(exists)
}
self.sendUploadResponse = async (req, res, result) => {
self.sendUploadResponse = async (req, res, user, result) => {
// Send response
res.json({
success: true,
@ -751,18 +751,35 @@ self.sendUploadResponse = async (req, res, result) => {
if (req.path === '/nojs')
map.original = file.original
// If uploaded by user, add delete URL (intended for ShareX and its derivatives)
// Homepage uploader will not use this (use dashboard instead)
if (user)
map.deleteUrl = `${config.homeDomain}/api/upload/delete/${file.name}`
return map
})
})
}
self.delete = async (req, res) => {
// Map /delete requests to /bulkdelete route
const id = parseInt(req.body.id)
const body = {
field: 'id',
values: isNaN(id) ? undefined : [id]
// Map /api/delete requests to /api/bulkdelete
let body
if (req.method === 'POST') {
// Original lolisafe API (this fork uses /api/bulkdelete immediately)
const id = parseInt(req.body.id)
body = {
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)
}

2
dist/js/home.js vendored

File diff suppressed because one or more lines are too long

2
dist/js/home.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
lsKeys.siBytes="siBytes",page.prepareShareX=function(){var e=document.querySelector("#ShareX");if(e){var t=page.token?{token:page.token||"",albumid:page.album||""}:{};t.filelength=page.fileLength||"",t.age=page.uploadAge||"",t.striptags=page.stripTags||"";for(var a=[],n=Object.keys(t),r=0;r<n.length;r++)a.push(' "'+n[r]+'": "'+t[n[r]]+'"');var o=(window.location.hostname+window.location.pathname).replace(/\/(dashboard)?$/,""),i=o.replace(/\//g,"_"),s='{\n "Name": "'+i+'",\n "DestinationType": "ImageUploader, FileUploader",\n "RequestMethod": "POST",\n "RequestURL": "'+window.location.protocol+"//"+o+'/api/upload",\n "Headers": {\n'+a.join(",\n")+'\n },\n "Body": "MultipartFormData",\n "FileFormName": "files[]",\n "URL": "$json:files[0].url$",\n "ThumbnailURL": "$json:files[0].url$"\n}',l=new Blob([s],{type:"application/octet-binary"});e.setAttribute("href",URL.createObjectURL(l)),e.setAttribute("download",i+".sxcu")}},page.getPrettyDate=function(e){return e.getFullYear()+"/"+(e.getMonth()<9?"0":"")+(e.getMonth()+1)+"/"+(e.getDate()<10?"0":"")+e.getDate()+" "+(e.getHours()<10?"0":"")+e.getHours()+":"+(e.getMinutes()<10?"0":"")+e.getMinutes()+":"+(e.getSeconds()<10?"0":"")+e.getSeconds()},page.getPrettyBytes=function(e){if("number"!=typeof e&&!isFinite(e))return e;var t="0"!==localStorage[lsKeys.siBytes],a=e<0?"-":"",n=t?1e3:1024;if(a&&(e=-e),e<n)return""+a+e+" B";var r=Math.min(Math.floor(Math.log(e)*Math.LOG10E/3),8);return""+a+Number((e/Math.pow(n,r)).toPrecision(3))+" "+((t?"kMGTPEZY":"KMGTPEZY").charAt(r-1)+(t?"":"i"))+"B"},page.escape=function(e){if(!e)return e;var t,a=String(e),n=/["'&<>]/.exec(a);if(!n)return a;var r="",o=0,i=0;for(o=n.index;o<a.length;o++){switch(a.charCodeAt(o)){case 34:t="&quot;";break;case 38:t="&amp;";break;case 39:t="&#39;";break;case 60:t="&lt;";break;case 62:t="&gt;";break;default:continue}i!==o&&(r+=a.substring(i,o)),i=o+1,r+=t}return i!==o?r+a.substring(i,o):r};
lsKeys.siBytes="siBytes",page.prepareShareX=function(){var e=document.querySelector("#ShareX");if(e){var t={};page.token&&(t.token=page.token||"",t.albumid=page.album||""),t.filelength=page.fileLength||"",t.age=page.uploadAge||"",t.striptags=page.stripTags||"";var a=(window.location.hostname+window.location.pathname).replace(/\/(dashboard)?$/,""),r=a.replace(/\//g,"_"),o={Name:r,DestinationType:"ImageUploader, FileUploader",RequestMethod:"POST",RequestURL:window.location.protocol+"//"+a+"/api/upload",Headers:t,Body:"MultipartFromData",FileFormName:"files[]",URL:"$json:files[0].url$",ThumbnailURL:"$json:files[0].url$"};page.token&&(o.DeletionURL="$json:files[0].deleteUrl$");var n=JSON.stringify(o,null,2),i=new Blob([n],{type:"application/octet-binary"});e.setAttribute("href",URL.createObjectURL(i)),e.setAttribute("download",r+".sxcu")}},page.getPrettyDate=function(e){return e.getFullYear()+"/"+(e.getMonth()<9?"0":"")+(e.getMonth()+1)+"/"+(e.getDate()<10?"0":"")+e.getDate()+" "+(e.getHours()<10?"0":"")+e.getHours()+":"+(e.getMinutes()<10?"0":"")+e.getMinutes()+":"+(e.getSeconds()<10?"0":"")+e.getSeconds()},page.getPrettyBytes=function(e){if("number"!=typeof e&&!isFinite(e))return e;var t="0"!==localStorage[lsKeys.siBytes],a=e<0?"-":"",r=t?1e3:1024;if(a&&(e=-e),e<r)return""+a+e+" B";var o=Math.min(Math.floor(Math.log(e)*Math.LOG10E/3),8);return""+a+Number((e/Math.pow(r,o)).toPrecision(3))+" "+((t?"kMGTPEZY":"KMGTPEZY").charAt(o-1)+(t?"":"i"))+"B"},page.escape=function(e){if(!e)return e;var t,a=String(e),r=/["'&<>]/.exec(a);if(!r)return a;var o="",n=0,i=0;for(n=r.index;n<a.length;n++){switch(a.charCodeAt(n)){case 34:t="&quot;";break;case 38:t="&amp;";break;case 39:t="&#39;";break;case 60:t="&lt;";break;case 62:t="&gt;";break;default:continue}i!==n&&(o+=a.substring(i,n)),i=n+1,o+=t}return i!==n?o+a.substring(i,n):o};
//# sourceMappingURL=utils.js.map

File diff suppressed because one or more lines are too long

View File

@ -46,8 +46,16 @@ self.error = (content, options = {}) => {
}
self.debug = (...args) => {
const options = {
colors: true,
depth: Infinity
}
if (args.length > 1 && typeof args[args.length - 1] === 'object') {
Object.assign(options, args[args.length - 1])
args.splice(args.length - 1, 1)
}
for (const arg of args)
console.log(inspect(arg, { depth: Infinity }))
console.log(inspect(arg, options))
}
module.exports = self

View File

@ -27,6 +27,7 @@ routes.get('/uploads', (req, res, next) => uploadController.list(req, res, next)
routes.get('/uploads/:page', (req, res, next) => uploadController.list(req, res, next))
routes.post('/upload', (req, res, next) => uploadController.upload(req, res, next))
routes.post('/upload/delete', (req, res, next) => uploadController.delete(req, res, next))
routes.get('/upload/delete/:name', (req, res, next) => uploadController.delete(req, res, next))
routes.post('/upload/bulkdelete', (req, res, next) => uploadController.bulkDelete(req, res, next))
routes.post('/upload/finishchunks', (req, res, next) => uploadController.finishChunks(req, res, next))
routes.post('/upload/:albumid', (req, res, next) => uploadController.upload(req, res, next))

View File

@ -767,6 +767,10 @@ page.createAlbum = () => {
}
page.prepareUploadConfig = () => {
// This object should only be used to set fallback values for page[key]
// (essentially for page[key] properties that explicitly need to be set as something)
// As for default values in the Config tab (which will not set page[key]),
// check out number.default property of each config
const fallback = {
chunkSize: page.chunkSizeConfig.default,
parallelUploads: 2
@ -794,6 +798,7 @@ page.prepareUploadConfig = () => {
number: fileIdentifierLength ? {
min: page.fileIdentifierLength.min,
max: page.fileIdentifierLength.max,
default: page.fileIdentifierLength.default,
round: true
} : undefined,
help: true, // true means auto-generated, for number-based configs only
@ -822,6 +827,7 @@ page.prepareUploadConfig = () => {
number: {
min: 1,
max: page.chunkSizeConfig.max,
default: fallback.chunkSize,
suffix: ' MB',
round: true
},
@ -832,6 +838,7 @@ page.prepareUploadConfig = () => {
number: {
min: 1,
max: 10,
default: fallback.parallelUploads,
round: true
},
help: true
@ -879,7 +886,6 @@ page.prepareUploadConfig = () => {
}
if (fileIdentifierLength) {
fallback.fileLength = page.fileIdentifierLength.default || undefined
const stored = parseInt(localStorage[lsKeys.fileLength])
if (!page.fileIdentifierLength.force &&
!isNaN(stored) &&
@ -952,7 +958,7 @@ page.prepareUploadConfig = () => {
${opts.join('\n')}
</select>
`
} else if (conf.number !== undefined) {
} else if (conf.number) {
control = document.createElement('input')
control.id = control.name = key
control.className = 'input is-fullwidth'
@ -964,8 +970,8 @@ page.prepareUploadConfig = () => {
control.max = conf.number.max
if (typeof value === 'number')
control.value = value
else if (fallback[key] !== undefined)
control.value = fallback[key]
else if (conf.number.default !== undefined)
control.value = conf.number.default
}
let help
@ -980,8 +986,8 @@ page.prepareUploadConfig = () => {
} else if (conf.help === true && conf.number !== undefined) {
const tmp = []
if (fallback[key] !== undefined)
tmp.push(`Default is ${fallback[key]}${conf.number.suffix || ''}.`)
if (conf.number.default !== undefined)
tmp.push(`Default is ${conf.number.default}${conf.number.suffix || ''}.`)
if (conf.number.min !== undefined)
tmp.push(`Min is ${conf.number.min}${conf.number.suffix || ''}.`)
if (conf.number.max !== undefined)
@ -1037,7 +1043,7 @@ page.prepareUploadConfig = () => {
value = Math.min(Math.max(parsed, config[key].number.min), config[key].number.max)
}
if (value !== undefined && value !== fallback[key])
if (value !== undefined && config[key].number && value !== config[key].number.default)
localStorage[lsKeys[key]] = value
else
localStorage.removeItem(lsKeys[key])

View File

@ -7,38 +7,37 @@ page.prepareShareX = () => {
const sharexElement = document.querySelector('#ShareX')
if (!sharexElement) return
const values = page.token ? {
token: page.token || '',
albumid: page.album || ''
} : {}
values.filelength = page.fileLength || ''
values.age = page.uploadAge || ''
values.striptags = page.stripTags || ''
const headers = {}
const headers = []
const keys = Object.keys(values)
for (let i = 0; i < keys.length; i++)
// Pad by 4 space
headers.push(` "${keys[i]}": "${values[keys[i]]}"`)
if (page.token) {
headers.token = page.token || ''
headers.albumid = page.album || ''
}
headers.filelength = page.fileLength || ''
headers.age = page.uploadAge || ''
headers.striptags = page.stripTags || ''
const origin = (window.location.hostname + window.location.pathname).replace(/\/(dashboard)?$/, '')
const originClean = origin.replace(/\//g, '_')
const sharexFile = `{
"Name": "${originClean}",
"DestinationType": "ImageUploader, FileUploader",
"RequestMethod": "POST",
"RequestURL": "${window.location.protocol}//${origin}/api/upload",
"Headers": {
${headers.join(',\n')}
},
"Body": "MultipartFormData",
"FileFormName": "files[]",
"URL": "$json:files[0].url$",
"ThumbnailURL": "$json:files[0].url$"
}`
const sharexConfObj = {
Name: originClean,
DestinationType: 'ImageUploader, FileUploader',
RequestMethod: 'POST',
RequestURL: `${window.location.protocol}//${origin}/api/upload`,
Headers: headers,
Body: 'MultipartFromData',
FileFormName: 'files[]',
URL: '$json:files[0].url$',
ThumbnailURL: '$json:files[0].url$'
}
const sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' })
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' })
/* eslint-disable-next-line compat/compat */
sharexElement.setAttribute('href', URL.createObjectURL(sharexBlob))
sharexElement.setAttribute('download', `${originClean}.sxcu`)

View File

@ -1,5 +1,5 @@
{
"1": "1592210320",
"1": "1592591301",
"2": "1589010026",
"3": "1581416390",
"4": "1581416390",