cabloy 5.1.102 → 5.1.103
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 +29 -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/file-guide.md +147 -0
- package/cabloy-docs/frontend/image-guide.md +18 -9
- package/cabloy-docs/frontend/model-state-guide.md +12 -0
- 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/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 +44 -1
- 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/lib/utils.ts +29 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
- 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
|
@@ -5,16 +5,18 @@ import { Bean } from 'vona-module-a-bean';
|
|
|
5
5
|
|
|
6
6
|
import type { EntityFile } from '../entity/file.ts';
|
|
7
7
|
import type {
|
|
8
|
+
IFileActionResponse,
|
|
8
9
|
IFileDeliveryOptions,
|
|
9
10
|
IFileDirectUploadInput,
|
|
10
|
-
|
|
11
|
+
IFileDirectUploadResponse,
|
|
12
|
+
IFileProviderDeliveryOptions,
|
|
11
13
|
IFileProviderDirectUploadResource,
|
|
12
14
|
IFileProviderResource,
|
|
13
15
|
IFileResource,
|
|
14
|
-
IFileUploadContextResolved,
|
|
15
16
|
IFileUploadInput,
|
|
16
17
|
IFileUploadOptions,
|
|
17
18
|
IFileUploadUrlInput,
|
|
19
|
+
IFileView,
|
|
18
20
|
} from '../types/file.ts';
|
|
19
21
|
import type {
|
|
20
22
|
IFileProviderRecord,
|
|
@@ -22,6 +24,7 @@ import type {
|
|
|
22
24
|
TypeFileProviderExecuteByName,
|
|
23
25
|
TypeFileProviderOptionsByName,
|
|
24
26
|
} from '../types/fileProvider.ts';
|
|
27
|
+
import type { IFileSceneRecord } from '../types/fileScene.ts';
|
|
25
28
|
|
|
26
29
|
interface IFileProviderContext<N extends keyof IFileProviderRecord = keyof IFileProviderRecord> {
|
|
27
30
|
beanFileProvider: TypeFileProviderExecuteByName<N>;
|
|
@@ -29,6 +32,18 @@ interface IFileProviderContext<N extends keyof IFileProviderRecord = keyof IFile
|
|
|
29
32
|
onionOptions: TypeFileProviderOptionsByName<N>;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
interface IInsertFileContext {
|
|
36
|
+
fileScene?: keyof IFileSceneRecord;
|
|
37
|
+
public?: boolean;
|
|
38
|
+
status?: EntityFile['status'];
|
|
39
|
+
draftExpiresAt?: Date;
|
|
40
|
+
finalizedAt?: Date;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface IFileDeliveryOptionsResolved extends IFileProviderDeliveryOptions {
|
|
44
|
+
audience: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
@Bean()
|
|
33
48
|
export class BeanFile extends BeanBase {
|
|
34
49
|
async upload<N extends keyof IFileProviderRecord>(
|
|
@@ -49,6 +64,7 @@ export class BeanFile extends BeanBase {
|
|
|
49
64
|
{
|
|
50
65
|
fileScene: options?.fileScene,
|
|
51
66
|
public: options?.public ?? input.public,
|
|
67
|
+
status: 'ready',
|
|
52
68
|
},
|
|
53
69
|
);
|
|
54
70
|
return this._combineFileResource(file, fileProviderResource);
|
|
@@ -61,7 +77,10 @@ export class BeanFile extends BeanBase {
|
|
|
61
77
|
): Promise<IFileResource> {
|
|
62
78
|
const providerContext = await this._getProviderContextByInput(providerName, options);
|
|
63
79
|
if (!providerContext.beanFileProvider.uploadUrl) {
|
|
64
|
-
throw
|
|
80
|
+
return this.app.throw(
|
|
81
|
+
403,
|
|
82
|
+
`File provider does not support uploadUrl: ${String(providerName)}`,
|
|
83
|
+
);
|
|
65
84
|
}
|
|
66
85
|
const fileProviderResource = await providerContext.beanFileProvider.uploadUrl(
|
|
67
86
|
{ ...input, public: options?.public ?? input.public, meta: options?.meta ?? input.meta },
|
|
@@ -75,6 +94,7 @@ export class BeanFile extends BeanBase {
|
|
|
75
94
|
{
|
|
76
95
|
fileScene: options?.fileScene,
|
|
77
96
|
public: options?.public ?? input.public,
|
|
97
|
+
status: 'ready',
|
|
78
98
|
},
|
|
79
99
|
);
|
|
80
100
|
return this._combineFileResource(file, fileProviderResource);
|
|
@@ -84,10 +104,16 @@ export class BeanFile extends BeanBase {
|
|
|
84
104
|
providerName: N,
|
|
85
105
|
input: IFileDirectUploadInput,
|
|
86
106
|
options?: IFileUploadOptions<TypeFileProviderClientOptionsByName<N>>,
|
|
87
|
-
): Promise<
|
|
107
|
+
): Promise<IFileDirectUploadResponse> {
|
|
88
108
|
const providerContext = await this._getProviderContextByInput(providerName, options);
|
|
89
|
-
if (
|
|
90
|
-
|
|
109
|
+
if (
|
|
110
|
+
!providerContext.beanFileProvider.createDirectUpload ||
|
|
111
|
+
!providerContext.beanFileProvider.finalizeDirectUpload
|
|
112
|
+
) {
|
|
113
|
+
return this.app.throw(
|
|
114
|
+
403,
|
|
115
|
+
`File provider does not support createDirectUpload: ${String(providerName)}`,
|
|
116
|
+
);
|
|
91
117
|
}
|
|
92
118
|
const fileProviderResource = await providerContext.beanFileProvider.createDirectUpload(
|
|
93
119
|
{ ...input, public: options?.public ?? input.public, meta: options?.meta ?? input.meta },
|
|
@@ -101,14 +127,53 @@ export class BeanFile extends BeanBase {
|
|
|
101
127
|
{
|
|
102
128
|
fileScene: options?.fileScene,
|
|
103
129
|
public: options?.public ?? input.public,
|
|
130
|
+
status: 'draft',
|
|
131
|
+
draftExpiresAt: this._resolveDirectUploadDraftExpiresAt(input.expiry),
|
|
104
132
|
},
|
|
105
133
|
);
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
return this._createDirectUploadResponse(file, fileProviderResource);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async finalizeDirectUpload(fileId: TableIdentity): Promise<IFileResource> {
|
|
138
|
+
return await this.scope.redlock.lock(`file.directUpload.${fileId}`, async () => {
|
|
139
|
+
const file = await this.scope.model.file.getById(fileId);
|
|
140
|
+
if (!file) throw new Error(`not found file: ${fileId}`);
|
|
141
|
+
if (file.status !== 'draft') {
|
|
142
|
+
return this.app.throw(403, `file is not draft: ${fileId}`);
|
|
143
|
+
}
|
|
144
|
+
const draftExpiresAt = this._normalizeDate(file.draftExpiresAt);
|
|
145
|
+
if (draftExpiresAt && draftExpiresAt.getTime() < Date.now()) {
|
|
146
|
+
await this.scope.model.file.updateById(file.id, { status: 'expired' });
|
|
147
|
+
return this.app.throw(403, `file draft expired: ${fileId}`);
|
|
148
|
+
}
|
|
149
|
+
const providerContext = await this._getProviderContext(file);
|
|
150
|
+
if (!providerContext.beanFileProvider.finalizeDirectUpload) {
|
|
151
|
+
return this.app.throw(
|
|
152
|
+
403,
|
|
153
|
+
`File provider does not support finalizeDirectUpload: ${String(file.providerName)}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const fileProviderResource = await providerContext.beanFileProvider.finalizeDirectUpload(
|
|
157
|
+
file,
|
|
158
|
+
providerContext.clientOptions,
|
|
159
|
+
providerContext.onionOptions,
|
|
160
|
+
);
|
|
161
|
+
if (!fileProviderResource) {
|
|
162
|
+
return this.app.throw(403, `file direct upload not ready: ${fileId}`);
|
|
163
|
+
}
|
|
164
|
+
const finalizedAt = new Date();
|
|
165
|
+
const fileUpdated = await this.scope.model.file.updateById(
|
|
166
|
+
file.id,
|
|
167
|
+
this._buildFilePersistData(fileProviderResource, {
|
|
168
|
+
fileScene: file.fileScene,
|
|
169
|
+
public: file.public,
|
|
170
|
+
status: 'ready',
|
|
171
|
+
draftExpiresAt,
|
|
172
|
+
finalizedAt,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
return this._combineFileResource({ ...file, ...fileUpdated }, fileProviderResource);
|
|
176
|
+
});
|
|
112
177
|
}
|
|
113
178
|
|
|
114
179
|
async get(fileId: TableIdentity): Promise<IFileResource | undefined> {
|
|
@@ -118,6 +183,19 @@ export class BeanFile extends BeanBase {
|
|
|
118
183
|
return this._combineFileResource(file, fileProviderResource);
|
|
119
184
|
}
|
|
120
185
|
|
|
186
|
+
async expireDraftFile(fileId: TableIdentity) {
|
|
187
|
+
return await this.scope.redlock.lock(`file.directUpload.${fileId}`, async () => {
|
|
188
|
+
const file = await this.scope.model.file.getById(fileId);
|
|
189
|
+
if (!file || file.status !== 'draft') return;
|
|
190
|
+
const draftExpiresAt = this._normalizeDate(file.draftExpiresAt);
|
|
191
|
+
if (!draftExpiresAt || draftExpiresAt.getTime() >= Date.now()) return;
|
|
192
|
+
const { beanFileProvider, clientOptions, onionOptions } =
|
|
193
|
+
await this._getProviderContext(file);
|
|
194
|
+
await beanFileProvider.delete(file, clientOptions, onionOptions);
|
|
195
|
+
await this.scope.model.file.updateById(file.id, { status: 'expired' });
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
121
199
|
async delete(fileId: TableIdentity) {
|
|
122
200
|
const file = await this.scope.model.file.getById(fileId);
|
|
123
201
|
if (!file) return;
|
|
@@ -129,12 +207,10 @@ export class BeanFile extends BeanBase {
|
|
|
129
207
|
async getDownloadUrl(fileId: TableIdentity, deliveryOptions?: IFileDeliveryOptions) {
|
|
130
208
|
const file = await this.scope.model.file.getById(fileId);
|
|
131
209
|
if (!file) throw new Error(`not found file: ${fileId}`);
|
|
210
|
+
this._assertFileReady(file);
|
|
132
211
|
const deliveryOptionsResolved = this._mergeDeliveryOptions(file, deliveryOptions);
|
|
133
212
|
const { beanFileProvider, clientOptions, onionOptions } = await this._getProviderContext(file);
|
|
134
|
-
if (
|
|
135
|
-
deliveryOptionsResolved.signed &&
|
|
136
|
-
(clientOptions.signedDeliveryKind ?? 'proxy') === 'proxy'
|
|
137
|
-
) {
|
|
213
|
+
if (this._shouldUseProxySignedDelivery(deliveryOptionsResolved, clientOptions)) {
|
|
138
214
|
return await this._createSignedDownloadUrl(file.id, deliveryOptionsResolved);
|
|
139
215
|
}
|
|
140
216
|
return await beanFileProvider.getDownloadUrl(
|
|
@@ -148,12 +224,10 @@ export class BeanFile extends BeanBase {
|
|
|
148
224
|
async download(fileId: TableIdentity, deliveryOptions?: IFileDeliveryOptions) {
|
|
149
225
|
const file = await this.scope.model.file.getById(fileId);
|
|
150
226
|
if (!file) throw new Error(`not found file: ${fileId}`);
|
|
227
|
+
this._assertFileReady(file);
|
|
151
228
|
const deliveryOptionsResolved = this._mergeDeliveryOptions(file, deliveryOptions);
|
|
152
229
|
const { beanFileProvider, clientOptions, onionOptions } = await this._getProviderContext(file);
|
|
153
|
-
if (
|
|
154
|
-
deliveryOptionsResolved.signed &&
|
|
155
|
-
(clientOptions.signedDeliveryKind ?? 'proxy') === 'proxy'
|
|
156
|
-
) {
|
|
230
|
+
if (this._shouldUseProxySignedDelivery(deliveryOptionsResolved, clientOptions)) {
|
|
157
231
|
return {
|
|
158
232
|
kind: 'url' as const,
|
|
159
233
|
url: await this._createSignedDownloadUrl(file.id, deliveryOptionsResolved),
|
|
@@ -162,26 +236,70 @@ export class BeanFile extends BeanBase {
|
|
|
162
236
|
signed: true,
|
|
163
237
|
};
|
|
164
238
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
239
|
+
return await this._downloadFromProvider(
|
|
240
|
+
file,
|
|
241
|
+
beanFileProvider,
|
|
242
|
+
clientOptions,
|
|
243
|
+
onionOptions,
|
|
244
|
+
deliveryOptionsResolved,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async downloadForDelivery(fileId: TableIdentity, options?: { protected?: boolean }) {
|
|
249
|
+
const file = await this.scope.model.file.getById(fileId);
|
|
250
|
+
if (!file) throw new Error(`not found file: ${fileId}`);
|
|
251
|
+
this._assertFileReady(file);
|
|
252
|
+
const { beanFileProvider, clientOptions, onionOptions } = await this._getProviderContext(file);
|
|
253
|
+
return await this._downloadFromProvider(file, beanFileProvider, clientOptions, onionOptions, {
|
|
254
|
+
protected: options?.protected ?? !file.public,
|
|
255
|
+
responseMode: 'buffer',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async resolveView(
|
|
260
|
+
fileId?: TableIdentity,
|
|
261
|
+
fileScene?: keyof IFileSceneRecord,
|
|
262
|
+
deliveryOptions?: IFileDeliveryOptions,
|
|
263
|
+
): Promise<IFileView | undefined> {
|
|
264
|
+
if (!fileId) return;
|
|
265
|
+
const file = await this.get(fileId);
|
|
266
|
+
if (!file) return;
|
|
267
|
+
this._assertFileReady(file);
|
|
268
|
+
if (fileScene && file.fileScene !== fileScene) {
|
|
269
|
+
throw new Error(`file scene mismatch: file=${file.fileScene}, expected=${fileScene}`);
|
|
172
270
|
}
|
|
271
|
+
return await this._createFileView(file, deliveryOptions);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async resolveViews(
|
|
275
|
+
fileIds?: TableIdentity[],
|
|
276
|
+
fileScene?: keyof IFileSceneRecord,
|
|
277
|
+
deliveryOptions?: IFileDeliveryOptions,
|
|
278
|
+
) {
|
|
279
|
+
if (!fileIds) return;
|
|
280
|
+
if (!fileIds.length) return [];
|
|
281
|
+
const items = await Promise.all(
|
|
282
|
+
fileIds.map(fileId => this.resolveView(fileId, fileScene, deliveryOptions)),
|
|
283
|
+
);
|
|
284
|
+
return items.filter((item): item is IFileView => !!item);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async createFileActionResponse(
|
|
288
|
+
file: IFileResource,
|
|
289
|
+
deliveryOptions?: IFileDeliveryOptions,
|
|
290
|
+
): Promise<IFileActionResponse> {
|
|
291
|
+
this._assertFileReady(file);
|
|
292
|
+
const deliveryOptionsResolved = this._mergeDeliveryOptions(file, deliveryOptions);
|
|
173
293
|
return {
|
|
174
|
-
|
|
175
|
-
url: await beanFileProvider.getDownloadUrl(
|
|
176
|
-
file,
|
|
177
|
-
clientOptions,
|
|
178
|
-
onionOptions,
|
|
179
|
-
deliveryOptionsResolved,
|
|
180
|
-
),
|
|
294
|
+
id: file.id,
|
|
181
295
|
filename: file.filename,
|
|
182
296
|
contentType: file.contentType,
|
|
183
|
-
|
|
184
|
-
|
|
297
|
+
size: file.size,
|
|
298
|
+
public: file.public,
|
|
299
|
+
uploadedAt: file.uploadedAt,
|
|
300
|
+
url: await this.getDownloadUrl(file.id, deliveryOptions),
|
|
301
|
+
signed: deliveryOptionsResolved.protected,
|
|
302
|
+
} satisfies IFileActionResponse;
|
|
185
303
|
}
|
|
186
304
|
|
|
187
305
|
private async _getFileProviderResource(file: EntityFile) {
|
|
@@ -240,46 +358,138 @@ export class BeanFile extends BeanBase {
|
|
|
240
358
|
return (options ?? {}) as T;
|
|
241
359
|
}
|
|
242
360
|
|
|
361
|
+
private async _downloadFromProvider(
|
|
362
|
+
file: EntityFile,
|
|
363
|
+
beanFileProvider: TypeFileProviderExecuteByName<keyof IFileProviderRecord>,
|
|
364
|
+
clientOptions: TypeFileProviderClientOptionsByName<keyof IFileProviderRecord>,
|
|
365
|
+
onionOptions: TypeFileProviderOptionsByName<keyof IFileProviderRecord>,
|
|
366
|
+
deliveryOptions: IFileProviderDeliveryOptions,
|
|
367
|
+
) {
|
|
368
|
+
if (beanFileProvider.download) {
|
|
369
|
+
return await beanFileProvider.download(file, clientOptions, onionOptions, deliveryOptions);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
kind: 'url' as const,
|
|
373
|
+
url: await beanFileProvider.getDownloadUrl(
|
|
374
|
+
file,
|
|
375
|
+
clientOptions,
|
|
376
|
+
onionOptions,
|
|
377
|
+
deliveryOptions,
|
|
378
|
+
),
|
|
379
|
+
filename: file.filename,
|
|
380
|
+
contentType: file.contentType,
|
|
381
|
+
signed: deliveryOptions.protected,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
243
385
|
private _mergeDeliveryOptions(
|
|
244
386
|
file: Pick<IFileResource, 'public'>,
|
|
245
387
|
deliveryOptions?: IFileDeliveryOptions,
|
|
246
|
-
):
|
|
247
|
-
const
|
|
248
|
-
const expiresIn = deliveryOptions?.expiresIn;
|
|
249
|
-
const expiresAt = deliveryOptions?.expiresAt;
|
|
250
|
-
const responseMode = deliveryOptions?.responseMode;
|
|
388
|
+
): IFileDeliveryOptionsResolved {
|
|
389
|
+
const audience = deliveryOptions?.audience ?? false;
|
|
251
390
|
return {
|
|
252
|
-
|
|
253
|
-
expiresIn
|
|
254
|
-
|
|
255
|
-
|
|
391
|
+
protected: audience || !file.public,
|
|
392
|
+
expiresIn:
|
|
393
|
+
deliveryOptions?.expiresIn ??
|
|
394
|
+
(audience ? this.scope.config.file.delivery.audienceExpiresIn : undefined),
|
|
395
|
+
audience,
|
|
396
|
+
responseMode: deliveryOptions?.responseMode,
|
|
256
397
|
};
|
|
257
398
|
}
|
|
258
399
|
|
|
400
|
+
private _shouldUseProxySignedDelivery(
|
|
401
|
+
deliveryOptions: IFileDeliveryOptionsResolved,
|
|
402
|
+
clientOptions: TypeFileProviderClientOptionsByName<keyof IFileProviderRecord>,
|
|
403
|
+
) {
|
|
404
|
+
return (
|
|
405
|
+
deliveryOptions.protected &&
|
|
406
|
+
(deliveryOptions.audience || (clientOptions.signedDeliveryKind ?? 'proxy') === 'proxy')
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
259
410
|
private async _createSignedDownloadUrl(
|
|
260
411
|
fileId: TableIdentity,
|
|
261
|
-
deliveryOptions:
|
|
412
|
+
deliveryOptions: IFileDeliveryOptionsResolved,
|
|
262
413
|
) {
|
|
263
|
-
const routePath = this.scope.util.combineApiPath(
|
|
414
|
+
const routePath = this.scope.util.combineApiPath('file/download', false, true);
|
|
415
|
+
const audienceUserId = this._resolveAudienceUserId(deliveryOptions);
|
|
264
416
|
const tokenPayload = await this.bean.fileUploadPolicy.createDownloadToken({
|
|
265
417
|
fileId,
|
|
266
418
|
expiresIn: deliveryOptions.expiresIn,
|
|
419
|
+
audienceUserId,
|
|
267
420
|
});
|
|
268
421
|
const routeUrl = this.app.util.getAbsoluteUrlByApiPath(routePath);
|
|
269
422
|
const url = new URL(routeUrl);
|
|
423
|
+
url.searchParams.set('fileId', String(fileId));
|
|
270
424
|
url.searchParams.set('token', tokenPayload.token);
|
|
271
425
|
return url.toString();
|
|
272
426
|
}
|
|
273
427
|
|
|
274
|
-
private
|
|
275
|
-
|
|
276
|
-
|
|
428
|
+
private _resolveAudienceUserId(deliveryOptions: IFileDeliveryOptionsResolved) {
|
|
429
|
+
if (!deliveryOptions.audience) return;
|
|
430
|
+
const user = this.bean.passport.currentUser;
|
|
431
|
+
if (!user || user.anonymous) return this.app.throw(401);
|
|
432
|
+
return user.id;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private async _createFileView(file: IFileResource, deliveryOptions?: IFileDeliveryOptions) {
|
|
436
|
+
const deliveryOptionsResolved = this._mergeDeliveryOptions(file, deliveryOptions);
|
|
437
|
+
return {
|
|
438
|
+
id: file.id,
|
|
439
|
+
filename: file.filename,
|
|
440
|
+
contentType: file.contentType,
|
|
441
|
+
size: file.size,
|
|
442
|
+
public: file.public,
|
|
443
|
+
uploadedAt: file.uploadedAt,
|
|
444
|
+
downloadUrl: await this.getDownloadUrl(file.id, deliveryOptions),
|
|
445
|
+
signed: deliveryOptionsResolved.protected,
|
|
446
|
+
} satisfies IFileView;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private _createDirectUploadResponse(
|
|
450
|
+
file: EntityFile,
|
|
451
|
+
fileProviderResource: IFileProviderDirectUploadResource,
|
|
452
|
+
) {
|
|
453
|
+
return {
|
|
454
|
+
id: file.id,
|
|
455
|
+
uploadUrl: fileProviderResource.uploadUrl,
|
|
456
|
+
headers: fileProviderResource.headers,
|
|
457
|
+
method: fileProviderResource.method,
|
|
458
|
+
filename: fileProviderResource.filename,
|
|
459
|
+
public: fileProviderResource.public ?? file.public,
|
|
460
|
+
} satisfies IFileDirectUploadResponse;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private _resolveDirectUploadDraftExpiresAt(expiry?: IFileDirectUploadInput['expiry']) {
|
|
464
|
+
return (
|
|
465
|
+
this._normalizeDate(expiry) ??
|
|
466
|
+
new Date(Date.now() + this.scope.config.file.directUpload.draftExpiresIn)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private _normalizeDate(value: unknown) {
|
|
471
|
+
if (!value) return undefined;
|
|
472
|
+
if (value instanceof Date) {
|
|
473
|
+
return Number.isNaN(value.getTime()) ? undefined : value;
|
|
474
|
+
}
|
|
475
|
+
const date = new Date(value as string | number);
|
|
476
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private _assertFileReady(file: Pick<IFileResource, 'id' | 'status'>) {
|
|
480
|
+
if (file.status === 'draft') {
|
|
481
|
+
return this.app.throw(403, `file draft not ready: ${file.id}`);
|
|
482
|
+
}
|
|
483
|
+
if (file.status === 'expired') {
|
|
484
|
+
return this.app.throw(403, `file draft expired: ${file.id}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private _buildFilePersistData(
|
|
277
489
|
fileProviderResource: IFileProviderResource | IFileProviderDirectUploadResource,
|
|
278
|
-
context?:
|
|
490
|
+
context?: IInsertFileContext,
|
|
279
491
|
) {
|
|
280
|
-
return
|
|
281
|
-
providerName,
|
|
282
|
-
clientName,
|
|
492
|
+
return {
|
|
283
493
|
resourceId: fileProviderResource.resourceId,
|
|
284
494
|
bucket: fileProviderResource.bucket,
|
|
285
495
|
objectKey: fileProviderResource.objectKey,
|
|
@@ -292,6 +502,22 @@ export class BeanFile extends BeanBase {
|
|
|
292
502
|
storagePath: fileProviderResource.storagePath,
|
|
293
503
|
deliveryBaseUrl: fileProviderResource.deliveryBaseUrl,
|
|
294
504
|
fileScene: context?.fileScene,
|
|
505
|
+
status: context?.status,
|
|
506
|
+
draftExpiresAt: context?.draftExpiresAt,
|
|
507
|
+
finalizedAt: context?.finalizedAt,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private async _insertFile(
|
|
512
|
+
providerName: keyof IFileProviderRecord,
|
|
513
|
+
clientName: string,
|
|
514
|
+
fileProviderResource: IFileProviderResource | IFileProviderDirectUploadResource,
|
|
515
|
+
context?: IInsertFileContext,
|
|
516
|
+
) {
|
|
517
|
+
return await this.scope.model.file.insert({
|
|
518
|
+
providerName,
|
|
519
|
+
clientName,
|
|
520
|
+
...this._buildFilePersistData(fileProviderResource, context),
|
|
295
521
|
});
|
|
296
522
|
}
|
|
297
523
|
|
|
@@ -304,6 +530,9 @@ export class BeanFile extends BeanBase {
|
|
|
304
530
|
provider: file.providerName,
|
|
305
531
|
clientName: file.clientName,
|
|
306
532
|
fileScene: file.fileScene,
|
|
533
|
+
status: file.status,
|
|
534
|
+
draftExpiresAt: file.status === 'draft' ? file.draftExpiresAt : undefined,
|
|
535
|
+
finalizedAt: file.finalizedAt,
|
|
307
536
|
resourceId: fileProviderResource?.resourceId ?? file.resourceId,
|
|
308
537
|
bucket: fileProviderResource?.bucket ?? file.bucket,
|
|
309
538
|
objectKey: fileProviderResource?.objectKey ?? file.objectKey,
|
|
@@ -6,8 +6,8 @@ import type {
|
|
|
6
6
|
IFileDownloadTokenPayload,
|
|
7
7
|
IFileUploadContextResolved,
|
|
8
8
|
IFileUploadPolicyResolved,
|
|
9
|
-
IFileUploadTokenPayload,
|
|
10
9
|
} from '../types/file.ts';
|
|
10
|
+
import type { IFileProviderExecute } from '../types/fileProvider.ts';
|
|
11
11
|
import type {
|
|
12
12
|
IDecoratorFileSceneOptions,
|
|
13
13
|
IDecoratorFileSceneOptionsProvider,
|
|
@@ -18,33 +18,17 @@ import { getFileExtension, matchesFileMimeType } from '../lib/fileUploadValidati
|
|
|
18
18
|
|
|
19
19
|
@Bean()
|
|
20
20
|
export class BeanFileUploadPolicy extends BeanBase {
|
|
21
|
-
async
|
|
22
|
-
|
|
23
|
-
size: number;
|
|
24
|
-
mimeType: string;
|
|
21
|
+
async createDownloadToken(data: {
|
|
22
|
+
fileId: number | string;
|
|
25
23
|
expiresIn?: number;
|
|
24
|
+
audienceUserId?: IFileDownloadTokenPayload['audienceUserId'];
|
|
26
25
|
}) {
|
|
27
|
-
const
|
|
28
|
-
const path = this.scope.util.combineApiPath('file/upload', false, true);
|
|
29
|
-
const token = await this.bean.jwt.createTempAuthToken(
|
|
30
|
-
{
|
|
31
|
-
kind: 'fileUpload',
|
|
32
|
-
...payload,
|
|
33
|
-
} as IFileUploadTokenPayload,
|
|
34
|
-
{
|
|
35
|
-
path,
|
|
36
|
-
expiresIn: data.expiresIn,
|
|
37
|
-
},
|
|
38
|
-
);
|
|
39
|
-
return { token, expiresIn: data.expiresIn };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async createDownloadToken(data: { fileId: number | string; expiresIn?: number }) {
|
|
43
|
-
const path = this.scope.util.combineApiPath(`file/download/${data.fileId}`, false, true);
|
|
26
|
+
const path = this.scope.util.combineApiPath('file/download', false, true);
|
|
44
27
|
const token = await this.bean.jwt.createTempAuthToken(
|
|
45
28
|
{
|
|
46
29
|
kind: 'fileDownload',
|
|
47
30
|
fileId: data.fileId,
|
|
31
|
+
audienceUserId: data.audienceUserId,
|
|
48
32
|
} as IFileDownloadTokenPayload,
|
|
49
33
|
{
|
|
50
34
|
path,
|
|
@@ -54,16 +38,6 @@ export class BeanFileUploadPolicy extends BeanBase {
|
|
|
54
38
|
return { token, expiresIn: data.expiresIn };
|
|
55
39
|
}
|
|
56
40
|
|
|
57
|
-
async verifyUploadToken(token: string | undefined, routePathRaw: string) {
|
|
58
|
-
const payload = (await this.bean.jwt.get('access').verify(token, {
|
|
59
|
-
path: routePathRaw,
|
|
60
|
-
})) as IFileUploadTokenPayload | undefined;
|
|
61
|
-
if (!payload || payload.kind !== 'fileUpload') {
|
|
62
|
-
return this.app.throw(401);
|
|
63
|
-
}
|
|
64
|
-
return payload;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
41
|
async verifyDownloadToken(token: string | undefined, routePathRaw: string) {
|
|
68
42
|
const payload = (await this.bean.jwt.get('access').verify(token, {
|
|
69
43
|
path: routePathRaw,
|
|
@@ -80,26 +54,20 @@ export class BeanFileUploadPolicy extends BeanBase {
|
|
|
80
54
|
filename?: string;
|
|
81
55
|
mimeType: string;
|
|
82
56
|
},
|
|
83
|
-
|
|
57
|
+
policy: IFileUploadPolicyResolved,
|
|
84
58
|
) {
|
|
85
59
|
const stat = await fse.stat(file.file);
|
|
86
60
|
const fileSize = Number(stat.size);
|
|
87
|
-
if (fileSize
|
|
88
|
-
return this.app.throw(403, `file
|
|
89
|
-
}
|
|
90
|
-
if (payload.maxSize && fileSize > payload.maxSize) {
|
|
91
|
-
return this.app.throw(403, `file too large: maxSize=${payload.maxSize}`);
|
|
61
|
+
if (policy.maxSize && fileSize > policy.maxSize) {
|
|
62
|
+
return this.app.throw(403, `file too large: maxSize=${policy.maxSize}`);
|
|
92
63
|
}
|
|
93
64
|
const mimeType = file.mimeType.toLowerCase();
|
|
94
|
-
if (mimeType
|
|
95
|
-
return this.app.throw(403, `file mimeType mismatch: mimeType=${mimeType}`);
|
|
96
|
-
}
|
|
97
|
-
if (payload.mimeTypes?.length && !matchesFileMimeType(mimeType, payload.mimeTypes)) {
|
|
65
|
+
if (policy.mimeTypes?.length && !matchesFileMimeType(mimeType, policy.mimeTypes)) {
|
|
98
66
|
return this.app.throw(403, `unsupported file mimeType: ${mimeType}`);
|
|
99
67
|
}
|
|
100
68
|
const extension = getFileExtension(file.filename);
|
|
101
|
-
if (
|
|
102
|
-
return this.app.throw(403, `unsupported file extension: ${extension}`);
|
|
69
|
+
if (policy.extensions?.length && (!extension || !policy.extensions.includes(extension))) {
|
|
70
|
+
return this.app.throw(403, `unsupported file extension: ${extension ?? '(none)'}`);
|
|
103
71
|
}
|
|
104
72
|
}
|
|
105
73
|
|
|
@@ -130,13 +98,19 @@ export class BeanFileUploadPolicy extends BeanBase {
|
|
|
130
98
|
const fileScene = data.fileScene;
|
|
131
99
|
const sceneOptions = this._getSceneOptions(fileScene);
|
|
132
100
|
const { providerName, clientName } = await this._resolveProvider(sceneOptions);
|
|
133
|
-
const
|
|
101
|
+
const {
|
|
102
|
+
entityFileProvider,
|
|
103
|
+
disabled,
|
|
104
|
+
beanFullName,
|
|
105
|
+
clientOptions: providerClientOptions,
|
|
106
|
+
} = await this.bean.fileProvider.getClientOptions({
|
|
134
107
|
providerName,
|
|
135
108
|
clientName,
|
|
136
109
|
});
|
|
137
|
-
if (!
|
|
110
|
+
if (!entityFileProvider || disabled) {
|
|
138
111
|
return this.app.throw(403, `File provider unavailable: ${providerName}.${clientName}`);
|
|
139
112
|
}
|
|
113
|
+
const fileProvider = this.app.bean._getBean<IFileProviderExecute>(beanFullName as never);
|
|
140
114
|
const uploadOptions = {
|
|
141
115
|
...(fileConfig.upload ?? {}),
|
|
142
116
|
...(sceneOptions.upload ?? {}),
|
|
@@ -149,15 +123,16 @@ export class BeanFileUploadPolicy extends BeanBase {
|
|
|
149
123
|
mimeTypes: mimeTypes.length > 0 ? mimeTypes : undefined,
|
|
150
124
|
extensions: extensions.length > 0 ? extensions : undefined,
|
|
151
125
|
multiple: uploadOptions.multiple,
|
|
152
|
-
public: this._resolvePublic(
|
|
126
|
+
public: this._resolvePublic(providerClientOptions, sceneOptions),
|
|
127
|
+
directUpload:
|
|
128
|
+
typeof fileProvider.createDirectUpload === 'function' &&
|
|
129
|
+
typeof fileProvider.finalizeDirectUpload === 'function',
|
|
153
130
|
};
|
|
154
131
|
}
|
|
155
132
|
|
|
156
|
-
async
|
|
133
|
+
async resolveUploadUrlPolicy(data: {
|
|
157
134
|
fileScene: keyof IFileSceneRecord;
|
|
158
|
-
|
|
159
|
-
mimeType: string;
|
|
160
|
-
}): Promise<IFileUploadPolicyResolved> {
|
|
135
|
+
}): Promise<Omit<IFileUploadPolicyResolved, 'fileSize' | 'mimeType'>> {
|
|
161
136
|
const fileConfig = this.scope.config.file;
|
|
162
137
|
const context = await this.resolveUploadContext({ fileScene: data.fileScene });
|
|
163
138
|
const sceneOptions = this._getSceneOptions(context.fileScene);
|
|
@@ -165,22 +140,32 @@ export class BeanFileUploadPolicy extends BeanBase {
|
|
|
165
140
|
...(fileConfig.upload ?? {}),
|
|
166
141
|
...(sceneOptions.upload ?? {}),
|
|
167
142
|
};
|
|
168
|
-
const maxSize = uploadOptions.maxSize;
|
|
169
143
|
const mimeTypes = [...(uploadOptions.mimeTypes ?? [])];
|
|
170
144
|
const extensions = [...(uploadOptions.extensions ?? [])];
|
|
171
|
-
const mimeType = data.mimeType.toLowerCase();
|
|
172
|
-
if (maxSize && data.size > maxSize) {
|
|
173
|
-
return this.app.throw(403, `file too large: maxSize=${maxSize}`);
|
|
174
|
-
}
|
|
175
|
-
if (mimeTypes.length > 0 && !matchesFileMimeType(mimeType, mimeTypes)) {
|
|
176
|
-
return this.app.throw(403, `unsupported file mimeType: ${mimeType}`);
|
|
177
|
-
}
|
|
178
145
|
return {
|
|
179
146
|
...context,
|
|
180
|
-
maxSize,
|
|
147
|
+
maxSize: uploadOptions.maxSize,
|
|
181
148
|
mimeTypes: mimeTypes.length > 0 ? mimeTypes : undefined,
|
|
182
149
|
extensions: extensions.length > 0 ? extensions : undefined,
|
|
183
150
|
multiple: uploadOptions.multiple,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async resolveUploadPolicy(data: {
|
|
155
|
+
fileScene: keyof IFileSceneRecord;
|
|
156
|
+
size: number;
|
|
157
|
+
mimeType: string;
|
|
158
|
+
}): Promise<IFileUploadPolicyResolved> {
|
|
159
|
+
const policy = await this.resolveUploadUrlPolicy({ fileScene: data.fileScene });
|
|
160
|
+
const mimeType = data.mimeType.toLowerCase();
|
|
161
|
+
if (policy.maxSize && data.size > policy.maxSize) {
|
|
162
|
+
return this.app.throw(403, `file too large: maxSize=${policy.maxSize}`);
|
|
163
|
+
}
|
|
164
|
+
if (policy.mimeTypes?.length && !matchesFileMimeType(mimeType, policy.mimeTypes)) {
|
|
165
|
+
return this.app.throw(403, `unsupported file mimeType: ${mimeType}`);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
...policy,
|
|
184
169
|
fileSize: data.size,
|
|
185
170
|
mimeType,
|
|
186
171
|
};
|
|
@@ -217,8 +202,6 @@ export class BeanFileUploadPolicy extends BeanBase {
|
|
|
217
202
|
clientOptions: { public?: boolean } | undefined,
|
|
218
203
|
sceneOptions: IDecoratorFileSceneOptions,
|
|
219
204
|
) {
|
|
220
|
-
return
|
|
221
|
-
sceneOptions.upload?.public ?? clientOptions?.public ?? this.scope.config.file.public ?? false
|
|
222
|
-
);
|
|
205
|
+
return sceneOptions.public ?? clientOptions?.public ?? this.scope.config.file.public ?? false;
|
|
223
206
|
}
|
|
224
207
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IMetaOptionsIndex } from 'vona-module-a-index';
|
|
2
|
+
|
|
3
|
+
import { BeanBase } from 'vona';
|
|
4
|
+
import { Meta } from 'vona-module-a-meta';
|
|
5
|
+
import { $tableColumns } from 'vona-module-a-ormutils';
|
|
6
|
+
|
|
7
|
+
@Meta<IMetaOptionsIndex>({
|
|
8
|
+
indexes: {
|
|
9
|
+
...$tableColumns('aFile', 'status+draftExpiresAt'),
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
export class MetaIndex extends BeanBase {}
|