This commit is contained in:
Trevor Renshaw 2026-07-10 06:51:13 +02:00 committed by GitHub
commit 6a1363cb51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 281 additions and 6 deletions

View file

@ -301,6 +301,11 @@ class Server {
app.disable('x-powered-by')
this.server = http.createServer(app)
// Extend keep-alive timeout to 120s. Node's default (5s) closes idle
// connections quickly, forcing a new TCP handshake for each file when a
// client downloads multiple files in sequence (e.g. individual chapters).
this.server.keepAliveTimeout = 120000
this.server.headersTimeout = 125000
// Skip file upload parsing for internal-api routes (Next.js proxies read multipart bodies).
router.use(

View file

@ -184,7 +184,10 @@ class LibraryItemController {
if (audioMimeType) {
res.setHeader('Content-Type', audioMimeType)
}
await new Promise((resolve, reject) => res.download(libraryItemPath, req.libraryItem.relPath, (error) => (error ? reject(error) : resolve())))
// Use sendFile so the `send` module sets Content-Length, Accept-Ranges,
// and handles range requests natively — no extra stat() call needed.
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(Path.basename(req.libraryItem.relPath))}"`)
await new Promise((resolve, reject) => res.sendFile(libraryItemPath, (error) => (error ? reject(error) : resolve())))
} else {
const filename = `${itemTitle}.zip`
await zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
@ -1097,7 +1100,10 @@ class LibraryItemController {
}
try {
await new Promise((resolve, reject) => res.download(libraryFile.metadata.path, libraryFile.metadata.filename, (error) => (error ? reject(error) : resolve())))
// Use sendFile so the `send` module sets Content-Length, Accept-Ranges,
// and handles range requests natively — no extra stat() call needed.
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(libraryFile.metadata.filename)}"`)
await new Promise((resolve, reject) => res.sendFile(libraryFile.metadata.path, (error) => (error ? reject(error) : resolve())))
Logger.info(`[LibraryItemController] Downloaded file "${libraryFile.metadata.path}"`)
} catch (error) {
Logger.error(`[LibraryItemController] Failed to download file "${libraryFile.metadata.path}"`, error)

View file

@ -318,8 +318,9 @@ module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => {
const totalSize = parseInt(response.headers['content-length'], 10)
let downloadedSize = 0
// Write to filepath
const writer = fs.createWriteStream(filepath)
// Use a 512 KiB write buffer. Node's default 16 KiB highWaterMark causes
// many small write syscalls for large audio files and limits throughput.
const writer = fs.createWriteStream(filepath, { highWaterMark: 524288 })
response.data.pipe(writer)
let lastProgress = 0

View file

@ -9,7 +9,11 @@ module.exports.zipDirectoryPipe = (path, filename, res) => {
res.attachment(filename)
const archive = archiver('zip', {
zlib: { level: 0 } // Sets the compression level.
zlib: { level: 0 }, // Sets the compression level (0 = store, no CPU overhead).
// Increase the internal pipeline highWaterMark from 16 KiB to 512 KiB.
// This allows archiver to buffer more data before applying backpressure,
// keeping the read-from-disk and write-to-network pipelines busy.
highWaterMark: 524288
})
// listen for all archive data to be written
@ -67,7 +71,9 @@ module.exports.zipDirectoriesPipe = (pathObjects, filename, res) => {
res.attachment(filename)
const archive = archiver('zip', {
zlib: { level: 0 } // Sets the compression level.
zlib: { level: 0 }, // Sets the compression level (0 = store, no CPU overhead).
// Increase the internal pipeline highWaterMark from 16 KiB to 512 KiB.
highWaterMark: 524288
})
// listen for all archive data to be written