fix: bad file name downloading non-ascii files with firefox on android #790
Massimo Melina committed
Oct 30, 2024 at 16:03 UTC
2926a603bbf02be5ff958a794dcc5271d5c1daa7
4 files changed
+18
-8
src/api.log.ts
+2
-2
@@ -5,7 +5,7 @@ import { HTTP_NOT_ACCEPTABLE, HTTP_NOT_FOUND, wait } from './cross'
5
import events from './events'
6
import { loggers } from './log'
7
import { SendListReadable } from './SendList'
8
-import { serveFile } from './serveFile'
8
+import { forceDownload, serveFile } from './serveFile'
9
import { ips } from './ips'
10
11
export default {
@@ -15,7 +15,7 @@ export default {
15
throw HTTP_NOT_FOUND
16
if (!log.path)
17
throw HTTP_NOT_ACCEPTABLE
18
- ctx.attachment(log.path)
18
+ forceDownload(ctx, log.path)
19
if (range)
20
ctx.request.header.range = `bytes=${range}`
21
if (ctx.method === 'POST') // this would cause method_not_allowed
src/errorPages.ts
+1
-1
@@ -12,7 +12,7 @@ export function getErrorSections() {
12
// to be used with errors whose recipient is possibly human
13
export async function sendErrorPage(ctx: Koa.Context, code=ctx.status) {
14
ctx.type = 'text'
15
- ctx.set('content-disposition', '') // reset ctx.attachment
15
+ ctx.set('content-disposition', '') // reset ctx.attachment (or forceDownload)
16
ctx.status = code
17
const msg = HTTP_MESSAGES[ctx.status]
18
if (!msg) return
src/serveFile.ts
+13
-3
@@ -16,12 +16,23 @@ import { getCurrentUsername } from './auth'
16
import { sendErrorPage } from './errorPages'
17
import { Readable } from 'stream'
18
import { createHash } from 'crypto'
19
+import iconv from 'iconv-lite'
20
21
const allowedReferer = defineConfig('allowed_referer', '')
22
const maxDownloads = downloadLimiter(defineConfig(CFG.max_downloads, 0), () => true)
23
const maxDownloadsPerIp = downloadLimiter(defineConfig(CFG.max_downloads_per_ip, 0), ctx => ctx.ip)
24
const maxDownloadsPerAccount = downloadLimiter(defineConfig(CFG.max_downloads_per_account, 0), ctx => getCurrentUsername(ctx) || undefined)
25
26
+function toAsciiEquivalent(s: string) {
27
+ return iconv.encode(iconv.decode(Buffer.from(s), 'utf-8'), 'ascii').toString().replaceAll('?', '')
28
+}
29
+
30
+export function forceDownload(ctx: Koa.Context, name='') {
31
+ // ctx.attachment is not working well on Windows. Eg: for file "èÖ.txt" it is producing `Content-Disposition: attachment; filename="??.txt"`. Koa uses module content-disposition, that actually produces a better result anyway: ``
32
+ ctx.set('Content-Disposition', 'attachment'
33
+ + (name && `; filename="${toAsciiEquivalent(name)}"; filename*=UTF-8''${encodeURI(name).replace(/#/g, '%23')}`))
34
+}
35
+
36
export async function serveFileNode(ctx: Koa.Context, node: VfsNode) {
37
const { source, mime } = node
38
const name = getNodeName(node)
@@ -37,7 +48,7 @@ export async function serveFileNode(ctx: Koa.Context, node: VfsNode) {
48
ctx.vfsNode = // legacy pre-0.51 (download-quota)
49
ctx.state.vfsNode = node // useful to tell service files from files shared by the user
50
if ('dl' in ctx.query) // please, download
40
- ctx.attachment(name)
51
+ forceDownload(ctx, name)
52
else if (ctx.get('referer')?.endsWith('/') && with_(ctx.get('accept'), x => x && !x.includes('text')))
53
ctx.state.considerAsGui = true
54
await serveFile(ctx, source||'', mimeString)
@@ -63,8 +74,7 @@ const cacheControlDiskFiles = defineConfig('cache_control_disk_files', 5)
74
export async function serveFile(ctx: Koa.Context, source:string, mime?:string, content?: string | Buffer) {
75
if (!source)
76
return
66
- const fn = basename(source)
67
- mime = mime ?? mimeCfg.compiled()(fn)
77
+ mime ??= mimeCfg.compiled()(basename(source))
78
if (mime === undefined || mime === MIME_AUTO)
79
mime = mimetypes.lookup(source) || ''
80
if (mime)
src/zip.ts
+2
-2
@@ -8,7 +8,7 @@ import { createReadStream } from 'fs'
8
import fs from 'fs/promises'
9
import { defineConfig } from './config'
10
import { basename, dirname } from 'path'
11
-import { applyRange, monitorAsDownload } from './serveFile'
11
+import { applyRange, forceDownload, monitorAsDownload } from './serveFile'
12
import { HTTP_OK } from './const'
13
14
// expects 'node' to have had permissions checked by caller
@@ -19,7 +19,7 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
19
ctx.mime = 'zip'
20
// ctx.query.list is undefined | string | string[]
21
const name = list?.length === 1 ? safeDecodeURIComponent(basename(list[0]!)) : getNodeName(node)
22
- ctx.attachment((isWindowsDrive(name) ? name[0] : (name || 'archive')) + '.zip')
22
+ forceDownload(ctx, (isWindowsDrive(name) ? name[0] : (name || 'archive')) + '.zip')
23
const filter = pattern2filter(String(ctx.query.search||''))
24
const walker = !list ? walkNode(node, { ctx, requiredPerm: 'can_archive' })
25
: (async function*(): AsyncIterableIterator<VfsNode> {