mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-21 10:29:29 -05:00
Fix: preserve non-ASCII filenames in document downloads (#9702)
This commit is contained in:
parent
abf910fd93
commit
f52ebc7bf0
@ -77,6 +77,7 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
|||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { getFilenameFromContentDisposition } from 'src/app/utils/http'
|
||||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||||
import * as UTIF from 'utif'
|
import * as UTIF from 'utif'
|
||||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||||
@ -999,12 +1000,10 @@ export class DocumentDetailComponent
|
|||||||
.get(downloadUrl, { observe: 'response', responseType: 'blob' })
|
.get(downloadUrl, { observe: 'response', responseType: 'blob' })
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response: HttpResponse<Blob>) => {
|
next: (response: HttpResponse<Blob>) => {
|
||||||
const filename = response.headers
|
const contentDisposition = response.headers.get('Content-Disposition')
|
||||||
.get('Content-Disposition')
|
const filename =
|
||||||
?.split(';')
|
getFilenameFromContentDisposition(contentDisposition) ||
|
||||||
?.find((part) => part.trim().startsWith('filename='))
|
this.document.title
|
||||||
?.split('=')[1]
|
|
||||||
?.replace(/['"]/g, '')
|
|
||||||
const blob = new Blob([response.body], {
|
const blob = new Blob([response.body], {
|
||||||
type: response.body.type,
|
type: response.body.type,
|
||||||
})
|
})
|
||||||
|
31
src-ui/src/app/utils/http.spec.ts
Normal file
31
src-ui/src/app/utils/http.spec.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { getFilenameFromContentDisposition } from './http'
|
||||||
|
|
||||||
|
describe('getFilenameFromContentDisposition', () => {
|
||||||
|
it('should extract filename from Content-Disposition header with filename*', () => {
|
||||||
|
const header = "attachment; filename*=UTF-8''example%20file.txt"
|
||||||
|
expect(getFilenameFromContentDisposition(header)).toBe('example file.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should extract filename from Content-Disposition header with filename=', () => {
|
||||||
|
const header = 'attachment; filename="example-file.txt"'
|
||||||
|
expect(getFilenameFromContentDisposition(header)).toBe('example-file.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prioritize filename* over filename if both are present', () => {
|
||||||
|
const header =
|
||||||
|
'attachment; filename="fallback.txt"; filename*=UTF-8\'\'preferred%20file.txt'
|
||||||
|
const result = getFilenameFromContentDisposition(header)
|
||||||
|
expect(result).toBe('preferred file.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should gracefully fall back to null', () => {
|
||||||
|
// invalid UTF-8 sequence
|
||||||
|
expect(
|
||||||
|
getFilenameFromContentDisposition("attachment; filename*=UTF-8''%E0%A4%A")
|
||||||
|
).toBeNull()
|
||||||
|
// missing filename
|
||||||
|
expect(getFilenameFromContentDisposition('attachment;')).toBeNull()
|
||||||
|
// empty header
|
||||||
|
expect(getFilenameFromContentDisposition(null)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
23
src-ui/src/app/utils/http.ts
Normal file
23
src-ui/src/app/utils/http.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export function getFilenameFromContentDisposition(header: string): string {
|
||||||
|
if (!header) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try filename* (RFC 5987)
|
||||||
|
const filenameStar = header.match(/filename\*=(?:UTF-\d['']*)?([^;]+)/i)
|
||||||
|
if (filenameStar?.[1]) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(filenameStar[1])
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore decoding errors and fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to filename=
|
||||||
|
const filenameMatch = header.match(/filename="?([^"]+)"?/)
|
||||||
|
if (filenameMatch?.[1]) {
|
||||||
|
return filenameMatch[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user