diff --git a/controllers/albumsController.js b/controllers/albumsController.js index bf67d05..aaec9c8 100644 --- a/controllers/albumsController.js +++ b/controllers/albumsController.js @@ -394,14 +394,20 @@ self.generateZip = async (req, res, next) => { if ((isNaN(versionString) || versionString <= 0) && album.editedAt) return res.redirect(`${album.identifier}?v=${album.editedAt}`) - if (album.zipGeneratedAt > album.editedAt) { - const filePath = path.join(paths.zips, `${identifier}.zip`) - const exists = await new Promise(resolve => fs.access(filePath, error => resolve(!error))) - if (exists) { - const fileName = `${album.name}.zip` - return download(filePath, fileName) + // TODO: editedAt column will now be updated whenever + // a user is simply editing the album's name/description. + // Perhaps add a new timestamp column that will only be updated + // when the files in the album are actually modified? + if (album.zipGeneratedAt > album.editedAt) + try { + const filePath = path.join(paths.zips, `${identifier}.zip`) + await paths.access(filePath) + return download(filePath, `${album.name}.zip`) + } catch (error) { + // Re-throw error + if (error.code !== 'ENOENT') + throw error } - } if (self.zipEmitters.has(identifier)) { logger.log(`Waiting previous zip task for album: ${identifier}.`) @@ -447,10 +453,13 @@ self.generateZip = async (req, res, next) => { const archive = new Zip() try { - for (const file of files) { + // Since we are adding all files concurrently, + // their order in the ZIP file may not be in alphabetical order. + // However, ZIP viewers in general should sort the files themselves. + await Promise.all(files.map(async file => { const data = await paths.readFile(path.join(paths.uploads, file.name)) archive.file(file.name, data) - } + })) await new Promise((resolve, reject) => { archive.generateNodeStream(zipOptions) .pipe(fs.createWriteStream(zipPath)) diff --git a/controllers/pathsController.js b/controllers/pathsController.js index 8559653..a269b2f 100644 --- a/controllers/pathsController.js +++ b/controllers/pathsController.js @@ -51,7 +51,7 @@ const verify = [ self.init = async () => { // Check & create directories - for (const p of verify) + await Promise.all(verify.map(async p => { try { await self.access(p) } catch (err) { @@ -63,16 +63,18 @@ self.init = async () => { logger.log(`Created directory: ${p}`) } } + })) // Purge any leftover in chunks directory const uuidDirs = await self.readdir(self.chunks) - for (const uuid of uuidDirs) { + await Promise.all(uuidDirs.map(async uuid => { const root = path.join(self.chunks, uuid) const chunks = await self.readdir(root) - for (const chunk of chunks) - await self.unlink(path.join(root, chunk)) + await Promise.all(chunks.map(async chunk => + self.unlink(path.join(root, chunk)) + )) await self.rmdir(root) - } + })) } module.exports = self diff --git a/controllers/uploadController.js b/controllers/uploadController.js index a248df6..282b676 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -22,6 +22,8 @@ const maxSize = parseInt(config.uploads.maxSize) const maxSizeBytes = maxSize * 1e6 const urlMaxSizeBytes = parseInt(config.uploads.urlMaxSize) * 1e6 +const maxFilesPerUpload = 20 + const chunkedUploads = Boolean(config.uploads.chunkSize) const chunksData = {} // Hard-coded min chunk size of 1 MB (e.i. 50 MB = max 50 chunks) @@ -62,7 +64,7 @@ const executeMulter = multer({ // Chunked uploads still need to provide only 1 file field. // Otherwise, only one of the files will end up being properly stored, // and that will also be as a chunk. - files: 20 + files: maxFilesPerUpload }, fileFilter (req, file, cb) { file.extname = utils.extname(file.originalname) @@ -258,9 +260,10 @@ self.actuallyUploadFiles = async (req, res, user, albumid, age) => { if (config.filterEmptyFile && infoMap.some(file => file.data.size === 0)) { // Unlink all files when at least one file is an empty file - for (const info of infoMap) - // Continue even when encountering errors - await utils.unlinkFile(info.data.filename).catch(logger.error) + // Should continue even when encountering errors + await Promise.all(infoMap.map(info => + utils.unlinkFile(info.data.filename).catch(logger.error) + )) throw 'Empty files are not allowed.' } @@ -282,10 +285,13 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => { if (!urls || !(urls instanceof Array)) throw 'Missing "urls" property (array).' + if (urls.length > maxFilesPerUpload) + throw `Maximum ${maxFilesPerUpload} URLs at a time.` + const downloaded = [] const infoMap = [] try { - for (let url of urls) { + await Promise.all(urls.map(async url => { const original = path.basename(url).split(/[?#]/)[0] const extname = utils.extname(original) @@ -337,9 +343,9 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => { age } }) - } + })) - // If no errors found, clear cache of downloaded files + // If no errors encountered, clear cache of downloaded files downloaded.length = 0 if (utils.clamd.scanner) { @@ -351,10 +357,11 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => { await self.sendUploadResponse(req, res, 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 if (downloaded.length) - for (const file of downloaded) - // Continue even when encountering errors - await utils.unlinkFile(file).catch(logger.error) + await Promise.all(downloaded.map(file => + utils.unlinkFile(file).catch(logger.error) + )) // Re-throw error throw error @@ -400,7 +407,7 @@ self.actuallyFinishChunks = async (req, res, user) => { const infoMap = [] try { - for (const file of files) { + await Promise.all(files.map(async file => { if (chunksData[file.uuid].chunks.length > maxChunksCount) throw 'Too many chunks.' @@ -451,7 +458,7 @@ self.actuallyFinishChunks = async (req, res, user) => { } infoMap.push({ path: destination, data }) - } + })) if (utils.clamd.scanner) { const scanResult = await self.scanFiles(req, infoMap) @@ -462,10 +469,12 @@ self.actuallyFinishChunks = async (req, res, user) => { await self.sendUploadResponse(req, res, result) } catch (error) { // Clean up leftover chunks - for (const file of files) + // Should continue even when encountering errors + await Promise.all(files.map(async file => { if (chunksData[file.uuid] !== undefined) - // Continue even when encountering errors await self.cleanUpChunks(file.uuid).catch(logger.error) + })) + // Re-throw error throw error } @@ -497,8 +506,9 @@ self.combineChunks = async (destination, uuid) => { self.cleanUpChunks = async (uuid) => { // Unlink chunks - for (const chunk of chunksData[uuid].chunks) - await paths.unlink(path.join(chunksData[uuid].root, chunk)) + await Promise.all(chunksData[uuid].chunks.map(chunk => + paths.unlink(path.join(chunksData[uuid].root, chunk)) + )) // Remove UUID dir await paths.rmdir(chunksData[uuid].root) // Delete cached date @@ -509,6 +519,8 @@ self.scanFiles = async (req, infoMap) => { let foundThreat let lastIteration let errorString + // TODO: Should these be processed concurrently? + // Not sure if it'll be too much load on ClamAV. for (let i = 0; i < infoMap.length; i++) { let reply try { @@ -518,6 +530,7 @@ self.scanFiles = async (req, infoMap) => { errorString = `[ClamAV]: ${error.code !== undefined ? `${error.code}, p` : 'P'}lease contact the site owner.` break } + if (!reply.includes('OK') || reply.includes('FOUND')) { // eslint-disable-next-line no-control-regex foundThreat = reply.replace(/^stream: /, '').replace(/ FOUND\u0000$/, '') @@ -531,9 +544,10 @@ self.scanFiles = async (req, infoMap) => { return false // Unlink all files when at least one threat is found - for (const info of infoMap) - // Continue even when encountering errors - await utils.unlinkFile(info.data.filename).catch(logger.error) + // Should ontinue even when encountering errors + await Promise.all(infoMap.map(info => + utils.unlinkFile(info.data.filename).catch(logger.error) + )) return errorString || `Threat found: ${foundThreat}${lastIteration ? '' : ', and maybe more'}.` @@ -543,7 +557,7 @@ self.storeFilesToDb = async (req, res, user, infoMap) => { const files = [] const exists = [] const albumids = [] - for (const info of infoMap) { + await Promise.all(infoMap.map(async info => { // Create hash of the file const hash = await new Promise((resolve, reject) => { const result = crypto.createHash('md5') @@ -579,7 +593,7 @@ self.storeFilesToDb = async (req, res, user, infoMap) => { dbFile.original = info.data.originalname exists.push(dbFile) - continue + return } const timestamp = Math.floor(Date.now() / 1000) @@ -609,7 +623,7 @@ self.storeFilesToDb = async (req, res, user, infoMap) => { // Generate thumbs, but do not wait if (utils.mayGenerateThumb(info.data.extname)) utils.generateThumbs(info.data.filename, info.data.extname).catch(logger.error) - } + })) if (files.length) { let authorizedIds = [] diff --git a/controllers/utilsController.js b/controllers/utilsController.js index 3d81a4c..a4d9efe 100644 --- a/controllers/utilsController.js +++ b/controllers/utilsController.js @@ -333,7 +333,9 @@ self.unlinkFile = async (filename, predb) => { } self.bulkDeleteFromDb = async (field, values, user) => { - if (!user || !['id', 'name'].includes(field)) return + // Always return an empty array on failure + if (!user || !['id', 'name'].includes(field) || !values.length) + return [] // SQLITE_LIMIT_VARIABLE_NUMBER, which defaults to 999 // Read more: https://www.sqlite.org/limits.html @@ -349,20 +351,21 @@ self.bulkDeleteFromDb = async (field, values, user) => { let unlinkeds = [] const albumids = [] - for (let i = 0; i < chunks.length; i++) { + await Promise.all(chunks.map(async chunk => { const files = await db.table('files') - .whereIn(field, chunks[i]) + .whereIn(field, chunk) .where(function () { if (!ismoderator) this.where('userid', user.id) }) // Push files that could not be found in db - failed = failed.concat(chunks[i].filter(value => !files.find(file => file[field] === value))) + failed = failed.concat(chunk.filter(value => !files.find(file => file[field] === value))) // Unlink all found files const unlinked = [] - for (const file of files) + + await Promise.all(files.map(async file => { try { await self.unlinkFile(file.name, true) unlinked.push(file) @@ -370,9 +373,9 @@ self.bulkDeleteFromDb = async (field, values, user) => { logger.error(error) failed.push(file[field]) } + })) - if (!unlinked.length) - continue + if (!unlinked.length) return // Delete all unlinked files from db await db.table('files') @@ -395,7 +398,7 @@ self.bulkDeleteFromDb = async (field, values, user) => { // Push unlinked files unlinkeds = unlinkeds.concat(unlinked) - } + })) if (unlinkeds.length) { // Update albums if necessary, but do not wait @@ -448,6 +451,7 @@ self.purgeCloudflareCache = async (names, uploads, thumbs) => { // Split array into multiple arrays with max length of 30 URLs // https://api.cloudflare.com/#zone-purge-files-by-url + // TODO: Handle API rate limits const MAX_LENGTH = 30 const chunks = [] while (names.length) @@ -456,7 +460,7 @@ self.purgeCloudflareCache = async (names, uploads, thumbs) => { const url = `https://api.cloudflare.com/client/v4/zones/${config.cloudflare.zoneId}/purge_cache` const results = [] - for (const chunk of chunks) { + await Promise.all(chunks.map(async chunk => { const result = { success: false, files: chunk, @@ -482,7 +486,7 @@ self.purgeCloudflareCache = async (names, uploads, thumbs) => { } results.push(result) - } + })) return results } @@ -791,7 +795,7 @@ self.stats = async (req, res, next) => { if (album.zipGeneratedAt) identifiers.push(album.identifier) } - for (const identifier of identifiers) + await Promise.all(identifiers.map(async identifier => { try { await paths.access(path.join(paths.zips, `${identifier}.zip`)) stats.albums.zipGenerated++ @@ -800,6 +804,7 @@ self.stats = async (req, res, next) => { if (error.code !== 'ENOENT') throw error } + })) // Update cache statsCache.albums.cache = stats.albums diff --git a/database/db.js b/database/db.js index d4252ba..9297424 100644 --- a/database/db.js +++ b/database/db.js @@ -2,9 +2,6 @@ const randomstring = require('randomstring') const perms = require('./../controllers/permissionController') const logger = require('./../logger') -// TODO: Auto-detect missing columns here -// That way we will no longer need the migration script - const init = function (db) { // Create the tables we need to store galleries and files db.schema.hasTable('albums').then(exists => { diff --git a/dist/js/dashboard.js b/dist/js/dashboard.js index a72e1a7..fabfb63 100644 --- a/dist/js/dashboard.js +++ b/dist/js/dashboard.js @@ -1,2 +1,2 @@ -var lsKeys={token:"token",viewType:{uploads:"viewTypeUploads",uploadsAll:"viewTypeUploadsAll"},selected:{uploads:"selectedUploads",uploadsAll:"selectedUploadsAll",users:"selectedUsers"}},page={dom:null,token:localStorage[lsKeys.token],username:null,permissions:null,menusContainer:null,menus:[],currentView:null,views:{uploads:{type:localStorage[lsKeys.viewType.uploads],album:null,pageNum:null},uploadsAll:{type:localStorage[lsKeys.viewType.uploadsAll],filters:null,pageNum:null,all:!0},users:{pageNum:null}},selected:{uploads:[],uploadsAll:[],users:[]},checkboxes:{uploads:[],uploadsAll:[],users:[]},lastSelected:{upload:null,uploadsAll:null,user:null},selectAlbumContainer:null,cache:{uploads:{},albums:{},users:{}},clipboardJS:null,lazyLoad:null,imageExts:[".webp",".jpg",".jpeg",".gif",".png",".tiff",".tif",".svg"],videoExts:[".webm",".mp4",".wmv",".avi",".mov",".mkv"],isTriggerLoading:null,fadingIn:null,albumTitleMaxLength:70,albumDescMaxLength:4e3,unhide:function(){document.querySelector("#loader").classList.add("is-hidden"),document.querySelector("#dashboard").classList.remove("is-hidden")},onError:function(e){console.error(e);var a=document.createElement("div");return a.innerHTML=""+e.toString()+"",swal({title:"An error occurred!",icon:"error",content:a})},onAxiosError:function(e){console.error(e);var a={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"}[e.response.status]||e.response.statusText,t=e.response.data&&e.response.data.description?e.response.data.description:"There was an error with the request, please check the console for more information.";return swal(e.response.status+" "+a,t,"error")},preparePage:function(){page.token?page.verifyToken(page.token,!0):window.location="auth"},verifyToken:function(e,a){axios.post("api/tokens/verify",{token:e}).then((function(t){if(!1===t.data.success)return swal({title:"An error occurred!",text:t.data.description,icon:"error"}).then((function(){a&&(localStorage.removeItem(lsKeys.token),window.location="auth")}));axios.defaults.headers.common.token=e,localStorage[lsKeys.token]=e,page.token=e,page.username=t.data.username,page.permissions=t.data.permissions,page.prepareDashboard()})).catch(page.onAxiosError)},prepareDashboard:function(){page.dom=document.querySelector("#page"),page.dom.addEventListener("click",page.domClick,!0),page.dom.addEventListener("submit",(function(e){if(e.target&&e.target.classList.contains("prevent-default"))return e.preventDefault()}),!0),page.menusContainer=document.querySelector("#menu");for(var e=[{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},{selector:"#itemManageUploads",onclick:page.getUploads,params:{all:!0},group:"moderator"},{selector:"#itemStatistics",onclick:page.getStatistics,group:"admin"},{selector:"#itemManageUsers",onclick:page.getUsers,group:"admin"}],a=function(a){if(!e[a].group||page.permissions[e[a].group]){var t=document.querySelector(e[a].selector);t.addEventListener("click",(function(t){page.menusContainer.classList.contains("is-loading")||e[a].onclick.call(null,Object.assign({trigger:t.currentTarget},e[a].params||{}))})),t.classList.remove("is-hidden"),page.menus.push(t)}},t=0;t';e.all&&(r='\n
\n
\n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n
\n
\n ');for(var o='\n
\n '+r+'\n
\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n ',c='\n
\n
\n \n \n
\n ',d=!1,u=t.some((function(e){return void 0!==e.expirydate})),p=0;p\n \n '+l+"\n ";for(var h=document.querySelector("#table"),b=0;b'+v.name+'':f.innerHTML='

'+(v.extname||"N/A")+"

",f.innerHTML+='\n \n
\n '+(v.thumb?'\n \n \n \n \n ':"")+'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n
\n

'+v.name+"

\n

"+(v.appendix?""+v.appendix+" – ":"")+v.prettyBytes+"

\n "+(u&&v.prettyExpiryDate?'\n

EXP: '+v.prettyExpiryDate+"

":"")+"\n
\n ",h.appendChild(f),page.checkboxes[page.currentView]=h.querySelectorAll('.checkbox[data-action="select"]')}page.lazyLoad.update()}else{page.dom.innerHTML="\n "+l+"\n "+o+"\n "+c+'\n
\n \n \n \n \n \n '+(void 0===e.album?"":"")+"\n \n "+(e.all?"":"")+"\n \n "+(u?"":"")+'\n \n \n \n \n \n
File"+(e.all?"User":"Album")+"SizeIPDateExpiry date
\n
\n '+l+"\n ";for(var w=document.querySelector("#table"),y=0;y\n '+k.name+"\n "+(void 0===e.album?""+k.appendix+"":"")+"\n "+k.prettyBytes+"\n "+(e.all?""+(k.ip||"")+"":"")+"\n "+k.prettyDate+"\n "+(u?""+(k.prettyExpiryDate||"-")+"":"")+'\n \n \n \n \n \n \n \n \n \n \n \n '+(e.all?"":'\n \n \n \n \n ')+'\n \n \n \n \n \n \n ',w.appendChild(x),page.checkboxes[page.currentView]=w.querySelectorAll('.checkbox[data-action="select"]')}}var T=document.querySelector("#selectAll");T&&!d&&(T.checked=!0,T.title="Unselect all"),page.fadeAndScroll(),page.updateTrigger(e.trigger,"active"),"uploads"===page.currentView&&(page.views.uploads.album=e.album),"uploadsAll"===page.currentView&&(page.views.uploadsAll.filters=e.filters),page.views[page.currentView].pageNum=t.length?e.pageNum:0})).catch((function(a){page.updateTrigger(e.trigger),page.onAxiosError(a)}))},setUploadsView:function(e,a){localStorage[lsKeys.viewType[page.currentView]]=e,page.views[page.currentView].type=e,page.getUploads(Object.assign({trigger:a},page.views[page.currentView]))},displayPreview:function(e){var a=page.cache.uploads[e];if(a.thumb){var t=document.createElement("div");if(t.innerHTML='\n
\n \n
\n \n
\n
\n ',a.original){var n=/.[\w]+(\?|$)/.exec(a.original),s=n&&n[0]?n[0].toLowerCase():null,i=page.imageExts.includes(s),l=!i&&page.videoExts.includes(s);(i||l)&&(t.innerHTML+='\n \n ',t.querySelector("#swalOriginal").addEventListener("click",(function(e){var n=e.currentTarget;if(!n.classList.contains("is-danger")){n.classList.add("is-loading");var s=t.querySelector("#swalThumb");if(i)s.src=a.original,s.onload=function(){n.classList.add("is-hidden"),document.body.querySelector(".swal-overlay .swal-modal:not(.is-expanded)").classList.add("is-expanded")},s.onerror=function(e){e.currentTarget.classList.add("is-hidden"),n.className="button is-danger is-fullwidth",n.innerHTML='\n \n \n \n Unable to load original\n '};else if(l){s.classList.add("is-hidden");var r=document.createElement("video");r.id="swalVideo",r.controls=!0,r.autoplay=!0,r.src=a.original,s.insertAdjacentElement("afterend",r),n.classList.add("is-hidden"),document.body.querySelector(".swal-overlay .swal-modal:not(.is-expanded)").classList.add("is-expanded")}}})))}return swal({content:t,buttons:!1}).then((function(){var e=t.querySelector("#swalVideo");e&&e.remove(),document.body.querySelector(".swal-overlay .swal-modal").classList.remove("is-expanded")}))}},selectAll:function(e){for(var a=0;an&&s>n&&st&&s"),swal({content:a})},filterUploads:function(e){var a=document.querySelector("#filters").value;page.getUploads({all:!0,filters:a},e)},viewUserUploads:function(e,a){var t=page.cache.users[e];t&&(a.classList.add("is-loading"),page.getUploads({all:!0,filters:"user:"+t.username.replace(/ /g,"\\ "),trigger:document.querySelector("#itemManageUploads")}))},deleteUpload:function(e){page.postBulkDeleteUploads({all:"uploadsAll"===page.currentView,field:"id",values:[e],cb:function(a){!a.length&&page.selected[page.currentView].includes(e)&&page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(e),1),page.selected[page.currentView].length?localStorage[lsKeys.selected[page.currentView]]=JSON.stringify(page.selected[page.currentView]):delete localStorage[lsKeys.selected[page.currentView]],page.getUploads(Object.assign({autoPage:!0},page.views[page.currentView]))}})},bulkDeleteUploads:function(){if(!page.selected[page.currentView].length)return swal("An error occurred!","You have not selected any uploads.","error");page.postBulkDeleteUploads({all:"uploadsAll"===page.currentView,field:"id",values:page.selected[page.currentView],cb:function(e){e.length?page.selected[page.currentView]=page.selected[page.currentView].filter((function(a){return e.includes(a)})):page.selected[page.currentView]=[],page.selected[page.currentView].length?localStorage[lsKeys.selected[page.currentView]]=JSON.stringify(page.selected[page.currentView]):delete localStorage[lsKeys.selected[page.currentView]],page.getUploads(Object.assign({autoPage:!0},page.views[page.currentView]))}})},deleteUploadsByNames:function(e){void 0===e&&(e={});var a="";page.permissions.moderator&&(a="
Hint: You can use this feature to delete uploads by other users."),page.dom.innerHTML='\n
\n
\n \n
\n \n
\n

Separate each entry with a new line.'+a+'

\n
\n
\n
\n \n
\n
\n
\n ',page.fadeAndScroll(),page.updateTrigger(e.trigger,"active"),document.querySelector("#submitBulkDelete").addEventListener("click",(function(){var e=document.querySelector("#bulkDeleteNames"),a={},t=e.value.split(/\r?\n/).map((function(e){var a=e.trim();return/^[^\s]+$/.test(a)?a:""})).filter((function(e){return!(!e||Object.prototype.hasOwnProperty.call(a,e))&&(a[e]=!0)}));if(e.value=t.join("\n"),!t.length)return swal("An error occurred!","You have not entered any upload names.","error");page.postBulkDeleteUploads({all:!0,field:"name",values:t,cb:function(a){e.value=a.join("\n")}})}))},postBulkDeleteUploads:function(e){void 0===e&&(e={});var a=e.values.length,t=e.values.length+" upload"+(1===a?"":"s"),n="

You won't be able to recover "+t.replace(/^(\d*)(.*)/,"$1$2")+"!

";e.all&&(n+="\n

Warning: You may be nuking "+(1===a?"an upload":"some uploads")+" by "+(1===a?"another user":"other users")+"!

");var s=document.createElement("div");s.innerHTML=n,swal({title:"Are you sure?",content:s,icon:"warning",dangerMode:!0,buttons:{cancel:!0,confirm:{text:"Yes, nuke "+(1===e.values.length?"it":"them")+"!",closeModal:!1}}}).then((function(n){n&&axios.post("api/upload/bulkdelete",{field:e.fields,values:e.values}).then((function(n){if(n){if(!1===n.data.success)return"No token provided"===n.data.description?page.verifyToken(page.token):swal("An error occurred!",n.data.description,"error");var s=Array.isArray(n.data.failed)?n.data.failed:[];s.length===e.values.length?swal("An error occurred!","Unable to delete any of the "+t+".","error"):s.length&&s.length\n

You are about to add '+t+" upload"+(1===t?"":"s")+' to an album.

\n

If an upload is already in an album, it will be moved.

\n \n
\n
\n
\n \n
\n
\n
\n ',swal({icon:"warning",content:n,buttons:{cancel:!0,confirm:{text:"OK",closeModal:!1}}}).then((function(t){if(t){var n=parseInt(document.querySelector("#swalAlbum").value);if(isNaN(n))return swal("An error occurred!","You did not choose an album.","error");axios.post("api/albums/addfiles",{ids:e,albumid:n}).then((function(t){if(t)if(!1!==t.data.success){var s=e.length;t.data.failed&&t.data.failed.length&&(s-=t.data.failed.length);var i="upload"+(1===e.length?"":"s");if(!s)return swal("An error occurred!","Could not add the "+i+" to the album.","error");swal("Woohoo!","Successfully "+(n<0?"removed":"added")+" "+s+" "+i+" "+(n<0?"from":"to")+" the album.","success"),a(t.data.failed)}else"No token provided"===t.data.description?page.verifyToken(page.token):swal("An error occurred!",t.data.description,"error")})).catch(page.onAxiosError)}})),axios.get("api/albums").then((function(e){if(!1!==e.data.success){var a=document.querySelector("#swalAlbum");a&&(a.innerHTML+=e.data.albums.map((function(e){return'"})).join("\n"),a.getElementsByTagName("option")[1].innerHTML="Choose an album",a.removeAttribute("disabled"))}else"No token provided"===e.data.description?page.verifyToken(page.token):swal("An error occurred!",e.data.description,"error")})).catch(page.onAxiosError)},getAlbums:function(e){void 0===e&&(e={}),page.updateTrigger(e.trigger,"loading"),axios.get("api/albums").then((function(a){if(a){if(!1===a.data.success)return"No token provided"===a.data.description?page.verifyToken(page.token):(page.updateTrigger(e.trigger),swal("An error occurred!",a.data.description,"error"));page.cache.albums={},page.dom.innerHTML='\n

Create new album

\n
\n
\n
\n \n
\n

Max length is '+page.albumTitleMaxLength+' characters.

\n
\n
\n
\n \n
\n

Max length is '+page.albumDescMaxLength+' characters.

\n
\n
\n
\n \n
\n
\n
\n
\n

List of albums

\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n
IDNameFilesCreated atPublic link
\n
\n ';for(var t=a.data.homeDomain,n=document.querySelector("#table"),s=0;s"+i.id+"\n "+i.name+"\n "+i.files+"\n "+i.prettyDate+"\n '+l+'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ',n.appendChild(r)}page.fadeAndScroll(),page.updateTrigger(e.trigger,"active")}})).catch((function(a){page.updateTrigger(e.trigger),page.onAxiosError(a)}))},editAlbum:function(e){var a=page.cache.albums[e];if(a){var t=document.createElement("div");t.innerHTML='\n
\n
\n \n
\n

Max length is '+page.albumTitleMaxLength+' characters.

\n
\n
\n
\n \n
\n

Max length is '+page.albumDescMaxLength+' characters.

\n
\n
\n
\n \n
\n
\n
\n
\n \n
\n
\n
\n
\n \n
\n
\n ',swal({title:"Edit album",icon:"info",content:t,buttons:{cancel:!0,confirm:{closeModal:!1}}}).then((function(t){t&&axios.post("api/albums/edit",{id:e,name:document.querySelector("#swalName").value.trim(),description:document.querySelector("#swalDescription").value.trim(),download:document.querySelector("#swalDownload").checked,public:document.querySelector("#swalPublic").checked,requestLink:document.querySelector("#swalRequestLink").checked}).then((function(e){if(e){if(!1===e.data.success)return"No token provided"===e.data.description?page.verifyToken(page.token):swal("An error occurred!",e.data.description,"error");e.data.identifier?swal("Success!","Your album's new identifier is: "+e.data.identifier+".","success"):e.data.name!==a.name?swal("Success!","Your album was renamed to: "+e.data.name+".","success"):swal("Success!","Your album was edited!","success"),page.getAlbumsSidebar(),page.getAlbums()}})).catch(page.onAxiosError)}))}},deleteAlbum:function(e){swal({title:"Are you sure?",text:"This won't delete your uploads, only the album!",icon:"warning",dangerMode:!0,buttons:{cancel:!0,confirm:{text:"Yes, delete it!",closeModal:!1},purge:{text:"Umm, delete the uploads too please?",value:"purge",className:"swal-button--danger",closeModal:!1}}}).then((function(a){a&&axios.post("api/albums/delete",{id:e,purge:"purge"===a}).then((function(e){if(!1===e.data.success)return"No token provided"===e.data.description?page.verifyToken(page.token):Array.isArray(e.data.failed)&&e.data.failed.length?swal("An error occurred!","Unable to delete ","error"):swal("An error occurred!",e.data.description,"error");swal("Deleted!","Your album has been deleted.","success"),page.getAlbumsSidebar(),page.getAlbums()})).catch(page.onAxiosError)}))},submitAlbum:function(e){page.updateTrigger(e,"loading"),axios.post("api/albums",{name:document.querySelector("#albumName").value.trim(),description:document.querySelector("#albumDescription").value.trim()}).then((function(a){if(a){if(page.updateTrigger(e),!1===a.data.success)return"No token provided"===a.data.description?page.verifyToken(page.token):swal("An error occurred!",a.data.description,"error");swal("Woohoo!","Album was created successfully.","success"),page.getAlbumsSidebar(),page.getAlbums()}})).catch((function(a){page.updateTrigger(e),page.onAxiosError(a)}))},getAlbumsSidebar:function(){axios.get("api/albums/sidebar").then((function(e){if(e){if(!1===e.data.success)return"No token provided"===e.data.description?page.verifyToken(page.token):swal("An error occurred!",e.data.description,"error");var a=document.querySelector("#albumsContainer"),t=a.querySelectorAll("li > a");if(t.length){for(var n=0;n\n \n
\n
\n \n
\n
\n \n \n ',page.fadeAndScroll(),page.updateTrigger(e.trigger,"active"),document.querySelector("#getNewToken").addEventListener("click",(function(e){var a=e.currentTarget;page.updateTrigger(a,"loading"),axios.post("api/tokens/change").then((function(e){if(!1===e.data.success)return"No token provided"===e.data.description?page.verifyToken(page.token):(page.updateTrigger(a),swal("An error occurred!",e.data.description,"error"));page.updateTrigger(a),swal({title:"Woohoo!",text:"Your token was successfully changed.",icon:"success"}).then((function(){axios.defaults.headers.common.token=e.data.token,localStorage[lsKeys.token]=e.data.token,page.token=e.data.token,page.changeToken()}))})).catch((function(e){page.updateTrigger(a),page.onAxiosError(e)}))}))},changePassword:function(e){void 0===e&&(e={}),page.dom.innerHTML='\n
\n
\n \n
\n \n
\n
\n
\n \n
\n \n
\n
\n
\n
\n \n
\n
\n
\n ',page.fadeAndScroll(),page.updateTrigger(e.trigger,"active"),document.querySelector("#sendChangePassword").addEventListener("click",(function(e){page.dom.querySelector("form").checkValidity()&&(document.querySelector("#password").value===document.querySelector("#passwordConfirm").value?page.sendNewPassword(document.querySelector("#password").value,e.currentTarget):swal({title:"Password mismatch!",text:"Your passwords do not match, please try again.",icon:"error"}))}))},sendNewPassword:function(e,a){page.updateTrigger(a,"loading"),axios.post("api/password/change",{password:e}).then((function(e){if(!1===e.data.success)return"No token provided"===e.data.description?page.verifyToken(page.token):(page.updateTrigger(a),swal("An error occurred!",e.data.description,"error"));page.updateTrigger(a),swal({title:"Woohoo!",text:"Your password was successfully changed.",icon:"success"}).then((function(){page.changePassword()}))})).catch((function(e){page.updateTrigger(a),page.onAxiosError(e)}))},getUsers:function(e){if(void 0===e&&(e={}),page.updateTrigger(e.trigger,"loading"),void 0===e.pageNum&&(e.pageNum=0),!page.permissions.admin)return swal("An error occurred!","You can not do this!","error");var a="api/users/"+e.pageNum;axios.get(a).then((function(a){if(!1===a.data.success)return"No token provided"===a.data.description?page.verifyToken(page.token):(page.updateTrigger(e.trigger),swal("An error occurred!",a.data.description,"error"));if(e.pageNum&&0===a.data.users.length)return page.updateTrigger(e.trigger),swal("An error occurred!","There are no more users to populate page "+(e.pageNum+1)+".","error");page.currentView="users",page.cache.users={};var t=page.paginate(a.data.count,25,e.pageNum),n='\n
\n
\n
\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n ',s=!1;page.dom.innerHTML="\n "+t+"\n "+n+'\n \n \n \n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
IDUsernameUploadsUsageGroup
\n
\n
\n '+t+"\n ";for(var i=document.querySelector("#table"),l=0;l