1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import type { DriveFilesRepository, MiDriveFile } from '@/models/_.js';
import { createTemp } from '@/misc/create-temp.js';
import type { DownloadService } from '@/core/DownloadService.js';
import type { FileInfoService } from '@/core/FileInfoService.js';
import type { InternalStorageService } from '@/core/InternalStorageService.js';
export type DownloadedFileResult = {
kind: 'downloaded';
mime: string;
ext: string | null;
path: string;
cleanup: () => void;
filename: string;
};
export type FileResolveResult =
| { kind: 'not-found' }
| { kind: 'unavailable' }
| {
kind: 'stored';
fileRole: 'thumbnail' | 'webpublic' | 'original';
file: MiDriveFile;
filename: string;
mime: string;
ext: string | null;
path: string;
}
| {
kind: 'remote';
fileRole: 'thumbnail' | 'webpublic' | 'original';
file: MiDriveFile;
filename: string;
url: string;
mime: string;
ext: string | null;
path: string;
cleanup: () => void;
};
export class FileServerFileResolver {
constructor(
private driveFilesRepository: DriveFilesRepository,
private fileInfoService: FileInfoService,
private downloadService: DownloadService,
private internalStorageService: InternalStorageService,
) {}
public async downloadAndDetectTypeFromUrl(url: string): Promise<DownloadedFileResult> {
const [path, cleanup] = await createTemp();
try {
const { filename } = await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
kind: 'downloaded',
mime, ext,
path, cleanup,
filename,
};
} catch (e) {
cleanup();
throw e;
}
}
public async resolveFileByAccessKey(key: string): Promise<FileResolveResult> {
// Fetch drive file
const file = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.accessKey = :accessKey', { accessKey: key })
.orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
.getOne();
if (file == null) return { kind: 'not-found' };
const isThumbnail = file.thumbnailAccessKey === key;
const isWebpublic = file.webpublicAccessKey === key;
if (!file.storedInternal) {
if (!(file.isLink && file.uri)) return { kind: 'unavailable' };
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
const { kind: _kind, ...downloaded } = result;
file.size = (await fs.promises.stat(downloaded.path)).size; // DB file.sizeは正確とは限らないので
return {
kind: 'remote',
...downloaded,
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file,
filename: file.name,
};
}
const path = this.internalStorageService.resolvePath(key);
if (isThumbnail || isWebpublic) {
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
kind: 'stored',
fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
file,
filename: file.name,
mime, ext,
path,
};
}
return {
kind: 'stored',
fileRole: 'original',
file,
filename: file.name,
// 古いファイルは修正前のmimeを持っているのでできるだけ修正してあげる
mime: this.fileInfoService.fixMime(file.type),
ext: null,
path,
};
}
}
|