@kmlckj/licos-platform-sdk 0.1.0 → 0.2.0
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 +30 -1
- package/package.json +4 -2
- package/src/browser.js +101 -0
- package/src/index.d.ts +29 -4
- package/src/index.js +54 -113
- package/src/shared.js +256 -0
package/README.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
1
|
# @kmlckj/licos-platform-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
LICOS 平台对象存储 SDK,兼容前端和后端调用。
|
|
4
|
+
|
|
5
|
+
## 后端模式
|
|
6
|
+
|
|
7
|
+
- `baseUrl` / `token` / `userId` / `projectId` 不传时,会回退读取 `LICOS_*` 环境变量
|
|
8
|
+
- `uploadUser` / `uploadProject` 支持直接传本地文件路径
|
|
9
|
+
- `download(url, outputPath)` 会把文件写到本地路径
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { uploadProject, download } from '@kmlckj/licos-platform-sdk';
|
|
13
|
+
|
|
14
|
+
const uploaded = await uploadProject('/tmp/report.pdf');
|
|
15
|
+
await download(uploaded.download_url, '/tmp/report-downloaded.pdf');
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 前端模式
|
|
19
|
+
|
|
20
|
+
- 默认走同源相对地址;也可以显式传 `baseUrl`
|
|
21
|
+
- 上传支持 `File` / `Blob` / `ArrayBuffer` / TypedArray / `{ data, fileName, contentType }`
|
|
22
|
+
- `download(url)` 返回 `Blob`
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { uploadUser, download } from '@kmlckj/licos-platform-sdk';
|
|
26
|
+
|
|
27
|
+
const uploaded = await uploadUser(fileInput.files[0], {
|
|
28
|
+
token: userJwt,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const blob = await download(uploaded.download_url);
|
|
32
|
+
```
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kmlckj/licos-platform-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "LICOS platform SDK for storage upload, download, and preview",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "LICOS platform SDK for storage upload, download, and preview in browser and Node runtimes",
|
|
5
5
|
"author": "kmlckj",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.js",
|
|
8
|
+
"browser": "./src/browser.js",
|
|
8
9
|
"types": "./src/index.d.ts",
|
|
9
10
|
"exports": {
|
|
10
11
|
".": {
|
|
11
12
|
"types": "./src/index.d.ts",
|
|
13
|
+
"browser": "./src/browser.js",
|
|
12
14
|
"import": "./src/index.js",
|
|
13
15
|
"default": "./src/index.js"
|
|
14
16
|
}
|
package/src/browser.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiError,
|
|
3
|
+
ConfigurationError,
|
|
4
|
+
PlatformSdkError,
|
|
5
|
+
downloadBlob,
|
|
6
|
+
encodeObjectKey,
|
|
7
|
+
encodeStorageSegment,
|
|
8
|
+
normalizeBaseUrl,
|
|
9
|
+
resolveProjectId,
|
|
10
|
+
resolveUserId,
|
|
11
|
+
uploadFile,
|
|
12
|
+
} from './shared.js';
|
|
13
|
+
|
|
14
|
+
export class StorageClient {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.baseUrl = options.baseUrl;
|
|
17
|
+
this.token = options.token;
|
|
18
|
+
this.userId = options.userId;
|
|
19
|
+
this.projectId = options.projectId;
|
|
20
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
21
|
+
this.headers = options.headers;
|
|
22
|
+
this.credentials = options.credentials;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
previewUrl(bucket, objectKey) {
|
|
26
|
+
const baseUrl = normalizeBaseUrl(this.baseUrl);
|
|
27
|
+
return `${baseUrl}/api/v1/public/storage/${encodeStorageSegment(bucket)}/${encodeObjectKey(objectKey)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
downloadUrl(bucket, objectKey) {
|
|
31
|
+
return `${this.previewUrl(bucket, objectKey)}?download=1`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async uploadUser(file) {
|
|
35
|
+
const baseUrl = normalizeBaseUrl(this.baseUrl);
|
|
36
|
+
return uploadFile(this.fetchImpl, `${baseUrl}/api/v1/storage/upload/user`, file, {
|
|
37
|
+
token: this.token,
|
|
38
|
+
headers: this.headers,
|
|
39
|
+
credentials: this.credentials,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async uploadProject(file, options = {}) {
|
|
44
|
+
const baseUrl = normalizeBaseUrl(this.baseUrl);
|
|
45
|
+
const userId = resolveUserId(options.userId ?? this.userId);
|
|
46
|
+
const projectId = resolveProjectId(options.projectId ?? this.projectId);
|
|
47
|
+
return uploadFile(
|
|
48
|
+
this.fetchImpl,
|
|
49
|
+
`${baseUrl}/api/v1/storage/upload/project/${encodeStorageSegment(userId)}/${encodeStorageSegment(projectId)}`,
|
|
50
|
+
file,
|
|
51
|
+
{
|
|
52
|
+
token: this.token,
|
|
53
|
+
headers: this.headers,
|
|
54
|
+
credentials: this.credentials,
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async download(url, outputPath) {
|
|
60
|
+
if (outputPath !== undefined) {
|
|
61
|
+
throw new ConfigurationError(
|
|
62
|
+
'Front-end mode does not support downloading directly to a filesystem path. Omit outputPath to receive a Blob.',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return downloadBlob(this.fetchImpl, url, {
|
|
66
|
+
headers: this.headers,
|
|
67
|
+
credentials: this.credentials,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function defaultClient(options = {}) {
|
|
73
|
+
return new StorageClient(options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function previewUrl(bucket, objectKey, options = {}) {
|
|
77
|
+
return defaultClient(options).previewUrl(bucket, objectKey);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function downloadUrl(bucket, objectKey, options = {}) {
|
|
81
|
+
return defaultClient(options).downloadUrl(bucket, objectKey);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function uploadUser(file, options = {}) {
|
|
85
|
+
return defaultClient(options).uploadUser(file);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function uploadProject(file, options = {}) {
|
|
89
|
+
return defaultClient(options).uploadProject(file, options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function download(url, outputPathOrOptions, maybeOptions = {}) {
|
|
93
|
+
if (typeof outputPathOrOptions === 'string') {
|
|
94
|
+
throw new ConfigurationError(
|
|
95
|
+
'Front-end mode does not support downloading directly to a filesystem path. Omit outputPath to receive a Blob.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return defaultClient(outputPathOrOptions ?? maybeOptions).download(url);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { PlatformSdkError, ConfigurationError, ApiError };
|
package/src/index.d.ts
CHANGED
|
@@ -8,12 +8,35 @@ export interface UploadResult {
|
|
|
8
8
|
download_url: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export interface BlobLike {
|
|
12
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
13
|
+
readonly size?: number;
|
|
14
|
+
readonly type?: string;
|
|
15
|
+
readonly name?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UploadBinaryDescriptor {
|
|
19
|
+
data?: ArrayBuffer | ArrayBufferView | BlobLike;
|
|
20
|
+
file?: ArrayBuffer | ArrayBufferView | BlobLike;
|
|
21
|
+
fileName?: string;
|
|
22
|
+
contentType?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type UploadSource =
|
|
26
|
+
| string
|
|
27
|
+
| ArrayBuffer
|
|
28
|
+
| ArrayBufferView
|
|
29
|
+
| BlobLike
|
|
30
|
+
| UploadBinaryDescriptor;
|
|
31
|
+
|
|
11
32
|
export interface StorageClientOptions {
|
|
12
33
|
baseUrl?: string;
|
|
13
34
|
token?: string;
|
|
14
35
|
userId?: string;
|
|
15
36
|
projectId?: string;
|
|
16
37
|
fetch?: typeof fetch;
|
|
38
|
+
headers?: HeadersInit;
|
|
39
|
+
credentials?: RequestCredentials;
|
|
17
40
|
}
|
|
18
41
|
|
|
19
42
|
export interface UploadProjectOptions extends StorageClientOptions {
|
|
@@ -34,8 +57,9 @@ export class StorageClient {
|
|
|
34
57
|
constructor(options?: StorageClientOptions);
|
|
35
58
|
previewUrl(bucket: string, objectKey: string): string;
|
|
36
59
|
downloadUrl(bucket: string, objectKey: string): string;
|
|
37
|
-
uploadUser(
|
|
38
|
-
uploadProject(
|
|
60
|
+
uploadUser(file: UploadSource): Promise<UploadResult>;
|
|
61
|
+
uploadProject(file: UploadSource, options?: UploadProjectOptions): Promise<UploadResult>;
|
|
62
|
+
download(url: string): Promise<Blob>;
|
|
39
63
|
download(url: string, outputPath: string): Promise<string>;
|
|
40
64
|
}
|
|
41
65
|
|
|
@@ -50,13 +74,14 @@ export function downloadUrl(
|
|
|
50
74
|
options?: StorageClientOptions,
|
|
51
75
|
): string;
|
|
52
76
|
export function uploadUser(
|
|
53
|
-
|
|
77
|
+
file: UploadSource,
|
|
54
78
|
options?: StorageClientOptions,
|
|
55
79
|
): Promise<UploadResult>;
|
|
56
80
|
export function uploadProject(
|
|
57
|
-
|
|
81
|
+
file: UploadSource,
|
|
58
82
|
options?: UploadProjectOptions,
|
|
59
83
|
): Promise<UploadResult>;
|
|
84
|
+
export function download(url: string, options?: StorageClientOptions): Promise<Blob>;
|
|
60
85
|
export function download(
|
|
61
86
|
url: string,
|
|
62
87
|
outputPath: string,
|
package/src/index.js
CHANGED
|
@@ -1,89 +1,20 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const value = process.env[name];
|
|
19
|
-
if (value) {
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
throw new ConfigurationError(`Missing required environment variable: ${name}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function normalizeBaseUrl(baseUrl) {
|
|
26
|
-
return (baseUrl ?? requireEnv('LICOS_API_BASE_URL')).replace(/\/+$/, '');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function resolveToken(token) {
|
|
30
|
-
return token ?? requireEnv('LICOS_USER_TOKEN');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resolveUserId(userId) {
|
|
34
|
-
return userId ?? requireEnv('LICOS_USER_ID');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resolveProjectId(projectId) {
|
|
38
|
-
return projectId ?? requireEnv('LICOS_PROJECT_ID');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function encodeStorageSegment(value) {
|
|
42
|
-
return encodeURIComponent(value);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function encodeObjectKey(value) {
|
|
46
|
-
return value.split('/').map(encodeStorageSegment).join('/');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function guessContentType(filePath) {
|
|
50
|
-
switch (path.extname(filePath).toLowerCase()) {
|
|
51
|
-
case '.txt':
|
|
52
|
-
return 'text/plain';
|
|
53
|
-
case '.json':
|
|
54
|
-
return 'application/json';
|
|
55
|
-
case '.pdf':
|
|
56
|
-
return 'application/pdf';
|
|
57
|
-
case '.png':
|
|
58
|
-
return 'image/png';
|
|
59
|
-
case '.jpg':
|
|
60
|
-
case '.jpeg':
|
|
61
|
-
return 'image/jpeg';
|
|
62
|
-
default:
|
|
63
|
-
return 'application/octet-stream';
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function readJsonEnvelope(response) {
|
|
68
|
-
let payload;
|
|
69
|
-
try {
|
|
70
|
-
payload = await response.json();
|
|
71
|
-
} catch (error) {
|
|
72
|
-
throw new ApiError('Platform API returned non-JSON response', {
|
|
73
|
-
status: response.status,
|
|
74
|
-
details: error instanceof Error ? error.message : String(error),
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!response.ok || payload?.code !== 0) {
|
|
79
|
-
throw new ApiError(payload?.message ?? `Platform API failed: HTTP ${response.status}`, {
|
|
80
|
-
status: response.status,
|
|
81
|
-
code: payload?.code,
|
|
82
|
-
details: payload?.data ?? payload,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
return payload.data;
|
|
86
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
ApiError,
|
|
6
|
+
ConfigurationError,
|
|
7
|
+
PlatformSdkError,
|
|
8
|
+
downloadBlob,
|
|
9
|
+
encodeObjectKey,
|
|
10
|
+
encodeStorageSegment,
|
|
11
|
+
fileNameFromPath,
|
|
12
|
+
guessContentType,
|
|
13
|
+
normalizeBaseUrl,
|
|
14
|
+
resolveProjectId,
|
|
15
|
+
resolveUserId,
|
|
16
|
+
uploadFile,
|
|
17
|
+
} from './shared.js';
|
|
87
18
|
|
|
88
19
|
export class StorageClient {
|
|
89
20
|
constructor(options = {}) {
|
|
@@ -92,6 +23,8 @@ export class StorageClient {
|
|
|
92
23
|
this.userId = options.userId;
|
|
93
24
|
this.projectId = options.projectId;
|
|
94
25
|
this.fetchImpl = options.fetch ?? fetch;
|
|
26
|
+
this.headers = options.headers;
|
|
27
|
+
this.credentials = options.credentials;
|
|
95
28
|
}
|
|
96
29
|
|
|
97
30
|
previewUrl(bucket, objectKey) {
|
|
@@ -103,53 +36,56 @@ export class StorageClient {
|
|
|
103
36
|
return `${this.previewUrl(bucket, objectKey)}?download=1`;
|
|
104
37
|
}
|
|
105
38
|
|
|
106
|
-
async uploadUser(
|
|
39
|
+
async uploadUser(file) {
|
|
107
40
|
const baseUrl = normalizeBaseUrl(this.baseUrl);
|
|
108
|
-
return this.#upload(`${baseUrl}/api/v1/storage/upload/user`,
|
|
41
|
+
return this.#upload(`${baseUrl}/api/v1/storage/upload/user`, file);
|
|
109
42
|
}
|
|
110
43
|
|
|
111
|
-
async uploadProject(
|
|
44
|
+
async uploadProject(file, options = {}) {
|
|
112
45
|
const baseUrl = normalizeBaseUrl(this.baseUrl);
|
|
113
46
|
const userId = resolveUserId(options.userId ?? this.userId);
|
|
114
47
|
const projectId = resolveProjectId(options.projectId ?? this.projectId);
|
|
115
48
|
return this.#upload(
|
|
116
49
|
`${baseUrl}/api/v1/storage/upload/project/${encodeStorageSegment(userId)}/${encodeStorageSegment(projectId)}`,
|
|
117
|
-
|
|
50
|
+
file,
|
|
118
51
|
);
|
|
119
52
|
}
|
|
120
53
|
|
|
121
54
|
async download(url, outputPath) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
55
|
+
const blob = await downloadBlob(this.fetchImpl, url, {
|
|
56
|
+
headers: this.headers,
|
|
57
|
+
credentials: this.credentials,
|
|
58
|
+
});
|
|
59
|
+
if (outputPath === undefined) {
|
|
60
|
+
return blob;
|
|
127
61
|
}
|
|
128
|
-
const buffer = Buffer.from(await
|
|
62
|
+
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
129
63
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
130
64
|
await fs.writeFile(outputPath, buffer);
|
|
131
65
|
return outputPath;
|
|
132
66
|
}
|
|
133
67
|
|
|
134
|
-
async #upload(url,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
const response = await this.fetchImpl(url, {
|
|
146
|
-
method: 'POST',
|
|
147
|
-
headers: {
|
|
148
|
-
Authorization: `Bearer ${token}`,
|
|
68
|
+
async #upload(url, file) {
|
|
69
|
+
return uploadFile(
|
|
70
|
+
this.fetchImpl,
|
|
71
|
+
url,
|
|
72
|
+
file,
|
|
73
|
+
{
|
|
74
|
+
token: this.token,
|
|
75
|
+
headers: this.headers,
|
|
76
|
+
credentials: this.credentials,
|
|
149
77
|
},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
78
|
+
async (filePath) => {
|
|
79
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
80
|
+
const fileName = fileNameFromPath(filePath);
|
|
81
|
+
const contentType = guessContentType(fileName);
|
|
82
|
+
return {
|
|
83
|
+
fileName,
|
|
84
|
+
contentType,
|
|
85
|
+
blob: new Blob([fileBuffer], { type: contentType }),
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
);
|
|
153
89
|
}
|
|
154
90
|
}
|
|
155
91
|
|
|
@@ -173,6 +109,11 @@ export async function uploadProject(filePath, options = {}) {
|
|
|
173
109
|
return defaultClient(options).uploadProject(filePath, options);
|
|
174
110
|
}
|
|
175
111
|
|
|
176
|
-
export async function download(url,
|
|
177
|
-
|
|
112
|
+
export async function download(url, outputPathOrOptions, maybeOptions = {}) {
|
|
113
|
+
if (typeof outputPathOrOptions === 'string') {
|
|
114
|
+
return defaultClient(maybeOptions).download(url, outputPathOrOptions);
|
|
115
|
+
}
|
|
116
|
+
return defaultClient(outputPathOrOptions ?? {}).download(url);
|
|
178
117
|
}
|
|
118
|
+
|
|
119
|
+
export { PlatformSdkError, ConfigurationError, ApiError };
|
package/src/shared.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
export class PlatformSdkError extends Error {
|
|
2
|
+
constructor(message, options = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = new.target.name;
|
|
5
|
+
this.status = options.status;
|
|
6
|
+
this.code = options.code;
|
|
7
|
+
this.details = options.details;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ConfigurationError extends PlatformSdkError {}
|
|
12
|
+
export class ApiError extends PlatformSdkError {}
|
|
13
|
+
|
|
14
|
+
export function isNodeRuntime() {
|
|
15
|
+
return typeof process !== 'undefined' && !!process?.versions?.node;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readEnv(name) {
|
|
19
|
+
if (typeof process === 'undefined' || !process?.env) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const value = process.env[name];
|
|
23
|
+
return value ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function requireEnv(name) {
|
|
27
|
+
const value = readEnv(name);
|
|
28
|
+
if (value) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
throw new ConfigurationError(`Missing required environment variable: ${name}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeBaseUrl(baseUrl) {
|
|
35
|
+
if (typeof baseUrl === 'string') {
|
|
36
|
+
return baseUrl.replace(/\/+$/, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const envBaseUrl = readEnv('LICOS_API_BASE_URL');
|
|
40
|
+
if (envBaseUrl) {
|
|
41
|
+
return envBaseUrl.replace(/\/+$/, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!isNodeRuntime()) {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new ConfigurationError('Missing required environment variable: LICOS_API_BASE_URL');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveUserId(userId) {
|
|
52
|
+
return userId ?? requireEnv('LICOS_USER_ID');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveProjectId(projectId) {
|
|
56
|
+
return projectId ?? requireEnv('LICOS_PROJECT_ID');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function encodeStorageSegment(value) {
|
|
60
|
+
return encodeURIComponent(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function encodeObjectKey(value) {
|
|
64
|
+
return value.split('/').map(encodeStorageSegment).join('/');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function fileNameFromPath(filePath) {
|
|
68
|
+
const parts = String(filePath).split(/[\\/]/);
|
|
69
|
+
const fileName = parts[parts.length - 1];
|
|
70
|
+
return fileName || 'upload.bin';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function guessContentType(fileName) {
|
|
74
|
+
const ext = /\.[^./\\]+$/.exec(String(fileName).toLowerCase())?.[0] ?? '';
|
|
75
|
+
switch (ext) {
|
|
76
|
+
case '.txt':
|
|
77
|
+
return 'text/plain';
|
|
78
|
+
case '.json':
|
|
79
|
+
return 'application/json';
|
|
80
|
+
case '.pdf':
|
|
81
|
+
return 'application/pdf';
|
|
82
|
+
case '.png':
|
|
83
|
+
return 'image/png';
|
|
84
|
+
case '.jpg':
|
|
85
|
+
case '.jpeg':
|
|
86
|
+
return 'image/jpeg';
|
|
87
|
+
case '.gif':
|
|
88
|
+
return 'image/gif';
|
|
89
|
+
case '.webp':
|
|
90
|
+
return 'image/webp';
|
|
91
|
+
case '.svg':
|
|
92
|
+
return 'image/svg+xml';
|
|
93
|
+
case '.md':
|
|
94
|
+
return 'text/markdown';
|
|
95
|
+
default:
|
|
96
|
+
return 'application/octet-stream';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isBlobLike(value) {
|
|
101
|
+
return !!value && typeof value === 'object' && typeof value.arrayBuffer === 'function';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isBinaryDescriptor(value) {
|
|
105
|
+
if (!value || typeof value !== 'object' || isBlobLike(value)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return 'data' in value || 'file' in value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function toUint8Array(data) {
|
|
112
|
+
if (data instanceof Uint8Array) {
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
if (ArrayBuffer.isView(data)) {
|
|
116
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
117
|
+
}
|
|
118
|
+
return new Uint8Array(data);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function normalizeBinaryPayload(data, overrides = {}) {
|
|
122
|
+
if (isBlobLike(data)) {
|
|
123
|
+
const fileName =
|
|
124
|
+
overrides.fileName ??
|
|
125
|
+
(typeof data.name === 'string' && data.name ? data.name : undefined) ??
|
|
126
|
+
'upload.bin';
|
|
127
|
+
const contentType =
|
|
128
|
+
overrides.contentType ??
|
|
129
|
+
(typeof data.type === 'string' && data.type ? data.type : undefined) ??
|
|
130
|
+
guessContentType(fileName);
|
|
131
|
+
return {
|
|
132
|
+
fileName,
|
|
133
|
+
contentType,
|
|
134
|
+
blob: new Blob([await data.arrayBuffer()], { type: contentType }),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
139
|
+
const fileName = overrides.fileName ?? 'upload.bin';
|
|
140
|
+
const contentType = overrides.contentType ?? guessContentType(fileName);
|
|
141
|
+
return {
|
|
142
|
+
fileName,
|
|
143
|
+
contentType,
|
|
144
|
+
blob: new Blob([toUint8Array(data)], { type: contentType }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new ConfigurationError(
|
|
149
|
+
'Unsupported upload input. Use a file path string, Blob/File, ArrayBuffer, TypedArray, or { data, fileName }.',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function normalizeUploadSource(source, readPathSource) {
|
|
154
|
+
if (typeof source === 'string') {
|
|
155
|
+
if (!readPathSource) {
|
|
156
|
+
throw new ConfigurationError(
|
|
157
|
+
'Front-end mode does not support filesystem path uploads. Pass File, Blob, ArrayBuffer, TypedArray, or { data, fileName } instead.',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return readPathSource(source);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (isBinaryDescriptor(source)) {
|
|
164
|
+
return normalizeBinaryPayload(source.data ?? source.file, {
|
|
165
|
+
fileName: source.fileName,
|
|
166
|
+
contentType: source.contentType,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return normalizeBinaryPayload(source);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function headersFromInit(headersInit) {
|
|
174
|
+
return new Headers(headersInit ?? undefined);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function buildRequestHeaders(options = {}, { authenticated = false } = {}) {
|
|
178
|
+
const headers = headersFromInit(options.headers);
|
|
179
|
+
if (authenticated && !headers.has('Authorization')) {
|
|
180
|
+
const resolvedToken = options.token ?? readEnv('LICOS_USER_TOKEN');
|
|
181
|
+
if (resolvedToken) {
|
|
182
|
+
headers.set('Authorization', `Bearer ${resolvedToken}`);
|
|
183
|
+
} else if (isNodeRuntime()) {
|
|
184
|
+
throw new ConfigurationError(
|
|
185
|
+
'Missing authentication token. Pass options.token, provide an Authorization header, or set LICOS_USER_TOKEN.',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return headers;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function buildRequestInit(options = {}, init = {}) {
|
|
193
|
+
const requestInit = { ...init };
|
|
194
|
+
if (options.credentials) {
|
|
195
|
+
requestInit.credentials = options.credentials;
|
|
196
|
+
}
|
|
197
|
+
return requestInit;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function readJsonEnvelope(response) {
|
|
201
|
+
let payload;
|
|
202
|
+
try {
|
|
203
|
+
payload = await response.json();
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new ApiError('Platform API returned non-JSON response', {
|
|
206
|
+
status: response.status,
|
|
207
|
+
details: error instanceof Error ? error.message : String(error),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!response.ok || payload?.code !== 0) {
|
|
212
|
+
throw new ApiError(payload?.message ?? `Platform API failed: HTTP ${response.status}`, {
|
|
213
|
+
status: response.status,
|
|
214
|
+
code: payload?.code,
|
|
215
|
+
details: payload?.data ?? payload,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return payload.data;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function downloadBlob(fetchImpl, url, options = {}) {
|
|
222
|
+
const response = await fetchImpl(
|
|
223
|
+
url,
|
|
224
|
+
buildRequestInit(options, {
|
|
225
|
+
method: 'GET',
|
|
226
|
+
headers: buildRequestHeaders(options),
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
throw new ApiError(`Platform download failed: HTTP ${response.status}`, {
|
|
231
|
+
status: response.status,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (typeof response.blob === 'function') {
|
|
235
|
+
return response.blob();
|
|
236
|
+
}
|
|
237
|
+
return new Blob([await response.arrayBuffer()], {
|
|
238
|
+
type: response.headers.get('content-type') ?? undefined,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function uploadFile(fetchImpl, url, source, options = {}, readPathSource) {
|
|
243
|
+
const normalized = await normalizeUploadSource(source, readPathSource);
|
|
244
|
+
const formData = new FormData();
|
|
245
|
+
formData.append('file', normalized.blob, normalized.fileName);
|
|
246
|
+
|
|
247
|
+
const response = await fetchImpl(
|
|
248
|
+
url,
|
|
249
|
+
buildRequestInit(options, {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: buildRequestHeaders(options, { authenticated: true }),
|
|
252
|
+
body: formData,
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
return readJsonEnvelope(response);
|
|
256
|
+
}
|