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
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import type { SharpConstructor } from 'sharp';
|
|
2
2
|
import type {
|
|
3
3
|
EntityImage,
|
|
4
|
-
|
|
5
|
-
IImageDirectUploadInput,
|
|
6
|
-
IImageProviderDirectUploadResource,
|
|
4
|
+
IImageProviderDeliveryOptions,
|
|
7
5
|
IImageProviderResolvedVariant,
|
|
8
6
|
IImageProviderResource,
|
|
9
7
|
IImageTransformOptions,
|
|
10
8
|
IImageUploadInput,
|
|
11
9
|
IImageVariantRequest,
|
|
12
10
|
} from 'vona-module-a-image';
|
|
13
|
-
import type { IUploadFile } from 'vona-module-a-upload';
|
|
14
11
|
|
|
15
12
|
import fse from 'fs-extra';
|
|
16
13
|
import { createHash } from 'node:crypto';
|
|
17
14
|
import path from 'node:path';
|
|
18
|
-
import { BeanBase, uuidv4 } from 'vona';
|
|
15
|
+
import { BeanBase, getRuntimePathPhysicalRoot, uuidv4 } from 'vona';
|
|
19
16
|
import { Service } from 'vona-module-a-bean';
|
|
20
17
|
import { resolveImageVariantRequestToTransform } from 'vona-module-a-image';
|
|
21
18
|
|
|
@@ -23,21 +20,7 @@ import type { IImageProviderNativeClientOptions } from '../bean/imageProvider.na
|
|
|
23
20
|
|
|
24
21
|
type IImageNativeStoredImage = Pick<
|
|
25
22
|
EntityImage,
|
|
26
|
-
'resourceId' | 'filename' | 'storagePath' | 'variants'
|
|
27
|
-
>;
|
|
28
|
-
|
|
29
|
-
type IImageNativeDraftImage = Pick<
|
|
30
|
-
EntityImage,
|
|
31
|
-
| 'resourceId'
|
|
32
|
-
| 'filename'
|
|
33
|
-
| 'contentType'
|
|
34
|
-
| 'meta'
|
|
35
|
-
| 'imageScene'
|
|
36
|
-
| 'requireSignedURLs'
|
|
37
|
-
| 'storagePath'
|
|
38
|
-
| 'deliveryBaseUrl'
|
|
39
|
-
| 'variants'
|
|
40
|
-
| 'clientName'
|
|
23
|
+
'id' | 'resourceId' | 'filename' | 'contentType' | 'public' | 'storagePath' | 'variants'
|
|
41
24
|
>;
|
|
42
25
|
|
|
43
26
|
@Service()
|
|
@@ -56,80 +39,24 @@ export class ServiceImageNative extends BeanBase {
|
|
|
56
39
|
options: IImageProviderNativeClientOptions,
|
|
57
40
|
): Promise<IImageProviderResource> {
|
|
58
41
|
const resourceId = uuidv4();
|
|
59
|
-
const
|
|
60
|
-
await
|
|
61
|
-
return await this._buildStoredResource(targetPath, {
|
|
42
|
+
const isPublic = input.public ?? options.public;
|
|
43
|
+
const targetPath = await this._getFinalPath(
|
|
62
44
|
resourceId,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
variants: options.variants ?? this.scope.config.imageNative.variants,
|
|
68
|
-
deliveryBaseUrl: options.deliveryBaseUrl,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async createDirectUpload(
|
|
73
|
-
input: IImageDirectUploadInput,
|
|
74
|
-
options: IImageProviderNativeClientOptions,
|
|
75
|
-
): Promise<IImageProviderDirectUploadResource> {
|
|
76
|
-
const resourceId = this._sanitizeResourceId(input.customId) ?? uuidv4();
|
|
77
|
-
const storagePath = await this._getFinalPath(resourceId, input.filename, options);
|
|
78
|
-
const routePath = this.scope.util.combineApiPath(
|
|
79
|
-
`image-native/direct-upload/${encodeURIComponent(resourceId)}`,
|
|
80
|
-
false,
|
|
81
|
-
true,
|
|
45
|
+
input.filename,
|
|
46
|
+
isPublic,
|
|
47
|
+
options,
|
|
48
|
+
input.file,
|
|
82
49
|
);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
true,
|
|
87
|
-
);
|
|
88
|
-
const tokenPayload = await this.bean.imageUploadPolicy.createDirectUploadToken({
|
|
89
|
-
resourceId,
|
|
90
|
-
path: tokenPath,
|
|
91
|
-
});
|
|
92
|
-
const uploadUrl = new URL(this.app.util.getAbsoluteUrlByApiPath(routePath));
|
|
93
|
-
uploadUrl.searchParams.set('token', tokenPayload.token);
|
|
94
|
-
return {
|
|
50
|
+
await fse.ensureDir(path.dirname(targetPath));
|
|
51
|
+
await fse.copy(input.file, targetPath);
|
|
52
|
+
return await this._buildStoredResource(targetPath, {
|
|
95
53
|
resourceId,
|
|
96
54
|
filename: input.filename,
|
|
97
55
|
contentType: input.contentType,
|
|
98
56
|
meta: input.meta,
|
|
99
|
-
|
|
57
|
+
public: isPublic,
|
|
100
58
|
variants: options.variants ?? this.scope.config.imageNative.variants,
|
|
101
59
|
deliveryBaseUrl: options.deliveryBaseUrl,
|
|
102
|
-
storagePath,
|
|
103
|
-
uploadUrl: uploadUrl.toString(),
|
|
104
|
-
draft: true,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async uploadDirectFile(image: IImageNativeDraftImage, file: IUploadFile) {
|
|
109
|
-
const draftPath = this._getDraftPath(image.storagePath, image.resourceId);
|
|
110
|
-
await fse.ensureDir(path.dirname(draftPath));
|
|
111
|
-
await fse.move(file.file, draftPath, { overwrite: true });
|
|
112
|
-
return draftPath;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async finalizeDirectUpload(
|
|
116
|
-
image: IImageNativeDraftImage,
|
|
117
|
-
options: IImageProviderNativeClientOptions,
|
|
118
|
-
): Promise<IImageProviderResource | undefined> {
|
|
119
|
-
const storagePath =
|
|
120
|
-
image.storagePath ?? (await this._getFinalPath(image.resourceId, image.filename, options));
|
|
121
|
-
const draftPath = this._getDraftPath(storagePath, image.resourceId);
|
|
122
|
-
if (!(await fse.pathExists(draftPath))) return undefined;
|
|
123
|
-
await fse.ensureDir(path.dirname(storagePath));
|
|
124
|
-
await fse.move(draftPath, storagePath, { overwrite: true });
|
|
125
|
-
return await this._buildStoredResource(storagePath, {
|
|
126
|
-
resourceId: image.resourceId,
|
|
127
|
-
filename: image.filename,
|
|
128
|
-
contentType: image.contentType,
|
|
129
|
-
meta: image.meta,
|
|
130
|
-
requireSignedURLs: image.requireSignedURLs ?? options.requireSignedURLs,
|
|
131
|
-
variants: image.variants ?? options.variants ?? this.scope.config.imageNative.variants,
|
|
132
|
-
deliveryBaseUrl: image.deliveryBaseUrl ?? options.deliveryBaseUrl,
|
|
133
60
|
});
|
|
134
61
|
}
|
|
135
62
|
|
|
@@ -137,8 +64,6 @@ export class ServiceImageNative extends BeanBase {
|
|
|
137
64
|
if (!image.storagePath) return;
|
|
138
65
|
const dir = path.dirname(image.storagePath);
|
|
139
66
|
if (!(await fse.pathExists(dir))) return;
|
|
140
|
-
const draftPath = this._getDraftPath(image.storagePath, image.resourceId);
|
|
141
|
-
await fse.remove(draftPath);
|
|
142
67
|
const entries = await fse.readdir(dir);
|
|
143
68
|
await Promise.all(
|
|
144
69
|
entries
|
|
@@ -154,8 +79,13 @@ export class ServiceImageNative extends BeanBase {
|
|
|
154
79
|
image: IImageNativeStoredImage,
|
|
155
80
|
request: IImageVariantRequest,
|
|
156
81
|
options: IImageProviderNativeClientOptions,
|
|
157
|
-
_deliveryOptions?:
|
|
82
|
+
_deliveryOptions?: IImageProviderDeliveryOptions,
|
|
158
83
|
) {
|
|
84
|
+
if (!(image.public ?? options.public)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Private image-native variant URL requires signed delivery: ${image.resourceId}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
159
89
|
const resolved = resolveImageVariantRequestToTransform(request, 'original', image.variants);
|
|
160
90
|
if (
|
|
161
91
|
resolved.variantName === 'original' &&
|
|
@@ -167,6 +97,34 @@ export class ServiceImageNative extends BeanBase {
|
|
|
167
97
|
return await this._buildUrl(targetPath, options.deliveryBaseUrl);
|
|
168
98
|
}
|
|
169
99
|
|
|
100
|
+
async downloadBuffer(
|
|
101
|
+
image: IImageNativeStoredImage,
|
|
102
|
+
request: IImageVariantRequest,
|
|
103
|
+
): Promise<{
|
|
104
|
+
buffer: Buffer;
|
|
105
|
+
filename?: string;
|
|
106
|
+
contentType?: string;
|
|
107
|
+
} | null> {
|
|
108
|
+
if (!image.storagePath) {
|
|
109
|
+
throw new Error(`Image storage path missing: ${image.resourceId}`);
|
|
110
|
+
}
|
|
111
|
+
const resolved = resolveImageVariantRequestToTransform(request, 'original', image.variants);
|
|
112
|
+
const targetPath =
|
|
113
|
+
resolved.variantName === 'original' && this._isOriginalTransform(resolved.transformOptions)
|
|
114
|
+
? image.storagePath
|
|
115
|
+
: await this._ensureVariantFile(image, resolved);
|
|
116
|
+
if (!(await fse.pathExists(targetPath))) return null;
|
|
117
|
+
return {
|
|
118
|
+
buffer: await fse.readFile(targetPath),
|
|
119
|
+
filename: image.filename,
|
|
120
|
+
contentType: this._resolveContentType(
|
|
121
|
+
targetPath,
|
|
122
|
+
image.contentType,
|
|
123
|
+
resolved.transformOptions,
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
170
128
|
private async _buildStoredResource(
|
|
171
129
|
targetPath: string,
|
|
172
130
|
data: {
|
|
@@ -174,7 +132,7 @@ export class ServiceImageNative extends BeanBase {
|
|
|
174
132
|
filename?: string;
|
|
175
133
|
contentType?: string;
|
|
176
134
|
meta?: Record<string, unknown>;
|
|
177
|
-
|
|
135
|
+
public?: boolean;
|
|
178
136
|
variants?: EntityImage['variants'];
|
|
179
137
|
deliveryBaseUrl?: string;
|
|
180
138
|
},
|
|
@@ -189,7 +147,7 @@ export class ServiceImageNative extends BeanBase {
|
|
|
189
147
|
size: Number(stat.size),
|
|
190
148
|
width: metadata.width,
|
|
191
149
|
height: metadata.height,
|
|
192
|
-
|
|
150
|
+
public: data.public,
|
|
193
151
|
variants: data.variants,
|
|
194
152
|
meta: data.meta,
|
|
195
153
|
storagePath: targetPath,
|
|
@@ -200,24 +158,34 @@ export class ServiceImageNative extends BeanBase {
|
|
|
200
158
|
private async _getFinalPath(
|
|
201
159
|
resourceId: string,
|
|
202
160
|
filename: string | undefined,
|
|
161
|
+
isPublic: boolean | undefined,
|
|
203
162
|
options: IImageProviderNativeClientOptions,
|
|
204
163
|
filePath?: string,
|
|
205
164
|
) {
|
|
206
|
-
const
|
|
207
|
-
path.join('image-native', options.subdir ?? 'default'),
|
|
208
|
-
true,
|
|
209
|
-
);
|
|
165
|
+
const basePath = await this._getBasePath(isPublic, options);
|
|
210
166
|
const ext = this._getOriginalExt(filename, filePath ?? resourceId);
|
|
211
|
-
return path.join(
|
|
167
|
+
return path.join(basePath, `${resourceId}${ext}`);
|
|
212
168
|
}
|
|
213
169
|
|
|
214
|
-
private
|
|
215
|
-
|
|
216
|
-
|
|
170
|
+
private async _getBasePath(
|
|
171
|
+
isPublic: boolean | undefined,
|
|
172
|
+
options: IImageProviderNativeClientOptions,
|
|
173
|
+
) {
|
|
174
|
+
if (isPublic) {
|
|
175
|
+
return await this.app.util.getPublicPathPhysical(
|
|
176
|
+
path.join('image-native', options.subdir ?? 'default'),
|
|
177
|
+
true,
|
|
178
|
+
);
|
|
217
179
|
}
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
180
|
+
const runtimeRoot = getRuntimePathPhysicalRoot(this.app);
|
|
181
|
+
const basePath = path.join(
|
|
182
|
+
runtimeRoot,
|
|
183
|
+
this.ctx.instance.id.toString(),
|
|
184
|
+
'image-native',
|
|
185
|
+
options.subdir ?? 'default',
|
|
186
|
+
);
|
|
187
|
+
await fse.ensureDir(basePath);
|
|
188
|
+
return basePath;
|
|
221
189
|
}
|
|
222
190
|
|
|
223
191
|
private async _ensureVariantFile(
|
|
@@ -308,8 +276,15 @@ export class ServiceImageNative extends BeanBase {
|
|
|
308
276
|
|
|
309
277
|
private async _getVariantStaticPath(targetPath: string) {
|
|
310
278
|
const publicRoot = await this.app.util.getPublicPathPhysical(undefined, true);
|
|
311
|
-
const relativePath = path.relative(publicRoot, targetPath)
|
|
312
|
-
|
|
279
|
+
const relativePath = path.relative(publicRoot, targetPath);
|
|
280
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
281
|
+
throw new Error(`Image target path is not publicly accessible: ${targetPath}`);
|
|
282
|
+
}
|
|
283
|
+
return path.posix.join(
|
|
284
|
+
'public',
|
|
285
|
+
this.ctx.instance.id.toString(),
|
|
286
|
+
relativePath.split(path.sep).join(path.posix.sep),
|
|
287
|
+
);
|
|
313
288
|
}
|
|
314
289
|
|
|
315
290
|
private async _getVariantAbsolutePath(
|
|
@@ -323,8 +298,8 @@ export class ServiceImageNative extends BeanBase {
|
|
|
323
298
|
const variantName = this._sanitizeVariantName(resolved.variantName);
|
|
324
299
|
return path.join(dir, `${image.resourceId}__${variantName}${ext}`);
|
|
325
300
|
}
|
|
326
|
-
const
|
|
327
|
-
return path.join(dir, `${image.resourceId}__t_${
|
|
301
|
+
const transformKey = this._buildTransformKey(resolved.transformOptions);
|
|
302
|
+
return path.join(dir, `${image.resourceId}__t_${transformKey}${ext}`);
|
|
328
303
|
}
|
|
329
304
|
|
|
330
305
|
private _getVariantExt(
|
|
@@ -348,7 +323,25 @@ export class ServiceImageNative extends BeanBase {
|
|
|
348
323
|
return format === 'jpeg' ? 'jpeg' : format;
|
|
349
324
|
}
|
|
350
325
|
|
|
351
|
-
private
|
|
326
|
+
private _resolveContentType(
|
|
327
|
+
targetPath: string,
|
|
328
|
+
contentType: string | undefined,
|
|
329
|
+
transformOptions: IImageTransformOptions,
|
|
330
|
+
) {
|
|
331
|
+
const format = this._normalizeFormat(transformOptions.format);
|
|
332
|
+
if (format === 'jpeg') return 'image/jpeg';
|
|
333
|
+
if (format === 'png') return 'image/png';
|
|
334
|
+
if (format === 'webp') return 'image/webp';
|
|
335
|
+
if (format === 'avif') return 'image/avif';
|
|
336
|
+
const ext = path.extname(targetPath).toLowerCase();
|
|
337
|
+
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
|
|
338
|
+
if (ext === '.png') return 'image/png';
|
|
339
|
+
if (ext === '.webp') return 'image/webp';
|
|
340
|
+
if (ext === '.avif') return 'image/avif';
|
|
341
|
+
return contentType;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private _buildTransformKey(transformOptions: IImageTransformOptions) {
|
|
352
345
|
const stable = this._stableStringify(transformOptions);
|
|
353
346
|
return createHash('sha1').update(stable).digest('hex').slice(0, 12);
|
|
354
347
|
}
|
|
@@ -368,12 +361,6 @@ export class ServiceImageNative extends BeanBase {
|
|
|
368
361
|
return JSON.stringify(value);
|
|
369
362
|
}
|
|
370
363
|
|
|
371
|
-
private _sanitizeResourceId(resourceId: string | undefined) {
|
|
372
|
-
if (!resourceId) return undefined;
|
|
373
|
-
const sanitized = resourceId.replace(/[^\w.-]/g, '-');
|
|
374
|
-
return sanitized || undefined;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
364
|
private _sanitizeVariantName(variantName: string) {
|
|
378
365
|
return variantName.replace(/[^\w.-]/g, '-');
|
|
379
366
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { VonaApplication } from 'vona';
|
|
2
2
|
|
|
3
|
+
import { $protocolKey } from 'vona';
|
|
4
|
+
|
|
3
5
|
import type { ConfigJwt } from '../types/jwt.ts';
|
|
4
6
|
|
|
5
7
|
export function config(app: VonaApplication) {
|
|
@@ -15,7 +17,7 @@ export function config(app: VonaApplication) {
|
|
|
15
17
|
header: '',
|
|
16
18
|
headerAuth: 'authorization',
|
|
17
19
|
headerAuthScheme: 'bearer',
|
|
18
|
-
query: '
|
|
20
|
+
query: $protocolKey('x-vona-passport-code'),
|
|
19
21
|
},
|
|
20
22
|
},
|
|
21
23
|
tempAuthToken: {
|
|
@@ -67,7 +67,9 @@ export class ServiceJwtClient extends BeanBase {
|
|
|
67
67
|
[this.fieldClient]: this._clientName,
|
|
68
68
|
[this.fieldData]: payloadData,
|
|
69
69
|
};
|
|
70
|
-
if (options?.path)
|
|
70
|
+
if (options?.path) {
|
|
71
|
+
payload[this.fieldPath] = options.path;
|
|
72
|
+
}
|
|
71
73
|
let signOptions = this._clientOptions.signOptions;
|
|
72
74
|
if (options?.dev) {
|
|
73
75
|
signOptions = Object.assign({}, signOptions, {
|
|
@@ -123,8 +125,9 @@ export class ServiceJwtClient extends BeanBase {
|
|
|
123
125
|
// check field client
|
|
124
126
|
if (payload[this.fieldClient] !== this._clientName) return this.app.throw(401);
|
|
125
127
|
// check field path
|
|
126
|
-
if (!this._checkVerifyPath(payload[this.fieldPath], options?.path))
|
|
128
|
+
if (!this._checkVerifyPath(payload[this.fieldPath], options?.path)) {
|
|
127
129
|
return this.app.throw(401);
|
|
130
|
+
}
|
|
128
131
|
// passed
|
|
129
132
|
resolve(payload[this.fieldData]);
|
|
130
133
|
},
|
|
@@ -135,7 +138,9 @@ export class ServiceJwtClient extends BeanBase {
|
|
|
135
138
|
_checkVerifyPath(pathTarget: string | string[] | undefined, pathReal: string | undefined) {
|
|
136
139
|
if (!pathTarget) return true;
|
|
137
140
|
const path = pathReal ?? String(this.ctx.route.routePathRaw);
|
|
138
|
-
|
|
139
|
-
return
|
|
141
|
+
const targets = Array.isArray(pathTarget) ? pathTarget : [pathTarget];
|
|
142
|
+
return (
|
|
143
|
+
targets.every(target => typeof target === 'string') && targets.some(target => target === path)
|
|
144
|
+
);
|
|
140
145
|
}
|
|
141
146
|
}
|
|
@@ -28,9 +28,7 @@ export class InterceptorUpload extends BeanBase implements IInterceptorExecute {
|
|
|
28
28
|
res = await next();
|
|
29
29
|
} finally {
|
|
30
30
|
// delete temp files
|
|
31
|
-
|
|
32
|
-
fileCleanup();
|
|
33
|
-
}
|
|
31
|
+
this._cleanupFiles(filesCleanup);
|
|
34
32
|
this.ctx[SymbolUploadValue] = undefined;
|
|
35
33
|
}
|
|
36
34
|
// ok
|
|
@@ -45,81 +43,132 @@ export class InterceptorUpload extends BeanBase implements IInterceptorExecute {
|
|
|
45
43
|
const files: IUploadFile[] = [];
|
|
46
44
|
const filesCleanup: Function[] = [];
|
|
47
45
|
const fileWrites: Promise<void>[] = [];
|
|
46
|
+
const fileStreams: any[] = [];
|
|
47
|
+
const writeStreams: ReturnType<typeof createWriteStream>[] = [];
|
|
48
|
+
let bb: any;
|
|
48
49
|
let settled = false;
|
|
49
50
|
const rejectOnce = (err: Error) => {
|
|
50
51
|
if (settled) return;
|
|
51
52
|
settled = true;
|
|
53
|
+
for (const file of fileStreams) {
|
|
54
|
+
file.unpipe();
|
|
55
|
+
file.resume();
|
|
56
|
+
}
|
|
57
|
+
for (const writeStream of writeStreams) {
|
|
58
|
+
writeStream.destroy();
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
this.ctx.req.unpipe(bb);
|
|
62
|
+
bb?.destroy();
|
|
63
|
+
this.ctx.req.resume();
|
|
64
|
+
} catch {
|
|
65
|
+
// ignore cleanup errors and preserve the receive error
|
|
66
|
+
}
|
|
67
|
+
this._cleanupFiles(filesCleanup);
|
|
52
68
|
reject(err);
|
|
53
69
|
};
|
|
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
|
-
|
|
70
|
+
try {
|
|
71
|
+
// bb
|
|
72
|
+
bb = Busboy(
|
|
73
|
+
Object.assign({ defParamCharset: 'utf8' }, options.busboy, {
|
|
74
|
+
headers: this.ctx.req.headers,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
bb.on('file', (name, file, info) => {
|
|
78
|
+
if (settled) {
|
|
79
|
+
file.resume();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
fileStreams.push(file);
|
|
83
|
+
let fileTempObj: tmp.FileResult;
|
|
84
|
+
try {
|
|
85
|
+
fileTempObj = tmp.fileSync({
|
|
86
|
+
prefix: '.temp-upload-',
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
file.resume();
|
|
90
|
+
rejectOnce(err as Error);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const fileTemp = fileTempObj.name;
|
|
94
|
+
filesCleanup.push(fileTempObj.removeCallback);
|
|
95
|
+
files.push({
|
|
96
|
+
name,
|
|
97
|
+
file: fileTemp,
|
|
98
|
+
info,
|
|
80
99
|
});
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
// save
|
|
101
|
+
const writeStream = createWriteStream(fileTemp);
|
|
102
|
+
writeStreams.push(writeStream);
|
|
103
|
+
const fileWrite = new Promise<void>(resolveWrite => {
|
|
104
|
+
const complete = () => resolveWrite();
|
|
105
|
+
writeStream.on('finish', complete);
|
|
106
|
+
writeStream.on('error', err => {
|
|
107
|
+
rejectOnce(err);
|
|
108
|
+
complete();
|
|
109
|
+
});
|
|
110
|
+
file.on('error', err => {
|
|
111
|
+
rejectOnce(err);
|
|
112
|
+
complete();
|
|
113
|
+
});
|
|
114
|
+
file.on('limit', () => {
|
|
115
|
+
rejectOnce(this._createLimitError());
|
|
116
|
+
complete();
|
|
117
|
+
});
|
|
83
118
|
});
|
|
119
|
+
fileWrites.push(fileWrite);
|
|
120
|
+
file.pipe(writeStream);
|
|
84
121
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
rejectOnce(
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
name,
|
|
96
|
-
value,
|
|
97
|
-
info,
|
|
122
|
+
bb.on('field', (name, value, info) => {
|
|
123
|
+
if (info.nameTruncated || info.valueTruncated) {
|
|
124
|
+
rejectOnce(this._createLimitError());
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
fields.push({
|
|
128
|
+
name,
|
|
129
|
+
value,
|
|
130
|
+
info,
|
|
131
|
+
});
|
|
98
132
|
});
|
|
99
|
-
|
|
100
|
-
bb.on('close', async () => {
|
|
101
|
-
try {
|
|
133
|
+
bb.on('close', async () => {
|
|
102
134
|
await Promise.all(fileWrites);
|
|
103
135
|
if (settled) return;
|
|
104
136
|
settled = true;
|
|
105
137
|
resolve([fields, files, filesCleanup]);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
});
|
|
139
|
+
bb.on('error', (err: Error) => {
|
|
140
|
+
rejectOnce(err);
|
|
141
|
+
});
|
|
142
|
+
bb.on('partsLimit', () => {
|
|
143
|
+
rejectOnce(this._createLimitError());
|
|
144
|
+
});
|
|
145
|
+
bb.on('filesLimit', () => {
|
|
146
|
+
rejectOnce(this._createLimitError());
|
|
147
|
+
});
|
|
148
|
+
bb.on('fieldsLimit', () => {
|
|
149
|
+
rejectOnce(this._createLimitError());
|
|
150
|
+
});
|
|
151
|
+
this.ctx.req.pipe(bb);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
rejectOnce(err as Error);
|
|
154
|
+
}
|
|
123
155
|
});
|
|
124
156
|
}
|
|
157
|
+
|
|
158
|
+
private _cleanupFiles(filesCleanup: Function[]) {
|
|
159
|
+
for (const fileCleanup of filesCleanup) {
|
|
160
|
+
try {
|
|
161
|
+
fileCleanup();
|
|
162
|
+
} catch {
|
|
163
|
+
// try every cleanup callback without masking the primary error
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private _createLimitError() {
|
|
169
|
+
const err: any = new Error('upload limit exceeded');
|
|
170
|
+
err.status = 413;
|
|
171
|
+
err.code = 413;
|
|
172
|
+
return err as Error;
|
|
173
|
+
}
|
|
125
174
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zova",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.134",
|
|
4
4
|
"gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
|
|
5
5
|
"description": "A vue3 framework with ioc",
|
|
6
6
|
"keywords": [
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"zova-core": "^5.1.77",
|
|
49
|
-
"zova-suite-a-zova": "^5.1.
|
|
49
|
+
"zova-suite-a-zova": "^5.1.133"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"clean-package": "^2.2.0",
|