cabloy 5.1.102 → 5.1.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cabloy-version +1 -1
- package/CHANGELOG.md +47 -0
- package/CLAUDE.local.md +4 -0
- package/cabloy-docs/.vitepress/config.mjs +3 -0
- package/cabloy-docs/backend/file-guide.md +148 -0
- package/cabloy-docs/backend/image-guide.md +40 -40
- package/cabloy-docs/backend/jwt-guide.md +21 -0
- package/cabloy-docs/frontend/a-model-under-the-hood.md +26 -4
- package/cabloy-docs/frontend/file-guide.md +147 -0
- package/cabloy-docs/frontend/image-guide.md +18 -9
- package/cabloy-docs/frontend/model-architecture.md +1 -1
- package/cabloy-docs/frontend/model-state-guide.md +132 -16
- package/cabloy-docs/frontend/ssr-init-data.md +12 -1
- package/cabloy-docs/frontend/use-state-data-best-practices.md +54 -0
- package/cabloy-docs/fullstack/file-workflow.md +135 -0
- package/cabloy-docs/fullstack/image-workflow.md +12 -3
- package/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +23 -23
- package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +7 -7
- package/vona/src/suite/a-training/modules/training-record/src/.metadata/index.ts +30 -4
- package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +1 -1
- package/vona/src/suite/a-training/modules/training-record/src/bean/imageScene.sceneImage.ts +1 -0
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +8 -6
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +8 -6
- package/vona/src/suite/a-training/modules/training-record/src/index.ts +0 -1
- package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +0 -1
- package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +10 -12
- package/vona/src/suite/a-training/modules/training-student/src/bean/imageScene.studentImage.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +9 -10
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +2 -2
- package/vona/src/suite-vendor/a-file/modules/a-file/package.json +3 -2
- package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +155 -35
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.file.ts +283 -54
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +46 -63
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.index.ts +12 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.redlock.ts +1 -1
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.version.ts +3 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/schedule.fileDraftPrune.ts +29 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveView.ts +45 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveViews.ts +45 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/config/config.ts +6 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +64 -56
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeRequest.ts +13 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeResponse.ts +10 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadRequest.ts +0 -3
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadResponse.ts +2 -14
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDownloadRequest.ts +3 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +3 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadResponse.ts +0 -25
- package/vona/src/{suite/a-training/modules/training-record/src/dto/recordDossierFileView.ts → suite-vendor/a-file/modules/a-file/src/dto/fileView.ts} +3 -15
- package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/file.ts +10 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileUploadUrl.ts +329 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/index.ts +1 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/file.ts +51 -10
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileProvider.ts +8 -3
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileScene.ts +1 -1
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/package.json +1 -1
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts +12 -4
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/service/fileCloudflare.ts +52 -18
- package/vona/src/suite-vendor/a-file/modules/file-native/package.json +1 -1
- package/vona/src/suite-vendor/a-file/modules/file-native/src/bean/fileProvider.native.ts +5 -5
- package/vona/src/suite-vendor/a-file/modules/file-native/src/service/fileNative.ts +7 -8
- package/vona/src/suite-vendor/a-file/package.json +4 -4
- package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +16 -19
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +99 -74
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +28 -68
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +1 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveView.ts +3 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveViews.ts +3 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +4 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +78 -34
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDeliveryRequest.ts +3 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadRequest.ts +0 -3
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +1 -27
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +6 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +2 -36
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadUrlRequest.ts +0 -3
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +4 -27
- package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +1 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +36 -29
- package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +4 -4
- package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageScene.ts +1 -0
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +8 -11
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/lib/cloudflareImageUrl.ts +4 -16
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +17 -16
- package/vona/src/suite-vendor/a-image/modules/image-native/package.json +2 -6
- package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +0 -37
- package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +13 -36
- package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +100 -113
- package/vona/src/suite-vendor/a-image/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/config/config.ts +3 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +9 -4
- package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +113 -64
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +50 -12
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +610 -114
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +51 -2
- package/zova/src/suite/a-home/modules/home-passport/src/monkey.ts +17 -3
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +50 -12
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +610 -114
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +16 -12
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +25 -25
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +18 -24
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +228 -291
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +5 -5
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +23 -5
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +72 -43
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +22 -4
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +16 -42
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +5 -25
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +10 -24
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +216 -333
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +0 -5
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/bean/tableCell.image.tsx +38 -12
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +79 -32
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/lib/preview.ts +22 -4
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/types/image.ts +3 -4
- package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +6 -6
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -5
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +29 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +11 -2
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +3 -3
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +4 -4
- package/zova/src/suite-vendor/a-zova/package.json +3 -3
- package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +0 -34
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenRequest.ts +0 -24
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenResponse.ts +0 -16
- package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +0 -57
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +0 -31
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +0 -13
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
2
|
+
import { lookup as dnsLookup } from 'node:dns/promises';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { Transform } from 'node:stream';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
9
|
+
|
|
10
|
+
import type { IFileUploadUrlPolicyResolved } from '../types/file.ts';
|
|
11
|
+
|
|
12
|
+
import { getFileExtension, matchesFileMimeType } from './fileUploadValidation.ts';
|
|
13
|
+
|
|
14
|
+
const MAX_REDIRECTS = 3;
|
|
15
|
+
const CONNECT_TIMEOUT = 10_000;
|
|
16
|
+
const RESPONSE_TIMEOUT = 15_000;
|
|
17
|
+
const READ_TIMEOUT = 15_000;
|
|
18
|
+
const OVERALL_TIMEOUT = 30_000;
|
|
19
|
+
|
|
20
|
+
export interface IRemoteFileUploadUrlInput {
|
|
21
|
+
url: string;
|
|
22
|
+
filename?: string;
|
|
23
|
+
policy: IFileUploadUrlPolicyResolved;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface IRemoteFileUploadUrlResult {
|
|
27
|
+
file: string;
|
|
28
|
+
filename: string;
|
|
29
|
+
contentType?: string;
|
|
30
|
+
size: number;
|
|
31
|
+
cleanup: () => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface IRemoteResponse {
|
|
35
|
+
request: import('node:http').ClientRequest;
|
|
36
|
+
response: import('node:http').IncomingMessage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function downloadFileUploadUrl(
|
|
40
|
+
input: IRemoteFileUploadUrlInput,
|
|
41
|
+
): Promise<IRemoteFileUploadUrlResult> {
|
|
42
|
+
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'cabloy-file-url-'));
|
|
43
|
+
let active: import('node:http').ClientRequest | import('node:http').IncomingMessage | undefined;
|
|
44
|
+
const overallTimer = setTimeout(() => {
|
|
45
|
+
active?.destroy(new Error('Remote file fetch timed out'));
|
|
46
|
+
}, OVERALL_TIMEOUT);
|
|
47
|
+
try {
|
|
48
|
+
let url = validateRemoteUrl(input.url);
|
|
49
|
+
let remote: IRemoteResponse | undefined;
|
|
50
|
+
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
|
|
51
|
+
remote = await requestRemote(url, item => {
|
|
52
|
+
active = item;
|
|
53
|
+
});
|
|
54
|
+
const location = remote.response.headers.location;
|
|
55
|
+
if (isRedirect(remote.response.statusCode) && location) {
|
|
56
|
+
if (redirects === MAX_REDIRECTS) {
|
|
57
|
+
throw new Error(`Remote file exceeded redirect limit: ${MAX_REDIRECTS}`);
|
|
58
|
+
}
|
|
59
|
+
remote.response.resume();
|
|
60
|
+
url = validateRemoteUrl(new URL(location, url).toString());
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
if (!remote || isRedirect(remote.response.statusCode)) {
|
|
66
|
+
throw new Error('Remote file redirect response is missing a location');
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
!remote.response.statusCode ||
|
|
70
|
+
remote.response.statusCode < 200 ||
|
|
71
|
+
remote.response.statusCode >= 300
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(`Remote file fetch failed: ${remote.response.statusCode ?? 0}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const filename = sanitizeFilename(input.filename) ?? filenameFromUrl(url);
|
|
77
|
+
validateExtension(filename, input.policy);
|
|
78
|
+
const contentType = normalizeContentType(remote.response.headers['content-type']);
|
|
79
|
+
validateContentType(contentType, input.policy);
|
|
80
|
+
const contentLength = parseContentLength(remote.response.headers['content-length']);
|
|
81
|
+
if (contentLength !== undefined) validateSize(contentLength, input.policy);
|
|
82
|
+
|
|
83
|
+
const file = path.join(tempDir, 'download');
|
|
84
|
+
let size = 0;
|
|
85
|
+
const limit = new Transform({
|
|
86
|
+
transform(chunk: Buffer, _encoding, callback) {
|
|
87
|
+
size += chunk.length;
|
|
88
|
+
try {
|
|
89
|
+
validateSize(size, input.policy);
|
|
90
|
+
callback(null, chunk);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
callback(error as Error);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
remote.response.setTimeout(READ_TIMEOUT, () => {
|
|
97
|
+
remote.response.destroy(new Error('Remote file read timed out'));
|
|
98
|
+
});
|
|
99
|
+
active = remote.response;
|
|
100
|
+
await pipeline(remote.response, limit, fse.createWriteStream(file, { mode: 0o600 }));
|
|
101
|
+
validateSize(size, input.policy);
|
|
102
|
+
return {
|
|
103
|
+
file,
|
|
104
|
+
filename,
|
|
105
|
+
contentType,
|
|
106
|
+
size,
|
|
107
|
+
cleanup: async () => await fse.remove(tempDir),
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
await fse.remove(tempDir);
|
|
111
|
+
throw error;
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(overallTimer);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateRemoteUrl(value: string) {
|
|
118
|
+
let url: URL;
|
|
119
|
+
try {
|
|
120
|
+
url = new URL(value);
|
|
121
|
+
} catch {
|
|
122
|
+
throw new TypeError('Remote file URL is invalid');
|
|
123
|
+
}
|
|
124
|
+
if (url.protocol !== 'https:') {
|
|
125
|
+
throw new TypeError('Remote file URL must use HTTPS');
|
|
126
|
+
}
|
|
127
|
+
if (url.username || url.password) {
|
|
128
|
+
throw new TypeError('Remote file URL must not include credentials');
|
|
129
|
+
}
|
|
130
|
+
if (url.port && url.port !== '443') {
|
|
131
|
+
throw new TypeError('Remote file URL must use port 443');
|
|
132
|
+
}
|
|
133
|
+
if (!url.hostname) {
|
|
134
|
+
throw new TypeError('Remote file URL must include a hostname');
|
|
135
|
+
}
|
|
136
|
+
return url;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function requestRemote(
|
|
140
|
+
url: URL,
|
|
141
|
+
setActive: (
|
|
142
|
+
item: import('node:http').ClientRequest | import('node:http').IncomingMessage,
|
|
143
|
+
) => void,
|
|
144
|
+
): Promise<IRemoteResponse> {
|
|
145
|
+
const address = await resolvePublicAddress(url.hostname);
|
|
146
|
+
return await new Promise<IRemoteResponse>((resolve, reject) => {
|
|
147
|
+
let settled = false;
|
|
148
|
+
const rejectOnce = (error: Error) => {
|
|
149
|
+
if (settled) return;
|
|
150
|
+
settled = true;
|
|
151
|
+
reject(error);
|
|
152
|
+
};
|
|
153
|
+
const request = https.request(
|
|
154
|
+
url,
|
|
155
|
+
{
|
|
156
|
+
agent: false,
|
|
157
|
+
lookup(hostname, _options, callback) {
|
|
158
|
+
if (hostname !== url.hostname) {
|
|
159
|
+
callback(
|
|
160
|
+
Object.assign(new Error('Remote file DNS hostname mismatch'), {
|
|
161
|
+
code: 'EHOSTUNREACH',
|
|
162
|
+
}),
|
|
163
|
+
address.address,
|
|
164
|
+
address.family,
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
callback(null, address.address, address.family);
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
response => {
|
|
172
|
+
if (settled) {
|
|
173
|
+
response.destroy();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
settled = true;
|
|
177
|
+
response.setTimeout(RESPONSE_TIMEOUT, () => {
|
|
178
|
+
response.destroy(new Error('Remote file response timed out'));
|
|
179
|
+
});
|
|
180
|
+
setActive(response);
|
|
181
|
+
resolve({ request, response });
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
request.setTimeout(CONNECT_TIMEOUT, () => {
|
|
185
|
+
request.destroy(new Error('Remote file connection timed out'));
|
|
186
|
+
});
|
|
187
|
+
request.once('error', rejectOnce);
|
|
188
|
+
setActive(request);
|
|
189
|
+
request.end();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function resolvePublicAddress(hostname: string) {
|
|
194
|
+
const literalFamily = net.isIP(hostname);
|
|
195
|
+
const addresses = literalFamily
|
|
196
|
+
? [{ address: hostname, family: literalFamily }]
|
|
197
|
+
: await dnsLookup(hostname, { all: true, verbatim: true });
|
|
198
|
+
if (!addresses.length) throw new Error('Remote file hostname did not resolve');
|
|
199
|
+
for (const address of addresses) {
|
|
200
|
+
if (!isPublicAddress(address.address, address.family)) {
|
|
201
|
+
throw new Error(`Remote file hostname resolved to a prohibited address: ${address.address}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return addresses[0]!;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isRedirect(statusCode?: number) {
|
|
208
|
+
return [301, 302, 303, 307, 308].includes(statusCode ?? 0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeContentType(value: string | string[] | undefined) {
|
|
212
|
+
const contentType = Array.isArray(value) ? value[0] : value;
|
|
213
|
+
return contentType?.split(';', 1)[0]?.trim().toLowerCase() || undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parseContentLength(value: string | string[] | undefined) {
|
|
217
|
+
const contentLength = Array.isArray(value) ? value[0] : value;
|
|
218
|
+
if (contentLength === undefined) return undefined;
|
|
219
|
+
if (!/^\d+$/.test(contentLength)) throw new Error('Remote file Content-Length is invalid');
|
|
220
|
+
const size = Number(contentLength);
|
|
221
|
+
if (!Number.isSafeInteger(size)) throw new Error('Remote file Content-Length is invalid');
|
|
222
|
+
return size;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateSize(size: number, policy: IFileUploadUrlPolicyResolved) {
|
|
226
|
+
if (policy.maxSize !== undefined && size > policy.maxSize) {
|
|
227
|
+
throw new Error(`Remote file is too large: maxSize=${policy.maxSize}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function validateContentType(
|
|
232
|
+
contentType: string | undefined,
|
|
233
|
+
policy: IFileUploadUrlPolicyResolved,
|
|
234
|
+
) {
|
|
235
|
+
if (
|
|
236
|
+
policy.mimeTypes?.length &&
|
|
237
|
+
(!contentType || !matchesFileMimeType(contentType, policy.mimeTypes))
|
|
238
|
+
) {
|
|
239
|
+
throw new Error(`Unsupported remote file mimeType: ${contentType ?? 'missing'}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function validateExtension(filename: string, policy: IFileUploadUrlPolicyResolved) {
|
|
244
|
+
const extension = getFileExtension(filename);
|
|
245
|
+
if (policy.extensions?.length && !policy.extensions.includes(extension)) {
|
|
246
|
+
throw new Error(`Unsupported remote file extension: ${extension || 'missing'}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function filenameFromUrl(url: URL) {
|
|
251
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
252
|
+
return sanitizeFilename(path.posix.basename(pathname)) ?? 'download';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function sanitizeFilename(value: string | undefined) {
|
|
256
|
+
if (!value) return undefined;
|
|
257
|
+
const filename = path
|
|
258
|
+
.basename(value.replaceAll('\\', '/'))
|
|
259
|
+
.replace(/\p{Cc}/gu, '')
|
|
260
|
+
.trim();
|
|
261
|
+
return filename || undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isPublicAddress(address: string, family: number) {
|
|
265
|
+
if (family === 4) return isPublicIpv4(address);
|
|
266
|
+
if (family === 6) return isPublicIpv6(address);
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function isPublicIpv4(address: string) {
|
|
271
|
+
const octets = address.split('.').map(Number);
|
|
272
|
+
if (
|
|
273
|
+
octets.length !== 4 ||
|
|
274
|
+
octets.some(item => !Number.isInteger(item) || item < 0 || item > 255)
|
|
275
|
+
) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
const [a, b, c] = octets;
|
|
279
|
+
if (a === 0 || a === 10 || a === 127 || a >= 224) return false;
|
|
280
|
+
if (a === 100 && b >= 64 && b <= 127) return false;
|
|
281
|
+
if (a === 169 && b === 254) return false;
|
|
282
|
+
if (a === 172 && b >= 16 && b <= 31) return false;
|
|
283
|
+
if (a === 192 && (b === 0 || b === 168)) return false;
|
|
284
|
+
if (a === 192 && b === 88 && c === 99) return false;
|
|
285
|
+
if (a === 192 && b === 0 && c === 2) return false;
|
|
286
|
+
if (a === 198 && (b === 18 || b === 19 || b === 51)) return false;
|
|
287
|
+
if (a === 203 && b === 0 && c === 113) return false;
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isPublicIpv6(address: string) {
|
|
292
|
+
const normalized = address.toLowerCase();
|
|
293
|
+
if (normalized === '::' || normalized === '::1') return false;
|
|
294
|
+
const mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
295
|
+
if (mapped) return isPublicIpv4(mapped[1]!);
|
|
296
|
+
const value = parseIpv6(normalized);
|
|
297
|
+
if (value === undefined) return false;
|
|
298
|
+
if (inIpv6Range(value, 0xfc000000000000000000000000000000n, 7)) return false;
|
|
299
|
+
if (inIpv6Range(value, 0xfe800000000000000000000000000000n, 10)) return false;
|
|
300
|
+
if (inIpv6Range(value, 0xff000000000000000000000000000000n, 8)) return false;
|
|
301
|
+
if (!inIpv6Range(value, 0x20000000000000000000000000000000n, 3)) return false;
|
|
302
|
+
if (inIpv6Range(value, 0x20010db8000000000000000000000000n, 32)) return false;
|
|
303
|
+
if (inIpv6Range(value, 0x2001000000000000000000000000000n, 32)) return false;
|
|
304
|
+
if (inIpv6Range(value, 0x2002000000000000000000000000000n, 16)) return false;
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function parseIpv6(value: string): bigint | undefined {
|
|
309
|
+
const parts = value.split('::');
|
|
310
|
+
if (parts.length > 2) return undefined;
|
|
311
|
+
const [head, tail] = parts;
|
|
312
|
+
const left = head ? head.split(':') : [];
|
|
313
|
+
const right = tail ? tail.split(':') : [];
|
|
314
|
+
if ((tail === undefined && left.length !== 8) || left.length + right.length > 8) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
const groups: string[] = [
|
|
318
|
+
...left,
|
|
319
|
+
...Array.from<string>({ length: 8 - left.length - right.length }).fill('0'),
|
|
320
|
+
...right,
|
|
321
|
+
];
|
|
322
|
+
if (groups.some(group => !/^[0-9a-f]{1,4}$/.test(group))) return undefined;
|
|
323
|
+
return groups.reduce<bigint>((result, group) => (result << 16n) + BigInt(`0x${group}`), 0n);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function inIpv6Range(value: bigint, prefix: bigint, bits: number) {
|
|
327
|
+
const mask = ((1n << BigInt(bits)) - 1n) << BigInt(128 - bits);
|
|
328
|
+
return (value & mask) === (prefix & mask);
|
|
329
|
+
}
|
|
@@ -4,12 +4,18 @@ import type { IFileProviderClientOptions, IFileProviderRecord } from './fileProv
|
|
|
4
4
|
import type { IFileSceneRecord } from './fileScene.ts';
|
|
5
5
|
|
|
6
6
|
export type TypeFileMeta = Record<string, unknown>;
|
|
7
|
-
export type
|
|
7
|
+
export type TypeFileDirectUploadExpiry = Date | string | number;
|
|
8
|
+
export type TypeFileStatus = 'draft' | 'ready' | 'expired';
|
|
8
9
|
|
|
9
10
|
export interface IFileDeliveryOptions {
|
|
10
|
-
signed?: boolean;
|
|
11
11
|
expiresIn?: number;
|
|
12
|
-
|
|
12
|
+
audience?: boolean;
|
|
13
|
+
responseMode?: 'auto' | 'buffer' | 'url';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IFileProviderDeliveryOptions {
|
|
17
|
+
protected: boolean;
|
|
18
|
+
expiresIn?: number;
|
|
13
19
|
responseMode?: 'auto' | 'buffer' | 'url';
|
|
14
20
|
}
|
|
15
21
|
|
|
@@ -25,6 +31,7 @@ export interface IFileUploadInput<TMeta extends TypeFileMeta = TypeFileMeta> {
|
|
|
25
31
|
|
|
26
32
|
export interface IFileUploadUrlInput<TMeta extends TypeFileMeta = TypeFileMeta> {
|
|
27
33
|
url: string;
|
|
34
|
+
policy?: IFileUploadUrlPolicyResolved;
|
|
28
35
|
filename?: string;
|
|
29
36
|
contentType?: string;
|
|
30
37
|
size?: number;
|
|
@@ -40,7 +47,7 @@ export interface IFileDirectUploadInput<TMeta extends TypeFileMeta = TypeFileMet
|
|
|
40
47
|
objectKey?: string;
|
|
41
48
|
public?: boolean;
|
|
42
49
|
meta?: TMeta;
|
|
43
|
-
expiry?:
|
|
50
|
+
expiry?: TypeFileDirectUploadExpiry;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export interface IFileUploadContextResolved<TMeta extends TypeFileMeta = TypeFileMeta> {
|
|
@@ -92,6 +99,9 @@ export interface IFileResource<
|
|
|
92
99
|
provider: keyof IFileProviderRecord;
|
|
93
100
|
clientName: string;
|
|
94
101
|
fileScene?: keyof IFileSceneRecord | string;
|
|
102
|
+
status?: TypeFileStatus;
|
|
103
|
+
draftExpiresAt?: Date;
|
|
104
|
+
finalizedAt?: Date;
|
|
95
105
|
uploadedAt?: Date;
|
|
96
106
|
}
|
|
97
107
|
|
|
@@ -104,6 +114,37 @@ export interface IFileDirectUploadResult<
|
|
|
104
114
|
method?: 'PUT' | 'POST';
|
|
105
115
|
}
|
|
106
116
|
|
|
117
|
+
export interface IFileActionResponse {
|
|
118
|
+
id: TableIdentity;
|
|
119
|
+
filename?: string;
|
|
120
|
+
contentType?: string;
|
|
121
|
+
size?: number;
|
|
122
|
+
public?: boolean;
|
|
123
|
+
uploadedAt?: Date;
|
|
124
|
+
url: string;
|
|
125
|
+
signed: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface IFileDirectUploadResponse {
|
|
129
|
+
id: TableIdentity;
|
|
130
|
+
uploadUrl: string;
|
|
131
|
+
headers?: Record<string, string>;
|
|
132
|
+
method?: 'PUT' | 'POST';
|
|
133
|
+
filename?: string;
|
|
134
|
+
public?: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface IFileView {
|
|
138
|
+
id: TableIdentity;
|
|
139
|
+
filename?: string;
|
|
140
|
+
contentType?: string;
|
|
141
|
+
size?: number;
|
|
142
|
+
public?: boolean;
|
|
143
|
+
uploadedAt?: Date;
|
|
144
|
+
downloadUrl: string;
|
|
145
|
+
signed?: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
107
148
|
export interface IFileUploadOptions<
|
|
108
149
|
TClientOptions extends IFileProviderClientOptions = IFileProviderClientOptions,
|
|
109
150
|
TMeta extends TypeFileMeta = TypeFileMeta,
|
|
@@ -115,26 +156,26 @@ export interface IFileUploadOptions<
|
|
|
115
156
|
fileScene?: keyof IFileSceneRecord;
|
|
116
157
|
}
|
|
117
158
|
|
|
118
|
-
export interface
|
|
159
|
+
export interface IFileUploadUrlPolicyResolved<
|
|
119
160
|
TMeta extends TypeFileMeta = TypeFileMeta,
|
|
120
161
|
> extends IFileUploadContextResolved<TMeta> {
|
|
121
162
|
maxSize?: number;
|
|
122
163
|
mimeTypes?: string[];
|
|
123
164
|
extensions?: string[];
|
|
124
165
|
multiple?: boolean;
|
|
125
|
-
fileSize: number;
|
|
126
|
-
mimeType: string;
|
|
127
166
|
}
|
|
128
167
|
|
|
129
|
-
export interface
|
|
168
|
+
export interface IFileUploadPolicyResolved<
|
|
130
169
|
TMeta extends TypeFileMeta = TypeFileMeta,
|
|
131
|
-
> extends
|
|
132
|
-
|
|
170
|
+
> extends IFileUploadUrlPolicyResolved<TMeta> {
|
|
171
|
+
fileSize: number;
|
|
172
|
+
mimeType: string;
|
|
133
173
|
}
|
|
134
174
|
|
|
135
175
|
export interface IFileDownloadTokenPayload {
|
|
136
176
|
kind: 'fileDownload';
|
|
137
177
|
fileId: TableIdentity;
|
|
178
|
+
audienceUserId?: TableIdentity;
|
|
138
179
|
}
|
|
139
180
|
|
|
140
181
|
declare module 'vona' {
|
|
@@ -4,8 +4,8 @@ import type { ServiceOnion, TypeOnionOptionsEnableSimple } from 'vona-module-a-o
|
|
|
4
4
|
import type { EntityFile } from '../entity/file.ts';
|
|
5
5
|
import type { EntityFileProvider } from '../entity/fileProvider.ts';
|
|
6
6
|
import type {
|
|
7
|
-
IFileDeliveryOptions,
|
|
8
7
|
IFileDirectUploadInput,
|
|
8
|
+
IFileProviderDeliveryOptions,
|
|
9
9
|
IFileDownloadResult,
|
|
10
10
|
IFileProviderDirectUploadResource,
|
|
11
11
|
IFileProviderResource,
|
|
@@ -78,19 +78,24 @@ export interface IFileProviderExecute<
|
|
|
78
78
|
clientOptions: T,
|
|
79
79
|
options: O,
|
|
80
80
|
): Promise<IFileProviderDirectUploadResource>;
|
|
81
|
+
finalizeDirectUpload?(
|
|
82
|
+
file: EntityFile,
|
|
83
|
+
clientOptions: T,
|
|
84
|
+
options: O,
|
|
85
|
+
): Promise<IFileProviderResource | undefined>;
|
|
81
86
|
get(file: EntityFile, clientOptions: T, options: O): Promise<IFileProviderResource | undefined>;
|
|
82
87
|
delete(file: EntityFile, clientOptions: T, options: O): Promise<void>;
|
|
83
88
|
getDownloadUrl(
|
|
84
89
|
file: EntityFile,
|
|
85
90
|
clientOptions: T,
|
|
86
91
|
options: O,
|
|
87
|
-
deliveryOptions?:
|
|
92
|
+
deliveryOptions?: IFileProviderDeliveryOptions,
|
|
88
93
|
): Promise<string>;
|
|
89
94
|
download?(
|
|
90
95
|
file: EntityFile,
|
|
91
96
|
clientOptions: T,
|
|
92
97
|
options: O,
|
|
93
|
-
deliveryOptions?:
|
|
98
|
+
deliveryOptions?: IFileProviderDeliveryOptions,
|
|
94
99
|
): Promise<IFileDownloadResult>;
|
|
95
100
|
}
|
|
96
101
|
|
|
@@ -10,7 +10,6 @@ export interface IFileSceneOptionsUpload {
|
|
|
10
10
|
mimeTypes?: string[];
|
|
11
11
|
extensions?: string[];
|
|
12
12
|
multiple?: boolean;
|
|
13
|
-
public?: boolean;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export interface IDecoratorFileSceneOptionsProvider {
|
|
@@ -33,6 +32,7 @@ export type TypeFileSceneOptionsMetaResolver = (
|
|
|
33
32
|
export interface IDecoratorFileSceneOptions {
|
|
34
33
|
provider?: TypeDecoratorFileSceneOptionsProvider;
|
|
35
34
|
upload?: IFileSceneOptionsUpload;
|
|
35
|
+
public?: boolean;
|
|
36
36
|
meta?: Record<string, any> | TypeFileSceneOptionsMetaResolver;
|
|
37
37
|
}
|
|
38
38
|
|
package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
EntityFile,
|
|
3
3
|
IDecoratorFileProviderOptions,
|
|
4
|
-
IFileDeliveryOptions,
|
|
5
4
|
IFileDirectUploadInput,
|
|
5
|
+
IFileProviderDeliveryOptions,
|
|
6
6
|
IFileDownloadResult,
|
|
7
7
|
IFileProviderClientOptions,
|
|
8
8
|
IFileProviderClientRecord,
|
|
@@ -66,6 +66,14 @@ export class FileProviderCloudflare
|
|
|
66
66
|
return await this.scope.service.fileCloudflare.createDirectUpload(input, clientOptions);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
async finalizeDirectUpload(
|
|
70
|
+
file: EntityFile,
|
|
71
|
+
clientOptions: IFileProviderCloudflareClientOptions,
|
|
72
|
+
_options: IFileProviderOptionsCloudflare,
|
|
73
|
+
) {
|
|
74
|
+
return await this.scope.service.fileCloudflare.finalizeDirectUpload(file, clientOptions);
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
async get(
|
|
70
78
|
file: EntityFile,
|
|
71
79
|
clientOptions: IFileProviderCloudflareClientOptions,
|
|
@@ -97,7 +105,7 @@ export class FileProviderCloudflare
|
|
|
97
105
|
file: EntityFile,
|
|
98
106
|
clientOptions: IFileProviderCloudflareClientOptions,
|
|
99
107
|
_options: IFileProviderOptionsCloudflare,
|
|
100
|
-
deliveryOptions?:
|
|
108
|
+
deliveryOptions?: IFileProviderDeliveryOptions,
|
|
101
109
|
) {
|
|
102
110
|
return await this.scope.service.fileCloudflare.getDownloadUrl(
|
|
103
111
|
file,
|
|
@@ -110,14 +118,14 @@ export class FileProviderCloudflare
|
|
|
110
118
|
file: EntityFile,
|
|
111
119
|
clientOptions: IFileProviderCloudflareClientOptions,
|
|
112
120
|
options: IFileProviderOptionsCloudflare,
|
|
113
|
-
deliveryOptions?:
|
|
121
|
+
deliveryOptions?: IFileProviderDeliveryOptions,
|
|
114
122
|
): Promise<IFileDownloadResult> {
|
|
115
123
|
return {
|
|
116
124
|
kind: 'url',
|
|
117
125
|
url: await this.getDownloadUrl(file, clientOptions, options, deliveryOptions),
|
|
118
126
|
filename: file.filename,
|
|
119
127
|
contentType: file.contentType,
|
|
120
|
-
signed: !!
|
|
128
|
+
signed: !!deliveryOptions?.protected,
|
|
121
129
|
};
|
|
122
130
|
}
|
|
123
131
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
EntityFile,
|
|
3
|
-
IFileDeliveryOptions,
|
|
4
3
|
IFileDirectUploadInput,
|
|
4
|
+
IFileProviderDeliveryOptions,
|
|
5
5
|
IFileProviderDirectUploadResource,
|
|
6
6
|
IFileProviderResource,
|
|
7
7
|
IFileUploadInput,
|
|
@@ -20,6 +20,7 @@ import fse from 'fs-extra';
|
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { BeanBase, uuidv4 } from 'vona';
|
|
22
22
|
import { Service } from 'vona-module-a-bean';
|
|
23
|
+
import { downloadFileUploadUrl } from 'vona-module-a-file';
|
|
23
24
|
|
|
24
25
|
import type { IFileProviderCloudflareClientOptions } from '../bean/fileProvider.cloudflare.ts';
|
|
25
26
|
|
|
@@ -67,32 +68,28 @@ export class ServiceFileCloudflare extends BeanBase {
|
|
|
67
68
|
input: IFileUploadUrlInput,
|
|
68
69
|
options: IFileProviderCloudflareClientOptions,
|
|
69
70
|
): Promise<IFileProviderResource> {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw new Error(`Remote file fetch failed: ${response.status}`);
|
|
71
|
+
if (!input.policy) {
|
|
72
|
+
throw new Error('Resolved file upload policy is required for remote URL ingestion');
|
|
73
73
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
);
|
|
79
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
80
|
-
await fse.writeFile(tempFile, Buffer.from(arrayBuffer));
|
|
74
|
+
const downloaded = await downloadFileUploadUrl({
|
|
75
|
+
url: input.url,
|
|
76
|
+
filename: input.filename,
|
|
77
|
+
policy: input.policy,
|
|
78
|
+
});
|
|
81
79
|
try {
|
|
82
80
|
return await this.upload(
|
|
83
81
|
{
|
|
84
|
-
file:
|
|
85
|
-
filename:
|
|
86
|
-
contentType:
|
|
87
|
-
size:
|
|
88
|
-
objectKey: input.objectKey,
|
|
82
|
+
file: downloaded.file,
|
|
83
|
+
filename: downloaded.filename,
|
|
84
|
+
contentType: downloaded.contentType,
|
|
85
|
+
size: downloaded.size,
|
|
89
86
|
public: input.public,
|
|
90
87
|
meta: input.meta,
|
|
91
88
|
},
|
|
92
89
|
options,
|
|
93
90
|
);
|
|
94
91
|
} finally {
|
|
95
|
-
await
|
|
92
|
+
await downloaded.cleanup();
|
|
96
93
|
}
|
|
97
94
|
}
|
|
98
95
|
|
|
@@ -126,6 +123,43 @@ export class ServiceFileCloudflare extends BeanBase {
|
|
|
126
123
|
};
|
|
127
124
|
}
|
|
128
125
|
|
|
126
|
+
async finalizeDirectUpload(
|
|
127
|
+
file: EntityFile,
|
|
128
|
+
options: IFileProviderCloudflareClientOptions,
|
|
129
|
+
): Promise<IFileProviderResource | undefined> {
|
|
130
|
+
let metadata;
|
|
131
|
+
try {
|
|
132
|
+
metadata = await this.getMetadata(file, options);
|
|
133
|
+
} catch (error: any) {
|
|
134
|
+
if (error?.$metadata?.httpStatusCode === 404 || error?.name === 'NotFound') return undefined;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
if (!metadata?.ContentLength) return undefined;
|
|
138
|
+
const size = Number(metadata.ContentLength);
|
|
139
|
+
if (file.size !== undefined && size !== file.size) {
|
|
140
|
+
throw new Error(`direct upload size mismatch: expected=${file.size}, actual=${size}`);
|
|
141
|
+
}
|
|
142
|
+
const contentType = metadata.ContentType?.toLowerCase();
|
|
143
|
+
if (file.contentType && contentType !== file.contentType.toLowerCase()) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`direct upload content type mismatch: expected=${file.contentType}, actual=${contentType}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
resourceId: file.resourceId,
|
|
150
|
+
bucket: file.bucket,
|
|
151
|
+
objectKey: file.objectKey,
|
|
152
|
+
filename: file.filename,
|
|
153
|
+
contentType: metadata.ContentType ?? file.contentType,
|
|
154
|
+
size,
|
|
155
|
+
etag: this._normalizeEtag(metadata.ETag),
|
|
156
|
+
public: file.public,
|
|
157
|
+
meta: file.meta,
|
|
158
|
+
deliveryBaseUrl: file.deliveryBaseUrl,
|
|
159
|
+
raw: metadata,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
129
163
|
async remove(
|
|
130
164
|
file: Pick<EntityFile, 'bucket' | 'objectKey'>,
|
|
131
165
|
options: IFileProviderCloudflareClientOptions,
|
|
@@ -143,7 +177,7 @@ export class ServiceFileCloudflare extends BeanBase {
|
|
|
143
177
|
async getDownloadUrl(
|
|
144
178
|
file: Pick<EntityFile, 'bucket' | 'objectKey' | 'deliveryBaseUrl' | 'public'>,
|
|
145
179
|
options: IFileProviderCloudflareClientOptions,
|
|
146
|
-
deliveryOptions?:
|
|
180
|
+
deliveryOptions?: IFileProviderDeliveryOptions,
|
|
147
181
|
) {
|
|
148
182
|
const normalized = this._normalizeClientOptions(options);
|
|
149
183
|
const bucket = file.bucket ?? normalized.bucket;
|