enhance(server): downloadUrlでContent-Dispositionからファイル名を取得 (#10150)

* enhance(server): downloadUrlでContent-Dispositionからファイル名を取得
Resolve #10036
Resolve #4750

* untitled

* オブジェクトストレージのContent-Dispositionのファイル名の拡張子をContent-Typeに添ったものにする

* ✌️

* tiff

* fix filename

* add test

* /files/でもContent-Disposition

* comment

* fix test
This commit is contained in:
tamaina 2023-03-04 16:51:07 +09:00 committed by GitHub
parent 49f0837729
commit 2d551a8598
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 134 additions and 42 deletions

View file

@ -22,6 +22,7 @@ import { bindThis } from '@/decorators.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import { isMimeImage } from '@/misc/is-mime-image.js';
import sharp from 'sharp';
import { correctFilename } from '@/misc/correct-filename.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@ -51,15 +52,6 @@ export class FileServerService {
//this.createServer = this.createServer.bind(this);
}
@bindThis
public commonReadableHandlerGenerator(reply: FastifyReply) {
return (err: Error): void => {
this.logger.error(err);
reply.code(500);
reply.header('Cache-Control', 'max-age=300');
};
}
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addHook('onRequest', (request, reply, done) => {
@ -190,13 +182,19 @@ export class FileServerService {
}
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext)
)
);
return image.data;
}
if (file.fileRole !== 'original') {
const filename = rename(file.file.name, {
const filename = rename(file.filename, {
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
extname: file.ext ? `.${file.ext}` : undefined,
extname: file.ext ? `.${file.ext}` : '.unknown',
}).toString();
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
@ -204,12 +202,10 @@ export class FileServerService {
reply.header('Content-Disposition', contentDisposition('inline', filename));
return fs.createReadStream(file.path);
} else {
const stream = fs.createReadStream(file.path);
stream.on('error', this.commonReadableHandlerGenerator(reply));
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.file.name));
return stream;
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
return fs.createReadStream(file.path);
}
} catch (e) {
if ('cleanup' in file) file.cleanup();
@ -360,6 +356,12 @@ export class FileServerService {
reply.header('Content-Type', image.type);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext)
)
);
return image.data;
} catch (e) {
if ('cleanup' in file) file.cleanup();
@ -369,8 +371,8 @@ export class FileServerService {
@bindThis
private async getStreamAndTypeFromUrl(url: string): Promise<
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; }
| '404'
| '204'
> {
@ -386,11 +388,11 @@ export class FileServerService {
@bindThis
private async downloadAndDetectTypeFromUrl(url: string): Promise<
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; }
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
> {
const [path, cleanup] = await createTemp();
try {
await this.downloadService.downloadUrl(url, path);
const { filename } = await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
@ -398,6 +400,7 @@ export class FileServerService {
state: 'remote',
mime, ext,
path, cleanup,
filename,
};
} catch (e) {
cleanup();
@ -407,8 +410,8 @@ export class FileServerService {
@bindThis
private async getFileFromKey(key: string): Promise<
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; }
| '404'
| '204'
> {
@ -432,6 +435,7 @@ export class FileServerService {
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file,
filename: file.name,
};
}
@ -443,6 +447,7 @@ export class FileServerService {
state: 'stored_internal',
fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
file,
filename: file.name,
mime, ext,
path,
};
@ -452,6 +457,7 @@ export class FileServerService {
state: 'stored_internal',
fileRole: 'original',
file,
filename: file.name,
mime: file.type,
ext: null,
path,