@ -5,6 +5,7 @@ const randomstring = require('randomstring')
const Zip = require ( 'jszip' )
const paths = require ( './pathsController' )
const perms = require ( './permissionController' )
const uploadController = require ( './uploadController' )
const utils = require ( './utilsController' )
const config = require ( './../config' )
const logger = require ( './../logger' )
@ -29,14 +30,10 @@ const zipOptions = config.uploads.jsZipOptions
zipOptions . type = 'nodebuffer'
// Apply fallbacks for missing config values
if ( zipOptions . streamFiles === undefined )
zipOptions . streamFiles = true
if ( zipOptions . compression === undefined )
zipOptions . compression = 'DEFLATE'
if ( zipOptions . compressionOptions === undefined )
zipOptions . compressionOptions = { }
if ( zipOptions . compressionOptions . level === undefined )
zipOptions . compressionOptions . level = 1
if ( zipOptions . streamFiles === undefined ) zipOptions . streamFiles = true
if ( zipOptions . compression === undefined ) zipOptions . compression = 'DEFLATE'
if ( zipOptions . compressionOptions === undefined ) zipOptions . compressionOptions = { }
if ( zipOptions . compressionOptions . level === undefined ) zipOptions . compressionOptions . level = 1
self . zipEmitters = new Map ( )
@ -51,8 +48,7 @@ class ZipEmitter extends EventEmitter {
self . getUniqueRandomName = async ( ) => {
for ( let i = 0 ; i < utils . idMaxTries ; i ++ ) {
const identifier = randomstring . generate ( config . uploads . albumIdentifierLength )
if ( self . onHold . has ( identifier ) )
continue
if ( self . onHold . has ( identifier ) ) continue
// Put token on-hold (wait for it to be inserted to DB)
self . onHold . add ( identifier )
@ -78,17 +74,17 @@ self.list = async (req, res, next) => {
if ( ! user ) return
const all = req . headers . all === '1'
const sidebar = req . headers . sidebar
const simple = req . headers . simple
const ismoderator = perms . is ( user , 'moderator' )
if ( all && ! ismoderator )
return res . status ( 403 ) . end ( )
if ( all && ! ismoderator ) return res . status ( 403 ) . end ( )
const filter = function ( ) {
if ( ! all )
if ( ! all ) {
this . where ( {
enabled : 1 ,
userid : user . id
} )
}
}
try {
@ -97,16 +93,14 @@ self.list = async (req, res, next) => {
. where ( filter )
. count ( 'id as count' )
. then ( rows => rows [ 0 ] . count )
if ( ! count )
return res . json ( { success : true , albums : [ ] , count } )
if ( ! count ) return res . json ( { success : true , albums : [ ] , count } )
const fields = [ 'id' , 'name' ]
let albums
if ( sidebar ) {
if ( simple ) {
albums = await db . table ( 'albums' )
. where ( filter )
. limit ( 9 )
. select ( fields )
return res . json ( { success : true , albums , count } )
@ -116,8 +110,7 @@ self.list = async (req, res, next) => {
else if ( offset < 0 ) offset = Math . max ( 0 , Math . ceil ( count / 25 ) + offset )
fields . push ( 'identifier' , 'enabled' , 'timestamp' , 'editedAt' , 'download' , 'public' , 'description' )
if ( all )
fields . push ( 'userid' )
if ( all ) fields . push ( 'userid' )
albums = await db . table ( 'albums' )
. where ( filter )
@ -140,13 +133,14 @@ self.list = async (req, res, next) => {
. whereIn ( 'albumid' , Object . keys ( albumids ) )
. select ( 'albumid' )
for ( const upload of uploads )
if ( albumids [ upload . albumid ] )
for ( const upload of uploads ) {
if ( albumids [ upload . albumid ] ) {
albumids [ upload . albumid ] . uploads ++
}
}
// If we are not listing all albums, send response
if ( ! all )
return res . json ( { success : true , albums , count , homeDomain } )
if ( ! all ) return res . json ( { success : true , albums , count , homeDomain } )
// Otherwise proceed to querying usernames
const userids = albums
@ -156,8 +150,7 @@ self.list = async (req, res, next) => {
} )
// If there are no albums attached to a registered user, send response
if ( userids . length === 0 )
return res . json ( { success : true , albums , count , homeDomain } )
if ( ! userids . length ) return res . json ( { success : true , albums , count , homeDomain } )
// Query usernames of user IDs from currently selected files
const usersTable = await db . table ( 'users' )
@ -165,8 +158,9 @@ self.list = async (req, res, next) => {
. select ( 'id' , 'username' )
const users = { }
for ( const user of usersTable )
for ( const user of usersTable ) {
users [ user . id ] = user . username
}
return res . json ( { success : true , albums , count , users , homeDomain } )
} catch ( error ) {
@ -183,8 +177,7 @@ self.create = async (req, res, next) => {
? utils . escape ( req . body . name . trim ( ) . substring ( 0 , self . titleMaxLength ) )
: ''
if ( ! name )
return res . json ( { success : false , description : 'No album name specified.' } )
if ( ! name ) return res . json ( { success : false , description : 'No album name specified.' } )
try {
const album = await db . table ( 'albums' )
@ -195,8 +188,7 @@ self.create = async (req, res, next) => {
} )
. first ( )
if ( album )
return res . json ( { success : false , description : 'There is already an album with that name.' } )
if ( album ) return res . json ( { success : false , description : 'There is already an album with that name.' } )
const identifier = await self . getUniqueRandomName ( )
@ -235,8 +227,7 @@ self.disable = async (req, res, next) => {
const id = req . body . id
const purge = req . body . purge
if ( ! Number . isFinite ( id ) )
return res . json ( { success : false , description : 'No album specified.' } )
if ( ! Number . isFinite ( id ) ) return res . json ( { success : false , description : 'No album specified.' } )
try {
if ( purge ) {
@ -249,8 +240,7 @@ self.disable = async (req, res, next) => {
if ( files . length ) {
const ids = files . map ( file => file . id )
const failed = await utils . bulkDeleteFromDb ( 'id' , ids , user )
if ( failed . length )
return res . json ( { success : false , failed } )
if ( failed . length ) return res . json ( { success : false , failed } )
}
utils . invalidateStatsCache ( 'uploads' )
}
@ -291,24 +281,23 @@ self.edit = async (req, res, next) => {
const ismoderator = perms . is ( user , 'moderator' )
const id = parseInt ( req . body . id )
if ( isNaN ( id ) )
return res . json ( { success : false , description : 'No album specified.' } )
if ( isNaN ( id ) ) return res . json ( { success : false , description : 'No album specified.' } )
const name = typeof req . body . name === 'string'
? utils . escape ( req . body . name . trim ( ) . substring ( 0 , self . titleMaxLength ) )
: ''
if ( ! name )
return res . json ( { success : false , description : 'No name specified.' } )
if ( ! name ) return res . json ( { success : false , description : 'No name specified.' } )
const filter = function ( ) {
this . where ( 'id' , id )
if ( ! ismoderator )
if ( ! ismoderator ) {
this . andWhere ( {
enabled : 1 ,
userid : user . id
} )
}
}
try {
@ -316,13 +305,14 @@ self.edit = async (req, res, next) => {
. where ( filter )
. first ( )
if ( ! album )
if ( ! album ) {
return res . json ( { success : false , description : 'Could not get album with the specified ID.' } )
else if ( album . id !== id )
} else if ( album . id !== id ) {
return res . json ( { success : false , description : 'Name already in use.' } )
else if ( req . _old && ( album . id === id ) )
} else if ( req . _old && ( album . id === id ) ) {
// Old rename API
return res . json ( { success : false , description : 'You did not specify a new name.' } )
}
const update = {
name ,
@ -333,11 +323,13 @@ self.edit = async (req, res, next) => {
: ''
}
if ( ismoderator )
if ( ismoderator && typeof req . body . enabled !== 'undefined' ) {
update . enabled = Boolean ( req . body . enabled )
}
if ( req . body . requestLink )
if ( req . body . requestLink ) {
update . identifier = await self . getUniqueRandomName ( )
}
await db . table ( 'albums' )
. where ( filter )
@ -353,10 +345,9 @@ self.edit = async (req, res, next) => {
const oldZip = path . join ( paths . zips , ` ${ album . identifier } .zip ` )
const newZip = path . join ( paths . zips , ` ${ update . identifier } .zip ` )
await paths . rename ( oldZip , newZip )
} catch ( err ) {
} catch ( error ) {
// Re-throw error
if ( err . code !== 'ENOENT' )
throw err
if ( error . code !== 'ENOENT' ) throw error
}
return res . json ( {
@ -380,8 +371,9 @@ self.rename = async (req, res, next) => {
self . get = async ( req , res , next ) => {
const identifier = req . params . identifier
if ( identifier === undefined )
if ( identifier === undefined ) {
return res . status ( 401 ) . json ( { success : false , description : 'No identifier provided.' } )
}
try {
const album = await db . table ( 'albums' )
@ -391,16 +383,9 @@ self.get = async (req, res, next) => {
} )
. first ( )
if ( ! album )
return res . json ( {
success : false ,
description : 'Album not found.'
} )
else if ( album . public === 0 )
return res . status ( 403 ) . json ( {
success : false ,
description : 'This album is not available for public.'
} )
if ( ! album || album . public === 0 ) {
return res . status ( 404 ) . json ( { success : false , description : 'The album could not be found.' } )
}
const title = album . name
const files = await db . table ( 'files' )
@ -409,16 +394,24 @@ self.get = async (req, res, next) => {
. orderBy ( 'id' , 'desc' )
for ( const file of files ) {
file . file = ` ${ config . domain } / ${ file . name } `
if ( req . _upstreamCompat ) {
file . url = ` ${ config . domain } / ${ file . name } `
} else {
file . file = ` ${ config . domain } / ${ file . name } `
}
const extname = utils . extname ( file . name )
if ( utils . mayGenerateThumb ( extname ) )
if ( utils . mayGenerateThumb ( extname ) ) {
file . thumb = ` ${ config . domain } /thumbs/ ${ file . name . slice ( 0 , - extname . length ) } .png `
if ( req . _upstreamCompat ) file . thumbSquare = file . thumb
}
}
return res . json ( {
success : true ,
description : 'Successfully retrieved files.' ,
title ,
download : Boolean ( album . download ) ,
count : files . length ,
files
} )
@ -432,17 +425,19 @@ self.generateZip = async (req, res, next) => {
const versionString = parseInt ( req . query . v )
const identifier = req . params . identifier
if ( identifier === undefined )
if ( identifier === undefined ) {
return res . status ( 401 ) . json ( {
success : false ,
description : 'No identifier provided.'
} )
}
if ( ! config . uploads . generateZips )
if ( ! config . uploads . generateZips ) {
return res . status ( 401 ) . json ( {
success : false ,
description : 'Zip generation disabled.'
description : 'ZIP generation disabled.'
} )
}
try {
const album = await db . table ( 'albums' )
@ -452,32 +447,35 @@ self.generateZip = async (req, res, next) => {
} )
. first ( )
if ( ! album )
if ( ! album ) {
return res . json ( { success : false , description : 'Album not found.' } )
else if ( album . download === 0 )
} else if ( album . download === 0 ) {
return res . json ( { success : false , description : 'Download for this album is disabled.' } )
}
if ( ( isNaN ( versionString ) || versionString <= 0 ) && album . editedAt )
if ( ( isNaN ( versionString ) || versionString <= 0 ) && album . editedAt ) {
return res . redirect ( ` ${ album . identifier } ?v= ${ album . editedAt } ` )
}
if ( album . zipGeneratedAt > album . editedAt )
if ( album . zipGeneratedAt > album . editedAt ) {
try {
const filePath = path . join ( paths . zips , ` ${ identifier } .zip ` )
await paths . access ( filePath )
return res . download ( filePath , ` ${ album . name } .zip ` )
} catch ( error ) {
// Re-throw error
if ( error . code !== 'ENOENT' )
throw error
if ( error . code !== 'ENOENT' ) throw error
}
}
if ( self . zipEmitters . has ( identifier ) ) {
logger . log ( ` Waiting previous zip task for album: ${ identifier } . ` )
return self . zipEmitters . get ( identifier ) . once ( 'done' , ( filePath , fileName , json ) => {
if ( filePath && fileName )
if ( filePath && fileName ) {
res . download ( filePath , fileName )
else if ( json )
} else if ( json ) {
res . json ( json )
}
} )
}
@ -554,13 +552,50 @@ self.generateZip = async (req, res, next) => {
}
}
self . listFiles = async ( req , res , next ) => {
if ( req . params . page === undefined ) {
// Map to /api/album/get, but with lolisafe upstream compatibility, when accessed with this API route
req . params . identifier = req . params . id
delete req . params . id
req . _upstreamCompat = true
res . _json = res . json
res . json = ( body = { } ) => {
// Rebuild JSON payload to match lolisafe upstream
const rebuild = { }
const maps = {
success : null ,
description : 'message' ,
title : 'name' ,
download : 'downloadEnabled' ,
count : null
}
Object . keys ( body ) . forEach ( key => {
if ( maps [ key ] !== undefined ) {
if ( maps [ key ] ) rebuild [ maps [ key ] ] = body [ key ]
} else {
rebuild [ key ] = body [ key ]
}
} )
if ( rebuild . message ) rebuild . message = rebuild . message . replace ( /\.$/ , '' )
return res . _json ( rebuild )
}
return self . get ( req , res , next )
} else {
return uploadController . list ( req , res , next )
}
}
self . addFiles = async ( req , res , next ) => {
const user = await utils . authorize ( req , res )
if ( ! user ) return
const ids = req . body . ids
if ( ! Array . isArray ( ids ) || ! ids . length )
if ( ! Array . isArray ( ids ) || ! ids . length ) {
return res . json ( { success : false , description : 'No files specified.' } )
}
let albumid = parseInt ( req . body . albumid )
if ( isNaN ( albumid ) || albumid < 0 ) albumid = null
@ -572,16 +607,18 @@ self.addFiles = async (req, res, next) => {
const album = await db . table ( 'albums' )
. where ( 'id' , albumid )
. where ( function ( ) {
if ( user . username !== 'root' )
if ( user . username !== 'root' ) {
this . where ( 'userid' , user . id )
}
} )
. first ( )
if ( ! album )
if ( ! album ) {
return res . json ( {
success : false ,
description : 'Album does not exist or it does not belong to the user.'
} )
}
albumids . push ( albumid )
}
@ -597,8 +634,9 @@ self.addFiles = async (req, res, next) => {
. update ( 'albumid' , albumid )
files . forEach ( file => {
if ( file . albumid && ! albumids . includes ( file . albumid ) )
if ( file . albumid && ! albumids . includes ( file . albumid ) ) {
albumids . push ( file . albumid )
}
} )
await db . table ( 'albums' )
@ -609,13 +647,14 @@ self.addFiles = async (req, res, next) => {
return res . json ( { success : true , failed } )
} catch ( error ) {
logger . error ( error )
if ( failed . length === ids . length )
if ( failed . length === ids . length ) {
return res . json ( {
success : false ,
description : ` Could not ${ albumid === null ? 'add' : 'remove' } any files ${ albumid === null ? 'to' : 'from' } the album. `
} )
else
} else {
return res . status ( 500 ) . json ( { success : false , description : 'An unexpected error occurred. Try again?' } )
}
}
}