mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-31 07:11:33 +00:00
feat: in-memory caching of content-disposition
please read config.sample.js ignore if not serving files with node or not having the option turned on
This commit is contained in:
parent
ad4c2c2e96
commit
8a1ff434d9
@ -38,11 +38,25 @@ module.exports = {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
If you serve files with node, you can optionally choose to set Content-Disposition header
|
If you serve files with node, you can optionally choose to set Content-Disposition header
|
||||||
into their original file names. This allows users to save files into their original file names.
|
with their original file names. This allows users to download files into their original file names.
|
||||||
|
|
||||||
This will query the DB every time users access uploaded files as there's no caching mechanism.
|
"contentDispositionOptions" configures in-memory caching options,
|
||||||
|
as it would otherwise have to query database every single time.
|
||||||
|
|
||||||
|
If enabled, but "contentDispositionOptions" is missing, it will use these defaults:
|
||||||
|
{ limit: 50, strategy: 'lastGetTime' }
|
||||||
*/
|
*/
|
||||||
setContentDisposition: false,
|
setContentDisposition: false,
|
||||||
|
contentDispositionOptions: {
|
||||||
|
limit: 50,
|
||||||
|
/*
|
||||||
|
Available strategies: lastGetTime, getsCount
|
||||||
|
|
||||||
|
lastGetTime: when cache store exceeds limit, remove cache with oldest access time
|
||||||
|
getsCount: when cache store exceeds limit, remove cache with fewest access count
|
||||||
|
*/
|
||||||
|
strategy: 'lastGetTime'
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
If you serve files with node, you can optionally choose to
|
If you serve files with node, you can optionally choose to
|
||||||
|
124
controllers/utils/SimpleDataStore.js
Normal file
124
controllers/utils/SimpleDataStore.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
const STRATEGIES = [
|
||||||
|
'lastGetTime',
|
||||||
|
'getsCount'
|
||||||
|
]
|
||||||
|
|
||||||
|
class SimpleDataStore {
|
||||||
|
#store
|
||||||
|
#limit
|
||||||
|
#strategy
|
||||||
|
|
||||||
|
constructor (options = {}) {
|
||||||
|
if (typeof options !== 'object') {
|
||||||
|
throw new TypeError('Missing options object.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(options.limit) || options.limit <= 1) {
|
||||||
|
throw new TypeError('Limit must be a finite number that is at least 2.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!STRATEGIES.includes(options.strategy)) {
|
||||||
|
throw new TypeError(`Strategy must be one of these: ${STRATEGIES.map(s => `"${s}"`).join(', ')}.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#store = new Map()
|
||||||
|
this.#limit = options.limit
|
||||||
|
this.#strategy = options.strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
clear () {
|
||||||
|
return this.#store.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete (key) {
|
||||||
|
return this.#store.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
get (key) {
|
||||||
|
const entry = this.#store.get(key)
|
||||||
|
if (typeof entry === 'undefined') return entry
|
||||||
|
|
||||||
|
switch (this.#strategy) {
|
||||||
|
case STRATEGIES[0]:
|
||||||
|
entry.stratval = Date.now()
|
||||||
|
break
|
||||||
|
case STRATEGIES[1]:
|
||||||
|
entry.stratval++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#store.set(key, entry)
|
||||||
|
return entry.value
|
||||||
|
}
|
||||||
|
|
||||||
|
getStalest () {
|
||||||
|
let stalest = null
|
||||||
|
switch (this.#strategy) {
|
||||||
|
// both "lastGetTime" and "getsCount" simply must find lowest value
|
||||||
|
// to determine the stalest entry
|
||||||
|
case STRATEGIES[0]:
|
||||||
|
case STRATEGIES[1]:
|
||||||
|
for (const entry of this.#store) {
|
||||||
|
if (!stalest || entry[1].stratval < stalest[1].stratval) {
|
||||||
|
stalest = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// return its key only
|
||||||
|
return stalest[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
set (key, value) {
|
||||||
|
if (this.#store.size >= this.#limit) {
|
||||||
|
const stalest = this.getStalest()
|
||||||
|
if (stalest) {
|
||||||
|
this.#store.delete(stalest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stratval
|
||||||
|
switch (this.#strategy) {
|
||||||
|
case STRATEGIES[0]:
|
||||||
|
stratval = Date.now()
|
||||||
|
break
|
||||||
|
case STRATEGIES[1]:
|
||||||
|
stratval = 0
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#store.set(key, { value, stratval }) && true
|
||||||
|
}
|
||||||
|
|
||||||
|
get limit () {
|
||||||
|
return this.#limit
|
||||||
|
}
|
||||||
|
|
||||||
|
set limit (_) {
|
||||||
|
throw Error('This property is read-only.')
|
||||||
|
}
|
||||||
|
|
||||||
|
get size () {
|
||||||
|
return this.#store.size
|
||||||
|
}
|
||||||
|
|
||||||
|
set size (_) {
|
||||||
|
throw Error('This property is read-only.')
|
||||||
|
}
|
||||||
|
|
||||||
|
get strategy () {
|
||||||
|
return this.#strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
set strategy (_) {
|
||||||
|
throw Error('This property is read-only.')
|
||||||
|
}
|
||||||
|
|
||||||
|
get store () {
|
||||||
|
// return shallow copy
|
||||||
|
return new Map(this.#store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SimpleDataStore
|
@ -66,7 +66,9 @@ const self = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
periods: {},
|
periods: {},
|
||||||
default: {}
|
default: {}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
contentDispositionStore: null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remember old renderer, if overridden, or proxy to default renderer
|
// Remember old renderer, if overridden, or proxy to default renderer
|
||||||
@ -654,11 +656,15 @@ self.bulkDeleteFromDb = async (field, values, user) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push album ids
|
|
||||||
unlinked.forEach(file => {
|
unlinked.forEach(file => {
|
||||||
|
// Push album ids
|
||||||
if (file.albumid && !albumids.includes(file.albumid)) {
|
if (file.albumid && !albumids.includes(file.albumid)) {
|
||||||
albumids.push(file.albumid)
|
albumids.push(file.albumid)
|
||||||
}
|
}
|
||||||
|
// Delete form Content-Disposition store if used
|
||||||
|
if (self.contentDispositionStore) {
|
||||||
|
self.contentDispositionStore.delete(file.name)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Push unlinked files
|
// Push unlinked files
|
||||||
|
34
lolisafe.js
34
lolisafe.js
@ -139,6 +139,13 @@ const overrideContentTypes = contentTypes && contentTypes.length && function (re
|
|||||||
|
|
||||||
const initServeStaticUploads = (opts = {}) => {
|
const initServeStaticUploads = (opts = {}) => {
|
||||||
if (config.setContentDisposition) {
|
if (config.setContentDisposition) {
|
||||||
|
const SimpleDataStore = require('./controllers/utils/SimpleDataStore')
|
||||||
|
utils.contentDispositionStore = new SimpleDataStore(
|
||||||
|
config.contentDispositionOptions || {
|
||||||
|
limit: 50,
|
||||||
|
strategy: 'lastGetTime'
|
||||||
|
}
|
||||||
|
)
|
||||||
opts.preSetHeaders = async (res, req, path, stat) => {
|
opts.preSetHeaders = async (res, req, path, stat) => {
|
||||||
try {
|
try {
|
||||||
// Do only if accessing files from uploads' root directory (i.e. not thumbs, etc.)
|
// Do only if accessing files from uploads' root directory (i.e. not thumbs, etc.)
|
||||||
@ -146,20 +153,33 @@ const initServeStaticUploads = (opts = {}) => {
|
|||||||
const relpath = path.replace(paths.uploads, '')
|
const relpath = path.replace(paths.uploads, '')
|
||||||
if (relpath.indexOf('/', 1) === -1 && req.method === 'GET') {
|
if (relpath.indexOf('/', 1) === -1 && req.method === 'GET') {
|
||||||
const name = relpath.substring(1)
|
const name = relpath.substring(1)
|
||||||
const _file = await utils.db.table('files')
|
let original = utils.contentDispositionStore.get(name)
|
||||||
.where('name', name)
|
if (!original) {
|
||||||
.select('original')
|
original = await utils.db.table('files')
|
||||||
.first()
|
.where('name', name)
|
||||||
res.set('Content-Disposition', contentDisposition(_file.original, { type: 'inline' }))
|
.select('original')
|
||||||
|
.first()
|
||||||
|
.then(_file => {
|
||||||
|
utils.contentDispositionStore.set(name, _file.original)
|
||||||
|
return _file.original
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (original) {
|
||||||
|
res.set('Content-Disposition', contentDisposition(original, { type: 'inline' }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// serveStatic is provided with @bobbywibowo/serve-static, a fork of express/serve-static.
|
// serveStatic is provided with @bobbywibowo/serve-static, a fork of express/serve-static.
|
||||||
// The fork allows specifying an async setHeaders function by the name preSetHeaders.
|
// The fork allows specifying an async function by the name preSetHeaders,
|
||||||
// It will await the said function before creating 'send' stream to client.
|
// which it will await before creating 'send' stream to client.
|
||||||
|
// This is necessary due to database queries being async tasks,
|
||||||
|
// and express/serve-static not having the functionality by default.
|
||||||
safe.use('/', require('@bobbywibowo/serve-static')(paths.uploads, opts))
|
safe.use('/', require('@bobbywibowo/serve-static')(paths.uploads, opts))
|
||||||
|
logger.debug('Inititated SimpleDataStore for Content-Disposition: ' +
|
||||||
|
`{ limit: ${utils.contentDispositionStore.limit}, strategy: "${utils.contentDispositionStore.strategy}" }`)
|
||||||
} else {
|
} else {
|
||||||
safe.use('/', express.static(paths.uploads, opts))
|
safe.use('/', express.static(paths.uploads, opts))
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user