@appsemble/node-utils 0.37.4 → 0.37.5
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/README.md +3 -3
- package/assetCssURL.d.ts +9 -0
- package/assetCssURL.js +134 -0
- package/getBlockAssetDownloadUrl.d.ts +3 -0
- package/getBlockAssetDownloadUrl.js +22 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +5 -5
- package/s3.d.ts +11 -2
- package/s3.js +81 -8
- package/server/routes/appRouter/blockAssetHandler.js +11 -2
- package/server/types.d.ts +5 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#  Appsemble Node Utilities
|
|
2
2
|
|
|
3
3
|
> NodeJS utilities used by Appsemble internally.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@appsemble/node-utils)
|
|
6
|
-
[](https://gitlab.com/appsemble/appsemble/-/releases/0.37.5)
|
|
7
7
|
[](https://prettier.io)
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
@@ -26,5 +26,5 @@ compatibility is not guaranteed.
|
|
|
26
26
|
|
|
27
27
|
## License
|
|
28
28
|
|
|
29
|
-
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.37.
|
|
29
|
+
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.37.5/LICENSE.md) ©
|
|
30
30
|
[Appsemble](https://appsemble.com)
|
package/assetCssURL.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `asset()` functions and app asset URLs in CSS to absolute app asset URLs.
|
|
3
|
+
*
|
|
4
|
+
* @param css The CSS to process.
|
|
5
|
+
* @param appId The id of the app the assets belong to.
|
|
6
|
+
* @param host The host on which the app asset endpoints are available.
|
|
7
|
+
* @returns The CSS with all app asset references resolved.
|
|
8
|
+
*/
|
|
9
|
+
export declare function replaceAssetFunctions(css: string, appId: number | undefined, host: string): string;
|
package/assetCssURL.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const assetUrlMatcher = /url\(\s*asset\(\s*(?:("|')(.*?)\1|([^\s"'()][^)]*))\s*\)\s*\)/g;
|
|
2
|
+
const standaloneAssetMatcher = /asset\(\s*(?:("|')(.*?)\1|([^\s"'()][^)]*))\s*\)/g;
|
|
3
|
+
const urlMatcher = /url\(\s*(?:("|')(.*?)\1|([^\s"'()][^)]*))\s*\)/g;
|
|
4
|
+
const appAssetPathMatcher = /^\/api\/apps\/\d+\/assets\/(.+)$/;
|
|
5
|
+
function hasRejectedTokens(value) {
|
|
6
|
+
let currentValue = value;
|
|
7
|
+
for (let i = 0; i < 3; i += 1) {
|
|
8
|
+
if (currentValue.includes('..') || /%2f|%5c/i.test(currentValue)) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (!currentValue.includes('%')) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const decodedValue = decodeURIComponent(currentValue);
|
|
16
|
+
if (decodedValue === currentValue) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (decodedValue.includes('/') || decodedValue.includes('\\')) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
currentValue = decodedValue;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function rewriteAppAssetURL(url, appId, host) {
|
|
31
|
+
const trimmedUrl = url.trim();
|
|
32
|
+
if (trimmedUrl.startsWith('/')) {
|
|
33
|
+
const appAssetPathMatch = appAssetPathMatcher.exec(trimmedUrl);
|
|
34
|
+
if (!appAssetPathMatch) {
|
|
35
|
+
return trimmedUrl;
|
|
36
|
+
}
|
|
37
|
+
const appAssetPath = appAssetPathMatch[1];
|
|
38
|
+
if (hasRejectedTokens(appAssetPath)) {
|
|
39
|
+
return trimmedUrl;
|
|
40
|
+
}
|
|
41
|
+
return String(new URL(`/api/apps/${appId}/assets/${appAssetPath}`, host));
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const parsedUrl = new URL(trimmedUrl, host);
|
|
45
|
+
const appAssetPathMatch = appAssetPathMatcher.exec(parsedUrl.pathname);
|
|
46
|
+
if (!appAssetPathMatch) {
|
|
47
|
+
return trimmedUrl;
|
|
48
|
+
}
|
|
49
|
+
const appAssetPath = appAssetPathMatch[1];
|
|
50
|
+
if (hasRejectedTokens(appAssetPath)) {
|
|
51
|
+
return trimmedUrl;
|
|
52
|
+
}
|
|
53
|
+
const rewrittenUrl = new URL(`/api/apps/${appId}/assets/${appAssetPath}`, host);
|
|
54
|
+
rewrittenUrl.search = parsedUrl.search;
|
|
55
|
+
rewrittenUrl.hash = parsedUrl.hash;
|
|
56
|
+
return String(rewrittenUrl);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return trimmedUrl;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function resolveAssetURL(appId, assetId, host) {
|
|
63
|
+
if (assetId.startsWith('data:')) {
|
|
64
|
+
return assetId;
|
|
65
|
+
}
|
|
66
|
+
if (assetId.startsWith('/')) {
|
|
67
|
+
const appAssetPathMatch = appAssetPathMatcher.exec(assetId.trim());
|
|
68
|
+
if (appAssetPathMatch && hasRejectedTokens(appAssetPathMatch[1])) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return rewriteAppAssetURL(assetId, appId, host);
|
|
72
|
+
}
|
|
73
|
+
if (/^https?:\/\//.test(assetId)) {
|
|
74
|
+
const trimmedAssetId = assetId.trim();
|
|
75
|
+
try {
|
|
76
|
+
const parsedUrl = new URL(trimmedAssetId, host);
|
|
77
|
+
const appAssetPathMatch = appAssetPathMatcher.exec(parsedUrl.pathname);
|
|
78
|
+
if (appAssetPathMatch && hasRejectedTokens(appAssetPathMatch[1])) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return rewriteAppAssetURL(assetId, appId, host);
|
|
86
|
+
}
|
|
87
|
+
if (hasRejectedTokens(assetId)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return String(new URL(`/api/apps/${appId}/assets/${assetId}`, host));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve `asset()` functions and app asset URLs in CSS to absolute app asset URLs.
|
|
94
|
+
*
|
|
95
|
+
* @param css The CSS to process.
|
|
96
|
+
* @param appId The id of the app the assets belong to.
|
|
97
|
+
* @param host The host on which the app asset endpoints are available.
|
|
98
|
+
* @returns The CSS with all app asset references resolved.
|
|
99
|
+
*/
|
|
100
|
+
export function replaceAssetFunctions(css, appId, host) {
|
|
101
|
+
if (!appId) {
|
|
102
|
+
return css;
|
|
103
|
+
}
|
|
104
|
+
const replaceAssetFunction = (match, ...args) => {
|
|
105
|
+
const quotedAssetId = args[1];
|
|
106
|
+
const unquotedAssetId = args[2];
|
|
107
|
+
const assetId = (quotedAssetId || unquotedAssetId)?.trim();
|
|
108
|
+
if (!assetId) {
|
|
109
|
+
return match;
|
|
110
|
+
}
|
|
111
|
+
const resolvedAssetUrl = resolveAssetURL(appId, assetId, host);
|
|
112
|
+
if (!resolvedAssetUrl) {
|
|
113
|
+
return match;
|
|
114
|
+
}
|
|
115
|
+
return `url('${resolvedAssetUrl}')`;
|
|
116
|
+
};
|
|
117
|
+
const cssWithResolvedAssetFunctionURLs = css
|
|
118
|
+
.replaceAll(assetUrlMatcher, replaceAssetFunction)
|
|
119
|
+
.replaceAll(standaloneAssetMatcher, replaceAssetFunction);
|
|
120
|
+
return cssWithResolvedAssetFunctionURLs.replaceAll(urlMatcher, (match, ...args) => {
|
|
121
|
+
const quotedUrl = args[1];
|
|
122
|
+
const unquotedUrl = args[2];
|
|
123
|
+
const url = (quotedUrl || unquotedUrl)?.trim();
|
|
124
|
+
if (!url) {
|
|
125
|
+
return match;
|
|
126
|
+
}
|
|
127
|
+
const rewrittenUrl = rewriteAppAssetURL(url, appId, host);
|
|
128
|
+
if (rewrittenUrl === url) {
|
|
129
|
+
return match;
|
|
130
|
+
}
|
|
131
|
+
return `url('${rewrittenUrl}')`;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=assetCssURL.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const blockAssetUnsafePathPattern = /(^|\/)\.{1,2}(\/|$)|^\/|\\/;
|
|
2
|
+
export function isValidBlockAssetFilename(filename) {
|
|
3
|
+
if (!filename || blockAssetUnsafePathPattern.test(filename)) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
for (let i = 0; i < filename.length; i += 1) {
|
|
7
|
+
if (filename.charCodeAt(i) < 0x20) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
export function getBlockAssetDownloadUrl(blockUrl, fileUrls, filename) {
|
|
14
|
+
const fileUrl = fileUrls && Object.hasOwn(fileUrls, filename) ? fileUrls[filename] : undefined;
|
|
15
|
+
if (fileUrl) {
|
|
16
|
+
return fileUrl;
|
|
17
|
+
}
|
|
18
|
+
const fallbackUrl = new URL(`${blockUrl.replace(/\/+$/, '')}/asset`);
|
|
19
|
+
fallbackUrl.searchParams.set('filename', filename);
|
|
20
|
+
return String(fallbackUrl);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=getBlockAssetDownloadUrl.js.map
|
package/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from './icon.js';
|
|
|
19
19
|
export * from './readAsset.js';
|
|
20
20
|
export * from './render.js';
|
|
21
21
|
export * from './getAppsembleMessages.js';
|
|
22
|
+
export * from './getBlockAssetDownloadUrl.js';
|
|
22
23
|
export * from './odata.js';
|
|
23
24
|
export * from './resource.js';
|
|
24
25
|
export * from './resourceEtag.js';
|
|
@@ -43,6 +44,7 @@ export * from './container/index.js';
|
|
|
43
44
|
export * from './s3.js';
|
|
44
45
|
export * from './uploads.js';
|
|
45
46
|
export * from './assets.js';
|
|
47
|
+
export * from './assetCssURL.js';
|
|
46
48
|
export * from './getValidTrainings.js';
|
|
47
49
|
export * from './PhoneNumberValidationError.js';
|
|
48
50
|
export * from './createUser.js';
|
package/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export * from './icon.js';
|
|
|
19
19
|
export * from './readAsset.js';
|
|
20
20
|
export * from './render.js';
|
|
21
21
|
export * from './getAppsembleMessages.js';
|
|
22
|
+
export * from './getBlockAssetDownloadUrl.js';
|
|
22
23
|
export * from './odata.js';
|
|
23
24
|
export * from './resource.js';
|
|
24
25
|
export * from './resourceEtag.js';
|
|
@@ -43,6 +44,7 @@ export * from './container/index.js';
|
|
|
43
44
|
export * from './s3.js';
|
|
44
45
|
export * from './uploads.js';
|
|
45
46
|
export * from './assets.js';
|
|
47
|
+
export * from './assetCssURL.js';
|
|
46
48
|
export * from './getValidTrainings.js';
|
|
47
49
|
export * from './PhoneNumberValidationError.js';
|
|
48
50
|
export * from './createUser.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/node-utils",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.5",
|
|
4
4
|
"description": "NodeJS utilities used by Appsemble internally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"test": "vitest"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@appsemble/lang-sdk": "0.37.
|
|
44
|
-
"@appsemble/types": "0.37.
|
|
45
|
-
"@appsemble/utils": "0.37.
|
|
43
|
+
"@appsemble/lang-sdk": "0.37.5",
|
|
44
|
+
"@appsemble/types": "0.37.5",
|
|
45
|
+
"@appsemble/utils": "0.37.5",
|
|
46
46
|
"@formatjs/fast-memoize": "^2.0.0",
|
|
47
47
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
|
48
48
|
"@inquirer/prompts": "^8.0.0",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"logform": "^2.0.0",
|
|
86
86
|
"memfs": "4.57.8",
|
|
87
87
|
"mime-types": "^2.0.0",
|
|
88
|
-
"minio": "^8.0.
|
|
88
|
+
"minio": "^8.0.7",
|
|
89
89
|
"mustache": "^4.0.0",
|
|
90
90
|
"openapi-types": "^12.1.3",
|
|
91
91
|
"parse-duration": "^1.0.0",
|
package/s3.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
2
|
import { type BucketItemStat } from 'minio';
|
|
3
|
+
export interface S3FileReference {
|
|
4
|
+
etag: string;
|
|
5
|
+
key: string;
|
|
6
|
+
lastModified: Date;
|
|
7
|
+
metadata: BucketItemStat['metaData'];
|
|
8
|
+
size: number;
|
|
9
|
+
}
|
|
3
10
|
export interface InitS3ClientParams {
|
|
4
11
|
endPoint: string;
|
|
5
12
|
port?: number;
|
|
@@ -8,11 +15,13 @@ export interface InitS3ClientParams {
|
|
|
8
15
|
secretKey: string;
|
|
9
16
|
}
|
|
10
17
|
export declare function initS3Client({ accessKey, endPoint, port, secretKey, useSSL, }: InitS3ClientParams): void;
|
|
11
|
-
export declare function uploadS3File(bucket: string, key: string, content: Buffer | Readable | string, size?: number): Promise<void>;
|
|
18
|
+
export declare function uploadS3File(bucket: string, key: string, content: Buffer | Readable | string, size?: number, metadata?: Record<string, string>): Promise<void>;
|
|
12
19
|
export declare function uploadS3FileFromPath(bucket: string, key: string, path: string): Promise<void>;
|
|
13
20
|
export declare function getS3File(bucket: string, key: string): Promise<Readable>;
|
|
14
21
|
export declare function getS3FileBuffer(bucket: string, key: string): Promise<Buffer>;
|
|
15
22
|
export declare function getS3FileStats(bucket: string, key: string): Promise<BucketItemStat>;
|
|
23
|
+
export declare function listS3Files(bucket: string): Promise<S3FileReference[]>;
|
|
24
|
+
export declare function setS3BucketPolicy(bucket: string, policy: string): Promise<void>;
|
|
16
25
|
export declare function deleteS3Files(bucket: string, keys: string[]): Promise<void>;
|
|
17
26
|
export declare function deleteS3File(bucket: string, key: string): Promise<void>;
|
|
18
27
|
export declare function clearAllS3Buckets(): Promise<void>;
|
package/s3.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
1
2
|
import { buffer as streamToBuffer } from 'node:stream/consumers';
|
|
2
3
|
import { Client, S3Error } from 'minio';
|
|
3
4
|
import { logger } from './logger.js';
|
|
@@ -41,12 +42,21 @@ async function ensureBucket(name) {
|
|
|
41
42
|
throw error;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
function isS3ErrorCode(error, code) {
|
|
46
|
+
return error instanceof S3Error && error.code === code;
|
|
47
|
+
}
|
|
48
|
+
export async function uploadS3File(bucket, key, content, size, metadata) {
|
|
45
49
|
try {
|
|
46
50
|
await ensureBucket(bucket);
|
|
47
|
-
await s3Client.putObject(bucket, key, content, size);
|
|
51
|
+
await s3Client.putObject(bucket, key, content, size, metadata);
|
|
48
52
|
}
|
|
49
53
|
catch (error) {
|
|
54
|
+
if (isS3ErrorCode(error, 'NoSuchBucket') && !(content instanceof Readable)) {
|
|
55
|
+
logger.warn(error);
|
|
56
|
+
await ensureBucket(bucket);
|
|
57
|
+
await s3Client.putObject(bucket, key, content, size, metadata);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
50
60
|
logger.error(error);
|
|
51
61
|
throw error;
|
|
52
62
|
}
|
|
@@ -57,6 +67,12 @@ export async function uploadS3FileFromPath(bucket, key, path) {
|
|
|
57
67
|
await s3Client.fPutObject(bucket, key, path);
|
|
58
68
|
}
|
|
59
69
|
catch (error) {
|
|
70
|
+
if (isS3ErrorCode(error, 'NoSuchBucket')) {
|
|
71
|
+
logger.warn(error);
|
|
72
|
+
await ensureBucket(bucket);
|
|
73
|
+
await s3Client.fPutObject(bucket, key, path);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
60
76
|
logger.error(error);
|
|
61
77
|
throw error;
|
|
62
78
|
}
|
|
@@ -92,6 +108,39 @@ export async function getS3FileStats(bucket, key) {
|
|
|
92
108
|
throw error;
|
|
93
109
|
}
|
|
94
110
|
}
|
|
111
|
+
export async function listS3Files(bucket) {
|
|
112
|
+
const keys = await new Promise((resolve, reject) => {
|
|
113
|
+
const objects = [];
|
|
114
|
+
const stream = s3Client.listObjectsV2(bucket, '', true);
|
|
115
|
+
stream.on('data', (item) => {
|
|
116
|
+
if (item.name) {
|
|
117
|
+
objects.push(item.name);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
stream.on('error', reject);
|
|
121
|
+
stream.on('end', () => resolve(objects));
|
|
122
|
+
});
|
|
123
|
+
return Promise.all(keys.map(async (key) => {
|
|
124
|
+
const stats = await getS3FileStats(bucket, key);
|
|
125
|
+
return {
|
|
126
|
+
etag: stats.etag,
|
|
127
|
+
key,
|
|
128
|
+
lastModified: stats.lastModified,
|
|
129
|
+
metadata: stats.metaData,
|
|
130
|
+
size: stats.size,
|
|
131
|
+
};
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
export async function setS3BucketPolicy(bucket, policy) {
|
|
135
|
+
try {
|
|
136
|
+
await ensureBucket(bucket);
|
|
137
|
+
await s3Client.setBucketPolicy(bucket, policy);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
logger.error(error);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
95
144
|
export async function deleteS3Files(bucket, keys) {
|
|
96
145
|
try {
|
|
97
146
|
await s3Client.removeObjects(bucket, keys);
|
|
@@ -112,13 +161,37 @@ export async function clearAllS3Buckets() {
|
|
|
112
161
|
try {
|
|
113
162
|
const buckets = await s3Client.listBuckets();
|
|
114
163
|
for (const bucket of buckets) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
164
|
+
try {
|
|
165
|
+
const objectsStream = s3Client.listObjectsV2(bucket.name, '', true);
|
|
166
|
+
const objects = [];
|
|
167
|
+
for await (const o of objectsStream) {
|
|
168
|
+
objects.push(o.name);
|
|
169
|
+
}
|
|
170
|
+
await s3Client.removeObjects(bucket.name, objects);
|
|
171
|
+
try {
|
|
172
|
+
await s3Client.removeBucket(bucket.name);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (isS3ErrorCode(error, 'NoSuchBucket')) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (!isS3ErrorCode(error, 'BucketNotEmpty')) {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
const remainingObjectsStream = s3Client.listObjectsV2(bucket.name, '', true);
|
|
182
|
+
const remainingObjects = [];
|
|
183
|
+
for await (const o of remainingObjectsStream) {
|
|
184
|
+
remainingObjects.push(o.name);
|
|
185
|
+
}
|
|
186
|
+
await s3Client.removeObjects(bucket.name, remainingObjects);
|
|
187
|
+
await s3Client.removeBucket(bucket.name);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (!isS3ErrorCode(error, 'NoSuchBucket')) {
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
119
194
|
}
|
|
120
|
-
await s3Client.removeObjects(bucket.name, objects);
|
|
121
|
-
await s3Client.removeBucket(bucket.name);
|
|
122
195
|
}
|
|
123
196
|
}
|
|
124
197
|
catch (error) {
|
|
@@ -6,8 +6,17 @@ export function createBlockAssetHandler({ getBlockAsset }) {
|
|
|
6
6
|
params: { filename, name, version }, } = ctx;
|
|
7
7
|
const blockAsset = await getBlockAsset({ filename, name, version, context: ctx });
|
|
8
8
|
assertKoaCondition(blockAsset != null, ctx, 404, 'Block asset not found');
|
|
9
|
-
ctx.set('Cache-Control', 'max-age=31536000,immutable');
|
|
10
|
-
|
|
9
|
+
ctx.set('Cache-Control', 'public,max-age=31536000,immutable');
|
|
10
|
+
if (blockAsset.size != null) {
|
|
11
|
+
ctx.set('Content-Length', String(blockAsset.size));
|
|
12
|
+
}
|
|
13
|
+
if (blockAsset.etag) {
|
|
14
|
+
ctx.set('ETag', blockAsset.etag);
|
|
15
|
+
}
|
|
16
|
+
if (blockAsset.lastModified) {
|
|
17
|
+
ctx.set('Last-Modified', blockAsset.lastModified.toUTCString());
|
|
18
|
+
}
|
|
19
|
+
ctx.body = blockAsset.stream ?? blockAsset.content;
|
|
11
20
|
ctx.type = blockAsset.mime;
|
|
12
21
|
};
|
|
13
22
|
}
|
package/server/types.d.ts
CHANGED
|
@@ -391,7 +391,11 @@ export interface AppAsset extends Asset {
|
|
|
391
391
|
export interface ProjectAsset {
|
|
392
392
|
filename: string;
|
|
393
393
|
mime: string;
|
|
394
|
-
content
|
|
394
|
+
content?: Buffer;
|
|
395
|
+
stream?: Readable;
|
|
396
|
+
size?: number;
|
|
397
|
+
etag?: string;
|
|
398
|
+
lastModified?: Date;
|
|
395
399
|
}
|
|
396
400
|
export interface Block extends BlockDefinition {
|
|
397
401
|
OrganizationId: string;
|