@promptbook/cli 0.112.0-102 → 0.112.0-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/apps/agents-server/README.md +0 -6
- package/apps/agents-server/src/app/admin/image-generator-test/ImageAttachmentsEditor.tsx +11 -6
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +13 -15
- package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +13 -14
- package/apps/agents-server/src/app/api/upload/route.ts +110 -383
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +52 -55
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +17 -49
- package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +5 -2
- package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +5 -0
- package/apps/agents-server/src/utils/shareTargetPayloads.ts +15 -63
- package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +23 -150
- package/apps/agents-server/src/utils/upload/uploadFileToServer.ts +113 -0
- package/esm/index.es.js +194 -35
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
- package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
- package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +1 -3
- package/src/other/templates/getTemplatesPipelineCollection.ts +737 -815
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +194 -35
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
- package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
- package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/utils/cdn/resolveCdnStorageProvider.ts +0 -40
|
@@ -3,87 +3,84 @@ import { DigitalOceanSpaces } from '../utils/cdn/classes/DigitalOceanSpaces';
|
|
|
3
3
|
import { TrackedFilesStorage } from '../utils/cdn/classes/TrackedFilesStorage';
|
|
4
4
|
import { VercelBlobStorage } from '../utils/cdn/classes/VercelBlobStorage';
|
|
5
5
|
import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
|
|
6
|
-
import { resolveCdnStorageProvider } from '../utils/cdn/resolveCdnStorageProvider';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
8
|
+
* Cache of CDN instance
|
|
10
9
|
*
|
|
11
10
|
* @private internal cache for `$provideCdnForServer`
|
|
12
11
|
*/
|
|
13
|
-
|
|
12
|
+
let cdn: IIFilesStorageWithCdn | null = null;
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @private internal cache for `$provideCdnForServer`
|
|
15
|
+
* Provides a CDN storage interface for server-side file operations, with caching to reuse instances.
|
|
19
16
|
*/
|
|
20
|
-
|
|
17
|
+
export function $provideCdnForServer(): IIFilesStorageWithCdn {
|
|
18
|
+
if (!cdn) {
|
|
19
|
+
const inner = createCdnStorageForServer();
|
|
20
|
+
const supabase = $provideSupabaseForServer();
|
|
21
|
+
cdn = new TrackedFilesStorage(inner, supabase);
|
|
22
|
+
}
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
* @private internal cache for `$provideCdnForServer`
|
|
26
|
-
*/
|
|
27
|
-
let trackedCdn: IIFilesStorageWithCdn | null = null;
|
|
24
|
+
return cdn;
|
|
25
|
+
}
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
|
-
* Creates
|
|
28
|
+
* Creates the configured CDN storage implementation for server-side file operations.
|
|
31
29
|
*
|
|
32
|
-
* @private
|
|
30
|
+
* @private helper of `$provideCdnForServer`
|
|
33
31
|
*/
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
gzip: process.env.CDN_GZIP !== 'false',
|
|
48
|
-
forcePathStyle: process.env.CDN_FORCE_PATH_STYLE === TRUE_ENV_VALUE,
|
|
49
|
-
isPublicReadAclEnabled: process.env.CDN_ENABLE_PUBLIC_READ_ACL !== 'false',
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
case 'vercel':
|
|
53
|
-
return new VercelBlobStorage({
|
|
54
|
-
token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
|
|
55
|
-
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
|
|
56
|
-
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
57
|
-
});
|
|
32
|
+
function createCdnStorageForServer(): IIFilesStorageWithCdn {
|
|
33
|
+
if (isS3CompatibleStorageSelected()) {
|
|
34
|
+
return new DigitalOceanSpaces({
|
|
35
|
+
bucket: process.env.CDN_BUCKET!,
|
|
36
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '',
|
|
37
|
+
endpoint: process.env.CDN_ENDPOINT!,
|
|
38
|
+
accessKeyId: process.env.CDN_ACCESS_KEY_ID!,
|
|
39
|
+
secretAccessKey: process.env.CDN_SECRET_ACCESS_KEY!,
|
|
40
|
+
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
41
|
+
gzip: true,
|
|
42
|
+
forcePathStyle: process.env.CDN_FORCE_PATH_STYLE === 'true',
|
|
43
|
+
region: process.env.CDN_REGION || 'auto',
|
|
44
|
+
});
|
|
58
45
|
}
|
|
46
|
+
|
|
47
|
+
return new VercelBlobStorage({
|
|
48
|
+
token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
|
|
49
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '',
|
|
50
|
+
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
51
|
+
});
|
|
59
52
|
}
|
|
60
53
|
|
|
61
54
|
/**
|
|
62
|
-
*
|
|
55
|
+
* Checks whether the current environment should use the S3-compatible storage implementation.
|
|
63
56
|
*
|
|
64
|
-
* @private
|
|
57
|
+
* @private helper of `$provideCdnForServer`
|
|
65
58
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
function isS3CompatibleStorageSelected(): boolean {
|
|
60
|
+
const storageMode = (process.env.PTBK_FILE_STORAGE_MODE || process.env.CDN_PROVIDER || '').toLowerCase();
|
|
61
|
+
const isS3StorageMode =
|
|
62
|
+
storageMode === 's3' || storageMode === 'external-s3' || storageMode === 'self-contained-s3';
|
|
63
|
+
|
|
64
|
+
if (isS3StorageMode) {
|
|
65
|
+
return hasS3CompatibleStorageConfiguration();
|
|
69
66
|
}
|
|
70
67
|
|
|
71
|
-
return
|
|
68
|
+
return !process.env.VERCEL_BLOB_READ_WRITE_TOKEN && hasS3CompatibleStorageConfiguration();
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
/**
|
|
75
|
-
*
|
|
72
|
+
* Checks whether all S3-compatible storage environment variables are present.
|
|
76
73
|
*
|
|
77
|
-
* @private
|
|
74
|
+
* @private helper of `$provideCdnForServer`
|
|
78
75
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
function hasS3CompatibleStorageConfiguration(): boolean {
|
|
77
|
+
return Boolean(
|
|
78
|
+
process.env.CDN_BUCKET &&
|
|
79
|
+
process.env.CDN_ENDPOINT &&
|
|
80
|
+
process.env.CDN_ACCESS_KEY_ID &&
|
|
81
|
+
process.env.CDN_SECRET_ACCESS_KEY &&
|
|
82
|
+
process.env.NEXT_PUBLIC_CDN_PUBLIC_URL,
|
|
83
|
+
);
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
// TODO: [🏓] Unite `xxxForServer` and `xxxForNode` naming
|
|
@@ -12,45 +12,16 @@ type IDigitalOceanSpacesConfig = {
|
|
|
12
12
|
readonly bucket: string;
|
|
13
13
|
readonly pathPrefix: string;
|
|
14
14
|
readonly endpoint: string;
|
|
15
|
-
readonly region?: string;
|
|
16
15
|
readonly accessKeyId: string;
|
|
17
16
|
readonly secretAccessKey: string;
|
|
18
17
|
readonly cdnPublicUrl: URL;
|
|
19
18
|
readonly gzip: boolean;
|
|
20
19
|
readonly forcePathStyle?: boolean;
|
|
21
|
-
readonly
|
|
20
|
+
readonly region?: string;
|
|
22
21
|
|
|
23
22
|
// TODO: [⛳️] Probbably prefix should be in this config not on the consumer side
|
|
24
23
|
};
|
|
25
24
|
|
|
26
|
-
/**
|
|
27
|
-
* Resolves the S3 endpoint URL, preserving explicit `http` endpoints for local MinIO.
|
|
28
|
-
*
|
|
29
|
-
* @private internal helper for DigitalOceanSpaces.
|
|
30
|
-
*/
|
|
31
|
-
function resolveS3Endpoint(endpoint: string): string {
|
|
32
|
-
if (/^https?:\/\//i.test(endpoint)) {
|
|
33
|
-
return endpoint;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return `https://${endpoint}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Returns a URL object whose pathname ends with `/`.
|
|
41
|
-
*
|
|
42
|
-
* @private internal helper for DigitalOceanSpaces.
|
|
43
|
-
*/
|
|
44
|
-
function ensureTrailingSlashUrl(url: URL): URL {
|
|
45
|
-
const normalizedUrl = new URL(url.href);
|
|
46
|
-
|
|
47
|
-
if (!normalizedUrl.pathname.endsWith('/')) {
|
|
48
|
-
normalizedUrl.pathname = `${normalizedUrl.pathname}/`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return normalizedUrl;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
25
|
/**
|
|
55
26
|
* Class implementing digital ocean spaces.
|
|
56
27
|
*/
|
|
@@ -64,7 +35,7 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
64
35
|
public constructor(private readonly config: IDigitalOceanSpacesConfig) {
|
|
65
36
|
this.s3 = new S3Client({
|
|
66
37
|
region: config.region || 'auto',
|
|
67
|
-
endpoint:
|
|
38
|
+
endpoint: normalizeS3Endpoint(config.endpoint),
|
|
68
39
|
forcePathStyle: config.forcePathStyle,
|
|
69
40
|
credentials: {
|
|
70
41
|
accessKeyId: config.accessKeyId,
|
|
@@ -74,13 +45,13 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
74
45
|
}
|
|
75
46
|
|
|
76
47
|
public getItemUrl(key: string): URL {
|
|
77
|
-
return new URL(this.
|
|
48
|
+
return new URL(this.config.pathPrefix + '/' + key, this.cdnPublicUrl);
|
|
78
49
|
}
|
|
79
50
|
|
|
80
51
|
public async getItem(key: string): Promise<IFile | null> {
|
|
81
52
|
const parameters = {
|
|
82
53
|
Bucket: this.config.bucket,
|
|
83
|
-
Key: this.
|
|
54
|
+
Key: this.config.pathPrefix + '/' + key,
|
|
84
55
|
};
|
|
85
56
|
|
|
86
57
|
try {
|
|
@@ -136,12 +107,12 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
136
107
|
const uploadResult = await this.s3.send(
|
|
137
108
|
new PutObjectCommand({
|
|
138
109
|
Bucket: this.config.bucket,
|
|
139
|
-
Key: this.
|
|
110
|
+
Key: this.config.pathPrefix + '/' + key,
|
|
140
111
|
ContentType: processedFile.type,
|
|
141
112
|
...putObjectRequestAdditional,
|
|
142
113
|
Body: processedFile.data,
|
|
143
114
|
// TODO: Public read access / just private to extending class
|
|
144
|
-
|
|
115
|
+
ACL: 'public-read',
|
|
145
116
|
}),
|
|
146
117
|
);
|
|
147
118
|
|
|
@@ -149,22 +120,19 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
149
120
|
throw new Error(`Upload result does not contain ETag`);
|
|
150
121
|
}
|
|
151
122
|
}
|
|
123
|
+
}
|
|
152
124
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!normalizedPathPrefix) {
|
|
163
|
-
return normalizedKey;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return `${normalizedPathPrefix}/${normalizedKey}`;
|
|
125
|
+
/**
|
|
126
|
+
* Normalizes endpoint values from legacy host-only configuration and full S3 URLs.
|
|
127
|
+
*
|
|
128
|
+
* @private helper of `DigitalOceanSpaces`
|
|
129
|
+
*/
|
|
130
|
+
function normalizeS3Endpoint(endpoint: string): string {
|
|
131
|
+
if (/^https?:\/\//i.test(endpoint)) {
|
|
132
|
+
return endpoint;
|
|
167
133
|
}
|
|
134
|
+
|
|
135
|
+
return `https://${endpoint}`;
|
|
168
136
|
}
|
|
169
137
|
|
|
170
138
|
// TODO: Implement Read-only mode
|
|
@@ -37,17 +37,20 @@ export class TrackedFilesStorage implements IIFilesStorageWithCdn {
|
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
39
|
const { userId, purpose } = file;
|
|
40
|
-
const
|
|
40
|
+
const cdnUrl = this.getItemUrl(key).href;
|
|
41
|
+
const securityResult =
|
|
42
|
+
file.securityResult as AgentsServerDatabase['public']['Tables']['File']['Insert']['securityResult'];
|
|
41
43
|
|
|
42
44
|
await this.supabase.from(await $getTableName('File')).insert({
|
|
43
45
|
userId: userId || null,
|
|
44
46
|
fileName: key,
|
|
45
47
|
fileSize: file.fileSize ?? file.data.length,
|
|
46
48
|
fileType: file.type,
|
|
47
|
-
storageUrl,
|
|
49
|
+
storageUrl: cdnUrl,
|
|
48
50
|
shortUrl: null,
|
|
49
51
|
purpose: purpose || 'UNKNOWN',
|
|
50
52
|
status: 'COMPLETED',
|
|
53
|
+
securityResult: securityResult || null,
|
|
51
54
|
});
|
|
52
55
|
} catch (error) {
|
|
53
56
|
console.error('Failed to track upload:', error);
|
|
@@ -25,6 +25,11 @@ export type IFile = {
|
|
|
25
25
|
* Note: This is optional, if not provided, the size of the buffer is used
|
|
26
26
|
*/
|
|
27
27
|
fileSize?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Optional file security result collected before the file is tracked.
|
|
31
|
+
*/
|
|
32
|
+
securityResult?: Record<string, unknown>;
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -2,7 +2,7 @@ import { $getTableName } from '@/src/database/$getTableName';
|
|
|
2
2
|
import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
|
|
3
3
|
import type { Json } from '@/src/database/schema';
|
|
4
4
|
import { FILE_SECURITY_CHECKERS } from '@/src/file-security-checkers';
|
|
5
|
-
import { $
|
|
5
|
+
import { $provideCdnForServer } from '@/src/tools/$provideCdnForServer';
|
|
6
6
|
import { $provideServer } from '@/src/tools/$provideServer';
|
|
7
7
|
import { getUserFileCdnKey } from '@/src/utils/cdn/utils/getUserFileCdnKey';
|
|
8
8
|
import { validateMimeType } from '@/src/utils/validators/validateMimeType';
|
|
@@ -231,12 +231,14 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
231
231
|
|
|
232
232
|
const mimeType = resolveShareTargetMimeType(file.type);
|
|
233
233
|
const blobPath = getUserFileCdnKey(buffer, normalizedFilename);
|
|
234
|
-
const cdn = $
|
|
234
|
+
const cdn = $provideCdnForServer();
|
|
235
|
+
const storageUrl = cdn.getItemUrl(blobPath).href;
|
|
236
|
+
|
|
235
237
|
await cdn.setItem(blobPath, {
|
|
236
|
-
data: buffer,
|
|
237
238
|
type: mimeType,
|
|
238
|
-
|
|
239
|
+
data: buffer,
|
|
239
240
|
purpose: SHARE_TARGET_FILE_PURPOSE,
|
|
241
|
+
fileSize: buffer.byteLength,
|
|
240
242
|
}).catch((error) => {
|
|
241
243
|
throw new DatabaseError(
|
|
242
244
|
spaceTrim(`
|
|
@@ -246,24 +248,14 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
246
248
|
`),
|
|
247
249
|
);
|
|
248
250
|
});
|
|
249
|
-
const storageUrl = cdn.getItemUrl(blobPath).href;
|
|
250
|
-
const fileRecordId = await insertShareTargetFileRecord({
|
|
251
|
-
fileName: normalizedFilename,
|
|
252
|
-
fileSize: buffer.byteLength,
|
|
253
|
-
fileType: mimeType,
|
|
254
|
-
storageUrl,
|
|
255
|
-
});
|
|
256
251
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}),
|
|
265
|
-
);
|
|
266
|
-
}
|
|
252
|
+
after(() =>
|
|
253
|
+
populateShareTargetFileSecurityResult({
|
|
254
|
+
storageUrl,
|
|
255
|
+
}).catch((error) => {
|
|
256
|
+
console.error('[share-target] Failed to finalize file security result', error);
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
267
259
|
|
|
268
260
|
return {
|
|
269
261
|
name: normalizedFilename,
|
|
@@ -272,50 +264,10 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
272
264
|
};
|
|
273
265
|
}
|
|
274
266
|
|
|
275
|
-
/**
|
|
276
|
-
* Inserts one admin-visible file row for a shared attachment.
|
|
277
|
-
*/
|
|
278
|
-
async function insertShareTargetFileRecord(options: {
|
|
279
|
-
fileName: string;
|
|
280
|
-
fileSize: number;
|
|
281
|
-
fileType: string;
|
|
282
|
-
storageUrl: string;
|
|
283
|
-
}): Promise<number | null> {
|
|
284
|
-
try {
|
|
285
|
-
const supabase = $provideSupabaseForServer() as TODO_any;
|
|
286
|
-
const fileTableName = await $getTableName('File');
|
|
287
|
-
const { data, error } = await supabase
|
|
288
|
-
.from(fileTableName)
|
|
289
|
-
.insert({
|
|
290
|
-
fileName: options.fileName,
|
|
291
|
-
fileSize: options.fileSize,
|
|
292
|
-
fileType: options.fileType,
|
|
293
|
-
storageUrl: options.storageUrl,
|
|
294
|
-
shortUrl: null,
|
|
295
|
-
purpose: SHARE_TARGET_FILE_PURPOSE,
|
|
296
|
-
status: 'COMPLETED',
|
|
297
|
-
agentId: null,
|
|
298
|
-
securityResult: null,
|
|
299
|
-
})
|
|
300
|
-
.select('id')
|
|
301
|
-
.maybeSingle();
|
|
302
|
-
|
|
303
|
-
if (error) {
|
|
304
|
-
console.error('[share-target] Failed to store uploaded file metadata', error);
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return typeof data?.id === 'number' ? data.id : null;
|
|
309
|
-
} catch (error) {
|
|
310
|
-
console.error('[share-target] Failed to insert shared file metadata', error);
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
267
|
/**
|
|
316
268
|
* Populates best-effort file-security results after the share redirect has already returned.
|
|
317
269
|
*/
|
|
318
|
-
async function populateShareTargetFileSecurityResult(options: {
|
|
270
|
+
async function populateShareTargetFileSecurityResult(options: { storageUrl: string }): Promise<void> {
|
|
319
271
|
const securityResult: Record<string, unknown> = {};
|
|
320
272
|
|
|
321
273
|
for (const checkerId of Object.keys(FILE_SECURITY_CHECKERS)) {
|
|
@@ -343,7 +295,7 @@ async function populateShareTargetFileSecurityResult(options: { fileId: number;
|
|
|
343
295
|
.update({
|
|
344
296
|
securityResult,
|
|
345
297
|
})
|
|
346
|
-
.eq('
|
|
298
|
+
.eq('storageUrl', options.storageUrl);
|
|
347
299
|
|
|
348
300
|
if (error) {
|
|
349
301
|
throw new DatabaseError(
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import type { number_percent, string_knowledge_source_content } from '@promptbook-local/types';
|
|
5
|
-
import { isServerRoutedCdnUploadProvider, resolveCdnStorageProvider } from '../cdn/resolveCdnStorageProvider';
|
|
3
|
+
import type { string_knowledge_source_content } from '@promptbook-local/types';
|
|
6
4
|
import { getSafeCdnPath } from '../cdn/utils/getSafeCdnPath';
|
|
7
5
|
import { normalizeUploadFilename } from '../normalization/normalizeUploadFilename';
|
|
6
|
+
import {
|
|
7
|
+
buildDefaultUserFileUploadPath,
|
|
8
|
+
uploadFileToServer,
|
|
9
|
+
type FileUploadProgressCallback,
|
|
10
|
+
} from './uploadFileToServer';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Progress callback invoked during file uploads.
|
|
11
14
|
*
|
|
12
15
|
* @private used by chat and book editors.
|
|
13
16
|
*/
|
|
14
|
-
export type FileUploadProgressCallback
|
|
15
|
-
progress: number_percent,
|
|
16
|
-
stats?: {
|
|
17
|
-
loadedBytes: number;
|
|
18
|
-
totalBytes: number;
|
|
19
|
-
},
|
|
20
|
-
) => void;
|
|
17
|
+
export type { FileUploadProgressCallback };
|
|
21
18
|
|
|
22
19
|
/**
|
|
23
20
|
* Optional upload configuration such as progress reporting or cancellation.
|
|
@@ -56,8 +53,8 @@ type UploadPathBuilder = (normalizedFilename: string, pathPrefix: string) => str
|
|
|
56
53
|
/**
|
|
57
54
|
* Constant for build default user file path.
|
|
58
55
|
*/
|
|
59
|
-
const buildDefaultUserFilePath: UploadPathBuilder = (normalizedFilename
|
|
60
|
-
|
|
56
|
+
const buildDefaultUserFilePath: UploadPathBuilder = (normalizedFilename) =>
|
|
57
|
+
buildDefaultUserFileUploadPath(normalizedFilename);
|
|
61
58
|
|
|
62
59
|
/**
|
|
63
60
|
* Configuration of the shared upload handler.
|
|
@@ -76,109 +73,6 @@ type SharedUploadHandlerConfig = {
|
|
|
76
73
|
*/
|
|
77
74
|
const DEFAULT_SHORT_URL_PREFIX = 'https://ptbk.io/k/';
|
|
78
75
|
|
|
79
|
-
/**
|
|
80
|
-
* Response returned by the server-routed upload API.
|
|
81
|
-
*
|
|
82
|
-
* @private used by chat and book editors.
|
|
83
|
-
*/
|
|
84
|
-
type ServerRoutedUploadResponse = {
|
|
85
|
-
url: string;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Uploads a file through `/api/upload` for S3-compatible storage providers.
|
|
90
|
-
*
|
|
91
|
-
* @private used by chat and book editors.
|
|
92
|
-
*/
|
|
93
|
-
function uploadFileThroughServer(options: {
|
|
94
|
-
safeUploadPath: string;
|
|
95
|
-
file: File;
|
|
96
|
-
purpose: string;
|
|
97
|
-
abortSignal?: AbortSignal;
|
|
98
|
-
onProgress?: FileUploadProgressCallback;
|
|
99
|
-
}): Promise<ServerRoutedUploadResponse> {
|
|
100
|
-
const { safeUploadPath, file, purpose, abortSignal, onProgress } = options;
|
|
101
|
-
const formData = new FormData();
|
|
102
|
-
formData.set('file', file);
|
|
103
|
-
formData.set('pathname', safeUploadPath);
|
|
104
|
-
formData.set('purpose', purpose);
|
|
105
|
-
formData.set('contentType', file.type || 'application/octet-stream');
|
|
106
|
-
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
const request = new XMLHttpRequest();
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Removes abort listeners once the upload finishes.
|
|
112
|
-
*/
|
|
113
|
-
const cleanup = () => {
|
|
114
|
-
abortSignal?.removeEventListener('abort', handleAbort);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Aborts the active XHR upload when the caller aborts.
|
|
119
|
-
*/
|
|
120
|
-
const handleAbort = () => {
|
|
121
|
-
request.abort();
|
|
122
|
-
cleanup();
|
|
123
|
-
reject(new DOMException('Upload aborted.', 'AbortError'));
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
request.upload.addEventListener('progress', (event) => {
|
|
127
|
-
if (!event.lengthComputable) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
onProgress?.((event.loaded / event.total) as number_percent, {
|
|
132
|
-
loadedBytes: event.loaded,
|
|
133
|
-
totalBytes: event.total,
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
request.addEventListener('load', () => {
|
|
138
|
-
cleanup();
|
|
139
|
-
|
|
140
|
-
if (request.status < 200 || request.status >= 300) {
|
|
141
|
-
reject(new Error(`Upload failed with HTTP ${request.status}: ${request.responseText}`));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const parsedResponse = JSON.parse(request.responseText) as Partial<ServerRoutedUploadResponse>;
|
|
147
|
-
if (typeof parsedResponse.url !== 'string' || parsedResponse.url.trim() === '') {
|
|
148
|
-
reject(new Error('Upload response did not contain a file URL.'));
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
onProgress?.(1 as number_percent, {
|
|
153
|
-
loadedBytes: file.size,
|
|
154
|
-
totalBytes: file.size,
|
|
155
|
-
});
|
|
156
|
-
resolve({ url: parsedResponse.url });
|
|
157
|
-
} catch (error) {
|
|
158
|
-
reject(error);
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
request.addEventListener('error', () => {
|
|
163
|
-
cleanup();
|
|
164
|
-
reject(new Error('Upload failed before the server responded.'));
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
request.addEventListener('abort', () => {
|
|
168
|
-
cleanup();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (abortSignal?.aborted) {
|
|
172
|
-
handleAbort();
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
abortSignal?.addEventListener('abort', handleAbort);
|
|
177
|
-
request.open('POST', '/api/upload');
|
|
178
|
-
request.send(formData);
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
76
|
/**
|
|
183
77
|
* Upload handler that normalizes the filename, uploads via `/api/upload`, and optionally returns a short URL.
|
|
184
78
|
*
|
|
@@ -196,50 +90,29 @@ export function createFileUploadHandler<ReturnType extends string = string>(
|
|
|
196
90
|
|
|
197
91
|
return async (file, optionsOrOnProgress) => {
|
|
198
92
|
const { onProgress, abortSignal } = normalizeUploadOptions(optionsOrOnProgress);
|
|
199
|
-
const provider = resolveCdnStorageProvider();
|
|
200
|
-
const isServerRoutedUploadEnabled = isServerRoutedCdnUploadProvider(provider);
|
|
201
93
|
const pathPrefix = process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '';
|
|
202
94
|
const normalizedFilename = normalizeUploadFilename(file.name);
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
95
|
+
const uploadPath = pathBuilder(normalizedFilename, pathPrefix);
|
|
96
|
+
const safeUploadPath = getSafeCdnPath({ pathname: uploadPath, pathPrefix });
|
|
97
|
+
|
|
98
|
+
const uploadResult = await uploadFileToServer({
|
|
99
|
+
file,
|
|
100
|
+
pathname: safeUploadPath,
|
|
101
|
+
purpose,
|
|
102
|
+
contentType: file.type || 'application/octet-stream',
|
|
103
|
+
abortSignal,
|
|
104
|
+
onProgress,
|
|
208
105
|
});
|
|
209
106
|
|
|
210
|
-
const blob = isServerRoutedUploadEnabled
|
|
211
|
-
? await uploadFileThroughServer({
|
|
212
|
-
safeUploadPath,
|
|
213
|
-
file,
|
|
214
|
-
purpose,
|
|
215
|
-
abortSignal,
|
|
216
|
-
onProgress,
|
|
217
|
-
})
|
|
218
|
-
: await upload(safeUploadPath, file, {
|
|
219
|
-
access: 'public',
|
|
220
|
-
handleUploadUrl: '/api/upload',
|
|
221
|
-
clientPayload: JSON.stringify({
|
|
222
|
-
purpose,
|
|
223
|
-
contentType: file.type || 'application/octet-stream',
|
|
224
|
-
}),
|
|
225
|
-
abortSignal,
|
|
226
|
-
onUploadProgress: (progressEvent) => {
|
|
227
|
-
onProgress?.(progressEvent.percentage / 100, {
|
|
228
|
-
loadedBytes: progressEvent.loaded,
|
|
229
|
-
totalBytes: progressEvent.total,
|
|
230
|
-
});
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
|
|
234
107
|
if (returnShortUrl && process.env.NEXT_PUBLIC_CDN_PUBLIC_URL) {
|
|
235
|
-
const slashIndex =
|
|
236
|
-
const directoryPath = slashIndex === -1 ? '' : `${
|
|
108
|
+
const slashIndex = uploadPath.lastIndexOf('/');
|
|
109
|
+
const directoryPath = slashIndex === -1 ? '' : `${uploadPath.slice(0, slashIndex + 1)}`;
|
|
237
110
|
const longUrlPrefix = `${process.env.NEXT_PUBLIC_CDN_PUBLIC_URL}/${directoryPath}`;
|
|
238
|
-
const shortUrl =
|
|
111
|
+
const shortUrl = uploadResult.url.split(longUrlPrefix).join(shortUrlPrefix);
|
|
239
112
|
return shortUrl as ReturnType;
|
|
240
113
|
}
|
|
241
114
|
|
|
242
|
-
return
|
|
115
|
+
return uploadResult.url as ReturnType;
|
|
243
116
|
};
|
|
244
117
|
}
|
|
245
118
|
|