@dofe/file-sdk-web 0.1.1
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 +68 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +2 -0
- package/dist/uploader.d.ts +37 -0
- package/dist/uploader.js +182 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @dofe/file-sdk-web
|
|
2
|
+
|
|
3
|
+
DofeAI unified file uploader SDK for **browsers** — direct upload, multipart upload with progress, via `sso.dofe.ai`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @dofe/file-sdk-web
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { FileUploader } from '@dofe/file-sdk-web';
|
|
15
|
+
|
|
16
|
+
const uploader = new FileUploader({
|
|
17
|
+
apiBase: '/api/proxy/sso', // via Next.js proxy
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Small files: direct upload
|
|
21
|
+
const { fileId, cdnUrl } = await uploader.upload(file, {
|
|
22
|
+
scope: 'avatar',
|
|
23
|
+
onProgress: ({ percent }) => console.log(`${percent}%`),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Large files: multipart upload with concurrency
|
|
27
|
+
const { fileId, cdnUrl } = await uploader.uploadMultipart(bigFile, {
|
|
28
|
+
scope: 'media-asset',
|
|
29
|
+
partSize: 5 * 1024 * 1024, // 5MB parts
|
|
30
|
+
concurrency: 3, // 3 parallel parts
|
|
31
|
+
onProgress: ({ percent, loaded, total }) => {
|
|
32
|
+
console.log(`${percent}% (${loaded}/${total})`);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API Reference
|
|
38
|
+
|
|
39
|
+
### `new FileUploader(config)`
|
|
40
|
+
|
|
41
|
+
| Option | Type | Required | Default | Description |
|
|
42
|
+
|--------|------|:---:|---------|-------------|
|
|
43
|
+
| `apiBase` | `string` | ✅ | — | API base URL (e.g., `/api/proxy/sso` or `https://sso.dofe.ai`) |
|
|
44
|
+
| `timeout` | `number` | ❌ | `30000` | Request timeout in ms |
|
|
45
|
+
|
|
46
|
+
### Methods
|
|
47
|
+
|
|
48
|
+
| Method | Returns | Description |
|
|
49
|
+
|--------|---------|-------------|
|
|
50
|
+
| `upload(file, options)` | `Promise<UploadResult>` | Auto-chooses direct or multipart based on file size (>5MB → multipart) |
|
|
51
|
+
| `uploadDirect(file, options)` | `Promise<UploadResult>` | Direct upload for small files |
|
|
52
|
+
| `uploadMultipart(file, options)` | `Promise<UploadResult>` | Multipart upload with configurable part size and concurrency |
|
|
53
|
+
|
|
54
|
+
### UploadResult
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
interface UploadResult {
|
|
58
|
+
fileId: string; // SSO FileSource UUID
|
|
59
|
+
key: string; // Object storage key
|
|
60
|
+
url: string; // Upload URL / token
|
|
61
|
+
cdnUrl: string | null; // CDN URL (null for private files)
|
|
62
|
+
bucket: string; // Storage bucket name
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileUploader = void 0;
|
|
4
|
+
var uploader_1 = require("./uploader");
|
|
5
|
+
Object.defineProperty(exports, "FileUploader", { enumerable: true, get: function () { return uploader_1.FileUploader; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** File scope types */
|
|
2
|
+
export type FileScope = 'avatar' | 'media_asset' | 'knowledge' | 'general' | 'system';
|
|
3
|
+
/** Upload progress callback */
|
|
4
|
+
export interface UploadProgress {
|
|
5
|
+
/** Percentage complete (0-100) */
|
|
6
|
+
percent: number;
|
|
7
|
+
/** Bytes uploaded so far */
|
|
8
|
+
loaded: number;
|
|
9
|
+
/** Total bytes to upload */
|
|
10
|
+
total: number;
|
|
11
|
+
}
|
|
12
|
+
/** Upload options */
|
|
13
|
+
export interface UploadOptions {
|
|
14
|
+
/** File scope / category */
|
|
15
|
+
scope: FileScope;
|
|
16
|
+
/** Progress callback */
|
|
17
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
18
|
+
/** Tenant ID (optional, derived from auth context if not provided) */
|
|
19
|
+
tenantId?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Multipart upload options */
|
|
22
|
+
export interface MultipartUploadOptions extends UploadOptions {
|
|
23
|
+
/** Part size in bytes (default: 5MB) */
|
|
24
|
+
partSize?: number;
|
|
25
|
+
/** Max concurrent parts (default: 3) */
|
|
26
|
+
concurrency?: number;
|
|
27
|
+
}
|
|
28
|
+
/** Upload result */
|
|
29
|
+
export interface UploadResult {
|
|
30
|
+
fileId: string;
|
|
31
|
+
key: string;
|
|
32
|
+
url: string;
|
|
33
|
+
cdnUrl: string | null;
|
|
34
|
+
bucket: string;
|
|
35
|
+
}
|
|
36
|
+
/** FileUploader configuration */
|
|
37
|
+
export interface FileUploaderConfig {
|
|
38
|
+
/** API base URL for upload token requests (e.g., "/api/proxy/sso" or "https://sso.dofe.ai") */
|
|
39
|
+
apiBase: string;
|
|
40
|
+
/** Timeout per request in ms (default: 30000) */
|
|
41
|
+
timeout?: number;
|
|
42
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { FileUploaderConfig, UploadOptions, MultipartUploadOptions, UploadResult } from './types';
|
|
2
|
+
export declare class FileUploader {
|
|
3
|
+
private readonly apiBase;
|
|
4
|
+
private readonly timeout;
|
|
5
|
+
constructor(config: FileUploaderConfig);
|
|
6
|
+
/**
|
|
7
|
+
* Upload a file. Automatically chooses between direct upload (small files)
|
|
8
|
+
* and multipart upload (large files > 5MB).
|
|
9
|
+
*/
|
|
10
|
+
upload(file: File, options: UploadOptions): Promise<UploadResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Direct upload for small files.
|
|
13
|
+
* 1. Get upload token from SSO API
|
|
14
|
+
* 2. PUT file directly to presigned URL
|
|
15
|
+
*/
|
|
16
|
+
uploadDirect(file: File, options: UploadOptions): Promise<UploadResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Multipart upload for large files.
|
|
19
|
+
* 1. Init multipart upload → get uploadId + first part token
|
|
20
|
+
* 2. Upload first part
|
|
21
|
+
* 3. Get tokens for remaining parts and upload in parallel
|
|
22
|
+
* 4. Complete multipart upload
|
|
23
|
+
*/
|
|
24
|
+
uploadMultipart(file: File, options: MultipartUploadOptions): Promise<UploadResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Upload a single part in a multipart upload.
|
|
27
|
+
*/
|
|
28
|
+
private uploadPart;
|
|
29
|
+
/**
|
|
30
|
+
* PUT file/blob directly to a presigned URL.
|
|
31
|
+
*/
|
|
32
|
+
private uploadToPresignedUrl;
|
|
33
|
+
/**
|
|
34
|
+
* Fetch JSON from API, extracting the data field from the standard response wrapper.
|
|
35
|
+
*/
|
|
36
|
+
private fetchJson;
|
|
37
|
+
}
|
package/dist/uploader.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileUploader = void 0;
|
|
4
|
+
/** Default part size for multipart uploads: 5MB */
|
|
5
|
+
const DEFAULT_PART_SIZE = 5 * 1024 * 1024;
|
|
6
|
+
class FileUploader {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
var _a;
|
|
9
|
+
this.apiBase = config.apiBase.replace(/\/$/, '');
|
|
10
|
+
this.timeout = (_a = config.timeout) !== null && _a !== void 0 ? _a : 30000;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Upload a file. Automatically chooses between direct upload (small files)
|
|
14
|
+
* and multipart upload (large files > 5MB).
|
|
15
|
+
*/
|
|
16
|
+
async upload(file, options) {
|
|
17
|
+
if (file.size > DEFAULT_PART_SIZE) {
|
|
18
|
+
return this.uploadMultipart(file, options);
|
|
19
|
+
}
|
|
20
|
+
return this.uploadDirect(file, options);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Direct upload for small files.
|
|
24
|
+
* 1. Get upload token from SSO API
|
|
25
|
+
* 2. PUT file directly to presigned URL
|
|
26
|
+
*/
|
|
27
|
+
async uploadDirect(file, options) {
|
|
28
|
+
var _a, _b, _c, _d;
|
|
29
|
+
// Step 1: Get upload token
|
|
30
|
+
const tokenRes = await this.fetchJson(`${this.apiBase}/api/uploader/token/private`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
filename: file.name,
|
|
34
|
+
fsize: file.size,
|
|
35
|
+
mimeType: file.type || 'application/octet-stream',
|
|
36
|
+
scope: options.scope,
|
|
37
|
+
signature: 'browser-upload', // Will be validated differently for browser uploads
|
|
38
|
+
tenantId: options.tenantId,
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
const { token, fileId, key, cdnUrl, bucket } = (_a = tokenRes.data) !== null && _a !== void 0 ? _a : tokenRes;
|
|
42
|
+
// Step 2: PUT file to presigned URL
|
|
43
|
+
await this.uploadToPresignedUrl(token, file);
|
|
44
|
+
(_b = options.onProgress) === null || _b === void 0 ? void 0 : _b.call(options, { percent: 100, loaded: file.size, total: file.size });
|
|
45
|
+
return { fileId, key, url: (_d = (_c = tokenRes.data) === null || _c === void 0 ? void 0 : _c.url) !== null && _d !== void 0 ? _d : token, cdnUrl, bucket };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Multipart upload for large files.
|
|
49
|
+
* 1. Init multipart upload → get uploadId + first part token
|
|
50
|
+
* 2. Upload first part
|
|
51
|
+
* 3. Get tokens for remaining parts and upload in parallel
|
|
52
|
+
* 4. Complete multipart upload
|
|
53
|
+
*/
|
|
54
|
+
async uploadMultipart(file, options) {
|
|
55
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
56
|
+
const partSize = (_a = options.partSize) !== null && _a !== void 0 ? _a : DEFAULT_PART_SIZE;
|
|
57
|
+
const concurrency = (_b = options.concurrency) !== null && _b !== void 0 ? _b : 3;
|
|
58
|
+
const totalParts = Math.ceil(file.size / partSize);
|
|
59
|
+
// Step 1: Init multipart upload
|
|
60
|
+
const initRes = await this.fetchJson(`${this.apiBase}/api/uploader/init/multipart`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
filename: file.name,
|
|
64
|
+
fsize: file.size,
|
|
65
|
+
mimeType: file.type || 'application/octet-stream',
|
|
66
|
+
scope: options.scope,
|
|
67
|
+
signature: 'browser-upload',
|
|
68
|
+
tenantId: options.tenantId,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
const { token, fileId, key, bucket, url: uploadId } = (_c = initRes.data) !== null && _c !== void 0 ? _c : initRes;
|
|
72
|
+
// Step 2: Upload first part
|
|
73
|
+
const part1 = file.slice(0, partSize);
|
|
74
|
+
await this.uploadToPresignedUrl(token, part1);
|
|
75
|
+
const parts = [{ partNumber: 1, etag: 'uploaded' }];
|
|
76
|
+
let loaded = partSize;
|
|
77
|
+
(_d = options.onProgress) === null || _d === void 0 ? void 0 : _d.call(options, { percent: Math.round((loaded / file.size) * 100), loaded, total: file.size });
|
|
78
|
+
// Step 3: Upload remaining parts in parallel batches
|
|
79
|
+
for (let i = 2; i <= totalParts; i += concurrency) {
|
|
80
|
+
const batch = [];
|
|
81
|
+
const batchEnd = Math.min(i + concurrency - 1, totalParts);
|
|
82
|
+
for (let partNumber = i; partNumber <= batchEnd; partNumber++) {
|
|
83
|
+
batch.push(this.uploadPart(partNumber, file, partSize, uploadId, fileId, options));
|
|
84
|
+
}
|
|
85
|
+
const results = await Promise.all(batch);
|
|
86
|
+
parts.push(...results.filter(Boolean));
|
|
87
|
+
loaded += (batchEnd - i + 1) * partSize;
|
|
88
|
+
const actualLoaded = Math.min(loaded, file.size);
|
|
89
|
+
(_e = options.onProgress) === null || _e === void 0 ? void 0 : _e.call(options, { percent: Math.round((actualLoaded / file.size) * 100), loaded: actualLoaded, total: file.size });
|
|
90
|
+
}
|
|
91
|
+
// Step 4: Complete multipart upload
|
|
92
|
+
const completeRes = await this.fetchJson(`${this.apiBase}/api/uploader/complete`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
fileId,
|
|
96
|
+
signature: 'browser-upload',
|
|
97
|
+
parts,
|
|
98
|
+
uploadId,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
fileId,
|
|
103
|
+
key,
|
|
104
|
+
url: (_g = (_f = completeRes.data) === null || _f === void 0 ? void 0 : _f.url) !== null && _g !== void 0 ? _g : '',
|
|
105
|
+
cdnUrl: (_j = (_h = completeRes.data) === null || _h === void 0 ? void 0 : _h.cdnUrl) !== null && _j !== void 0 ? _j : null,
|
|
106
|
+
bucket,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Upload a single part in a multipart upload.
|
|
111
|
+
*/
|
|
112
|
+
async uploadPart(partNumber, file, partSize, uploadId, fileId, options) {
|
|
113
|
+
var _a, _b;
|
|
114
|
+
try {
|
|
115
|
+
const tokenRes = await this.fetchJson(`${this.apiBase}/api/uploader/token/multipart`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
fileId,
|
|
119
|
+
uploadId,
|
|
120
|
+
partNumber,
|
|
121
|
+
signature: 'browser-upload',
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
const start = (partNumber - 1) * partSize;
|
|
125
|
+
const end = Math.min(start + partSize, file.size);
|
|
126
|
+
const chunk = file.slice(start, end);
|
|
127
|
+
await this.uploadToPresignedUrl((_b = (_a = tokenRes.data) === null || _a === void 0 ? void 0 : _a.token) !== null && _b !== void 0 ? _b : tokenRes.token, chunk);
|
|
128
|
+
return { partNumber, etag: `etag-${partNumber}` };
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
// Part upload failed; caller can retry
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* PUT file/blob directly to a presigned URL.
|
|
137
|
+
*/
|
|
138
|
+
async uploadToPresignedUrl(presignedUrl, data) {
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch(presignedUrl, {
|
|
143
|
+
method: 'PUT',
|
|
144
|
+
body: data,
|
|
145
|
+
signal: controller.signal,
|
|
146
|
+
headers: {
|
|
147
|
+
'Content-Type': data.type || 'application/octet-stream',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
throw new Error(`Upload to storage failed: HTTP ${res.status}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Fetch JSON from API, extracting the data field from the standard response wrapper.
|
|
160
|
+
*/
|
|
161
|
+
async fetchJson(url, options) {
|
|
162
|
+
var _a;
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal, headers: Object.assign({ 'Content-Type': 'application/json' }, ((_a = options === null || options === void 0 ? void 0 : options.headers) !== null && _a !== void 0 ? _a : {})), credentials: 'include' }));
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
169
|
+
}
|
|
170
|
+
const json = await res.json();
|
|
171
|
+
// Unwrap standard { code, msg, data } envelope
|
|
172
|
+
if (json && typeof json === 'object' && 'data' in json) {
|
|
173
|
+
return json;
|
|
174
|
+
}
|
|
175
|
+
return json;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
clearTimeout(timeoutId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.FileUploader = FileUploader;
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dofe/file-sdk-web",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DofeAI unified file uploader SDK for browsers — direct upload, multipart upload with progress, via sso.dofe.ai",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc --project tsconfig.build.json",
|
|
15
|
+
"clean": "rm -rf dist",
|
|
16
|
+
"typecheck": "tsc --project tsconfig.build.json --noEmit",
|
|
17
|
+
"prepublishOnly": "pnpm run clean && pnpm run build"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^6.0.3"
|
|
21
|
+
},
|
|
22
|
+
"type": "commonjs",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"keywords": [
|
|
31
|
+
"dofe",
|
|
32
|
+
"file",
|
|
33
|
+
"upload",
|
|
34
|
+
"browser",
|
|
35
|
+
"multipart",
|
|
36
|
+
"sso"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/dofe-ai/sso.dofe.ai.git"
|
|
42
|
+
},
|
|
43
|
+
"author": "Techwu <wumin.itea@gmail.com>"
|
|
44
|
+
}
|