@thetechfossil/upfiles 0.1.0 → 0.3.0-beta
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 +124 -32
- package/dist/index.d.mts +78 -5
- package/dist/index.d.ts +78 -5
- package/dist/index.js +278 -11
- package/dist/index.mjs +277 -11
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -1,46 +1,48 @@
|
|
|
1
|
-
# @upfiles
|
|
1
|
+
# @thetechfossil/upfiles
|
|
2
2
|
|
|
3
|
-
Lightweight JavaScript client and React component to upload files
|
|
3
|
+
Lightweight JavaScript client and React component to upload files via a presigned S3 flow. Similar to UploadThing-like DX.
|
|
4
4
|
|
|
5
5
|
- Client: get presigned URLs and PUT to S3
|
|
6
6
|
- React `<Uploader />`: drag/drop, progress, single/multiple, accepts/types, size limits
|
|
7
|
+
- React `<ProjectFilesWidget />`: list all files for a project (Plugin API), pick one, and optionally auto-save to your app
|
|
7
8
|
|
|
8
9
|
## Installation
|
|
9
10
|
|
|
10
11
|
```bash
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
|
|
12
|
+
# Bun (recommended)
|
|
13
|
+
bun add @thetechfossil/upfiles
|
|
14
|
+
|
|
15
|
+
# npm
|
|
16
|
+
npm install @thetechfossil/upfiles
|
|
17
|
+
|
|
18
|
+
# pnpm
|
|
19
|
+
pnpm add @thetechfossil/upfiles
|
|
20
|
+
|
|
21
|
+
# yarn
|
|
22
|
+
yarn add @thetechfossil/upfiles
|
|
16
23
|
```
|
|
17
24
|
|
|
18
25
|
## Server prerequisites
|
|
19
26
|
|
|
20
|
-
Your
|
|
27
|
+
Your Upfiles app must expose the Plugin API presign endpoint:
|
|
21
28
|
|
|
22
29
|
- Method: `POST`
|
|
23
|
-
- Path: `/api/upload/presigned-url` (configurable
|
|
24
|
-
- Body: `{ fileName, fileType, fileSize? }`
|
|
25
|
-
- Response: `{ presignedUrl, fileKey, publicUrl }`
|
|
30
|
+
- Path: `/api/plugin/upload/presigned-url` (default; configurable)
|
|
31
|
+
- Body: `{ fileName, fileType, fileSize, folderPath?, projectId? }` (`fileSize` is required)
|
|
32
|
+
- Response: `{ presignedUrl, fileKey, publicUrl, apiKeyId?, projectId? }`
|
|
26
33
|
|
|
27
|
-
This repository already provides
|
|
34
|
+
This repository already provides a Next.js route at `src/app/api/plugin/upload/presigned-url/route.ts`.
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
For plugins/packages calling across origins, authenticate using an API key header as documented below. CORS must allow your plugin origin.
|
|
30
37
|
|
|
31
38
|
## Basic usage (React)
|
|
32
39
|
|
|
33
40
|
```tsx
|
|
34
|
-
import { Uploader } from '@upfiles
|
|
41
|
+
import { Uploader } from '@thetechfossil/upfiles';
|
|
35
42
|
|
|
36
43
|
export default function Page() {
|
|
37
44
|
return (
|
|
38
45
|
<Uploader
|
|
39
|
-
clientOptions={{
|
|
40
|
-
baseUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:4000',
|
|
41
|
-
// headers: { Authorization: `Bearer ${token}` },
|
|
42
|
-
withCredentials: true, // if your server uses NextAuth session cookies
|
|
43
|
-
}}
|
|
44
46
|
multiple
|
|
45
47
|
accept={["image/*", "application/pdf"]}
|
|
46
48
|
maxFileSize={100 * 1024 * 1024}
|
|
@@ -52,6 +54,13 @@ export default function Page() {
|
|
|
52
54
|
}}
|
|
53
55
|
dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
|
|
54
56
|
buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
|
|
57
|
+
// Optional: pass clientOptions for cross-origin + API key auth
|
|
58
|
+
// clientOptions={{
|
|
59
|
+
// baseUrl: 'https://YOUR_APP_HOST',
|
|
60
|
+
// // or presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url',
|
|
61
|
+
// apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
|
|
62
|
+
// apiKeyHeader: 'authorization', // or 'x-api-key' | 'x-up-api-key'
|
|
63
|
+
// }}
|
|
55
64
|
/>
|
|
56
65
|
);
|
|
57
66
|
}
|
|
@@ -60,18 +69,25 @@ export default function Page() {
|
|
|
60
69
|
## Basic usage (vanilla JS)
|
|
61
70
|
|
|
62
71
|
```ts
|
|
63
|
-
import { UpfilesClient } from '@upfiles
|
|
72
|
+
import { UpfilesClient } from '@thetechfossil/upfiles';
|
|
64
73
|
|
|
74
|
+
// Same-origin by default; only set these when needed:
|
|
65
75
|
const client = new UpfilesClient({
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
// presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url', // full override
|
|
77
|
+
// baseUrl: 'https://YOUR_APP_HOST', // used with presignPath
|
|
78
|
+
// presignPath: '/api/plugin/upload/presigned-url', // default
|
|
79
|
+
// withCredentials: true, // for session routes (not typical for Plugin API)
|
|
80
|
+
// Provide API key for Plugin API auth:
|
|
81
|
+
// apiKey: 'upk_...'
|
|
82
|
+
// apiKeyHeader: 'authorization' // or 'x-api-key' | 'x-up-api-key'
|
|
68
83
|
});
|
|
69
84
|
|
|
70
85
|
async function upload(file) {
|
|
71
86
|
const { presignedUrl, publicUrl } = await client.getPresignedUrl({
|
|
72
87
|
fileName: file.name,
|
|
73
88
|
fileType: file.type,
|
|
74
|
-
fileSize: file.size,
|
|
89
|
+
fileSize: file.size, // required
|
|
90
|
+
folderPath: 'my-plugin/uploads/',
|
|
75
91
|
});
|
|
76
92
|
const res = await client.uploadToS3(presignedUrl, file);
|
|
77
93
|
if (!res.ok) throw new Error('Upload failed');
|
|
@@ -82,25 +98,101 @@ async function upload(file) {
|
|
|
82
98
|
## API
|
|
83
99
|
|
|
84
100
|
- `new UpfilesClient(options)`
|
|
85
|
-
- `
|
|
86
|
-
- `
|
|
87
|
-
- `
|
|
88
|
-
- `
|
|
89
|
-
- `
|
|
101
|
+
- `presignUrl` (optional): full absolute URL to presign endpoint. Highest priority.
|
|
102
|
+
- `baseUrl` (optional): API base origin. Used with `presignPath`.
|
|
103
|
+
- `presignPath` (optional): relative path, default `/api/plugin/upload/presigned-url`.
|
|
104
|
+
- `headers` (optional): extra headers to send.
|
|
105
|
+
- `apiKey` (optional): API key value (e.g., `upk_...`).
|
|
106
|
+
- `apiKeyHeader` (optional): one of `'authorization' | 'x-api-key' | 'x-up-api-key'` (default `'authorization'`). If `'authorization'`, `Bearer` prefix is auto-added for `upk_...`.
|
|
107
|
+
- `withCredentials` (optional): `true` to send cookies.
|
|
108
|
+
- Defaults: same-origin requests to `/api/plugin/upload/presigned-url`.
|
|
109
|
+
- `client.getPresignedUrl({ fileName, fileType, fileSize, projectId?, folderPath? })`
|
|
90
110
|
- `client.uploadToS3(presignedUrl, file)`
|
|
111
|
+
- `client.upload(file, { projectId?, folderPath?, fetchThumbnails? })` → returns `{ publicUrl, fileKey, fileName, fileType, fileSize, projectId?, apiKeyId?, thumbnails? }`
|
|
112
|
+
- `client.getThumbnails(fileKey)` → returns `Thumbnail[]`
|
|
113
|
+
- `client.getProjectFiles({ folderPath? })` → returns `FileListItem[]`
|
|
91
114
|
|
|
92
115
|
- `<Uploader />` props
|
|
93
|
-
- `clientOptions
|
|
116
|
+
- `clientOptions` (optional): same as `UpfilesClient` options (only needed for cross-origin or auth customization)
|
|
94
117
|
- `multiple` (default true)
|
|
95
118
|
- `accept` (array of MIME patterns)
|
|
96
119
|
- `maxFileSize` (bytes, default 100MB)
|
|
97
120
|
- `maxFiles` (default 10)
|
|
121
|
+
- `fetchThumbnails` (boolean): if true, fetch thumbnails after upload via Plugin API
|
|
98
122
|
- `onComplete(files)` and `onError(error)`
|
|
99
123
|
- `className`, `buttonClassName`, `dropzoneClassName`
|
|
100
124
|
- `children`: custom dropzone inner content
|
|
101
125
|
|
|
126
|
+
### Thumbnails usage
|
|
127
|
+
|
|
128
|
+
Your Upfiles app exposes `GET /api/plugin/thumbnails?fileKey=...` for plugins. Enable `fetchThumbnails` to auto-fetch after upload, or call `client.getThumbnails(fileKey)` yourself.
|
|
129
|
+
|
|
130
|
+
Example with `client.upload`:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const result = await client.upload(file, { folderPath: 'my-plugin/', fetchThumbnails: true });
|
|
134
|
+
console.log(result.thumbnails);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### List project files (Plugin API)
|
|
138
|
+
|
|
139
|
+
Your Upfiles app exposes `GET /api/plugin/files?folderPath=...` for plugins (API key auth + CORS). The client can call it via `getProjectFiles`:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { UpfilesClient } from '@thetechfossil/upfiles';
|
|
143
|
+
|
|
144
|
+
const client = new UpfilesClient({
|
|
145
|
+
baseUrl: 'https://YOUR_APP_HOST',
|
|
146
|
+
apiKey: 'upk_...',
|
|
147
|
+
thumbnailsPath: '/api/plugin/thumbnails', // used to derive /api/plugin/files
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const files = await client.getProjectFiles({ folderPath: 'optional/subfolder' });
|
|
151
|
+
// [{ key, originalName, size, contentType, uploadedAt, url }, ...]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `<ProjectFilesWidget />`
|
|
155
|
+
|
|
156
|
+
Simple UI to fetch and select a file for the project tied to the API key. Emits the selection, and can optionally POST it to your backend.
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { ProjectFilesWidget } from '@thetechfossil/upfiles';
|
|
160
|
+
|
|
161
|
+
export default function PickFile() {
|
|
162
|
+
return (
|
|
163
|
+
<ProjectFilesWidget
|
|
164
|
+
clientOptions={{
|
|
165
|
+
baseUrl: 'https://YOUR_APP_HOST',
|
|
166
|
+
apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
|
|
167
|
+
apiKeyHeader: 'authorization',
|
|
168
|
+
thumbnailsPath: '/api/plugin/thumbnails',
|
|
169
|
+
}}
|
|
170
|
+
// Optional: filter within a folder
|
|
171
|
+
// folderPath="my-plugin/"
|
|
172
|
+
|
|
173
|
+
onSelect={(f) => {
|
|
174
|
+
// { name, key, url, size, contentType }
|
|
175
|
+
console.log('Selected:', f.name, f.key);
|
|
176
|
+
}}
|
|
177
|
+
|
|
178
|
+
// Optional: built-in save
|
|
179
|
+
// saveUrl="/api/files/save" // Your app route to persist selection
|
|
180
|
+
// onSave={async (f) => { await fetch('/api/files/save', { method: 'POST', body: JSON.stringify(f) }); }}
|
|
181
|
+
// onSaved={(result) => console.log('Saved!', result)}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
102
187
|
## Notes
|
|
103
188
|
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
189
|
+
- Dev with Vite: configure a proxy so `/api/*` maps to your backend (e.g., `http://localhost:4000`), then `<Uploader />` works with no config.
|
|
190
|
+
- Ensure CORS where applicable (S3 bucket CORS; and set `PLUGIN_ALLOWED_ORIGINS` on your Upfiles app for cross-origin Plugin API calls).
|
|
191
|
+
- For cross-origin with cookies (NextAuth), set `withCredentials: true` and configure your server to allow credentials.
|
|
192
|
+
Plugin API typically uses API keys instead of cookies.
|
|
193
|
+
|
|
194
|
+
### Plugin endpoints provided by this repo
|
|
195
|
+
|
|
196
|
+
- `POST /api/plugin/upload/presigned-url` – generate presigned S3 URL for upload
|
|
197
|
+
- `GET /api/plugin/thumbnails?fileKey=...` – list thumbnails for a file
|
|
198
|
+
- `GET /api/plugin/files?folderPath=...` – list project files (API key scoped), optional folder prefix
|
package/dist/index.d.mts
CHANGED
|
@@ -4,27 +4,72 @@ type PresignResponse = {
|
|
|
4
4
|
presignedUrl: string;
|
|
5
5
|
fileKey: string;
|
|
6
6
|
publicUrl: string;
|
|
7
|
+
apiKeyId?: string;
|
|
8
|
+
projectId?: string;
|
|
9
|
+
};
|
|
10
|
+
type UploadedFileResult = {
|
|
11
|
+
publicUrl: string;
|
|
12
|
+
fileKey: string;
|
|
13
|
+
fileName: string;
|
|
14
|
+
fileType: string;
|
|
15
|
+
fileSize: number;
|
|
16
|
+
projectId?: string;
|
|
17
|
+
apiKeyId?: string;
|
|
18
|
+
thumbnails?: Thumbnail[];
|
|
19
|
+
};
|
|
20
|
+
type Thumbnail = {
|
|
21
|
+
id: string;
|
|
22
|
+
key: string;
|
|
23
|
+
url: string;
|
|
24
|
+
size: number;
|
|
25
|
+
sizeType?: string;
|
|
26
|
+
createdAt?: string;
|
|
27
|
+
};
|
|
28
|
+
type FileListItem = {
|
|
29
|
+
key: string;
|
|
30
|
+
originalName: string;
|
|
31
|
+
size: number;
|
|
32
|
+
contentType: string;
|
|
33
|
+
uploadedAt: string;
|
|
34
|
+
url: string;
|
|
7
35
|
};
|
|
8
36
|
type UpfilesClientOptions = {
|
|
9
|
-
baseUrl
|
|
37
|
+
baseUrl?: string;
|
|
10
38
|
presignPath?: string;
|
|
39
|
+
presignUrl?: string;
|
|
11
40
|
headers?: Record<string, string>;
|
|
12
41
|
withCredentials?: boolean;
|
|
42
|
+
apiKey?: string;
|
|
43
|
+
apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
|
|
44
|
+
thumbnailsPath?: string;
|
|
13
45
|
};
|
|
14
46
|
declare class UpfilesClient {
|
|
15
|
-
private baseUrl
|
|
47
|
+
private baseUrl?;
|
|
16
48
|
private presignPath;
|
|
49
|
+
private presignUrl?;
|
|
17
50
|
private headers?;
|
|
18
51
|
private withCredentials?;
|
|
52
|
+
private apiKey?;
|
|
53
|
+
private apiKeyHeader;
|
|
54
|
+
private thumbnailsPath;
|
|
19
55
|
constructor(opts: UpfilesClientOptions);
|
|
20
56
|
getPresignedUrl(params: {
|
|
21
57
|
fileName: string;
|
|
22
58
|
fileType: string;
|
|
23
|
-
fileSize
|
|
59
|
+
fileSize: number;
|
|
24
60
|
projectId?: string;
|
|
25
61
|
folderPath?: string;
|
|
26
62
|
}): Promise<PresignResponse>;
|
|
27
63
|
uploadToS3(presignedUrl: string, file: File): Promise<Response>;
|
|
64
|
+
upload(file: File, extras?: {
|
|
65
|
+
projectId?: string;
|
|
66
|
+
folderPath?: string;
|
|
67
|
+
fetchThumbnails?: boolean;
|
|
68
|
+
}): Promise<UploadedFileResult>;
|
|
69
|
+
getThumbnails(fileKey: string): Promise<Thumbnail[]>;
|
|
70
|
+
getProjectFiles(params?: {
|
|
71
|
+
folderPath?: string;
|
|
72
|
+
}): Promise<FileListItem[]>;
|
|
28
73
|
}
|
|
29
74
|
|
|
30
75
|
type UploaderFile = {
|
|
@@ -38,9 +83,11 @@ type UploaderFile = {
|
|
|
38
83
|
error?: string;
|
|
39
84
|
publicUrl?: string;
|
|
40
85
|
fileKey?: string;
|
|
86
|
+
projectId?: string;
|
|
87
|
+
apiKeyId?: string;
|
|
41
88
|
};
|
|
42
89
|
type UploaderProps = {
|
|
43
|
-
clientOptions
|
|
90
|
+
clientOptions?: UpfilesClientOptions;
|
|
44
91
|
multiple?: boolean;
|
|
45
92
|
accept?: string[];
|
|
46
93
|
maxFileSize?: number;
|
|
@@ -53,7 +100,33 @@ type UploaderProps = {
|
|
|
53
100
|
buttonClassName?: string;
|
|
54
101
|
dropzoneClassName?: string;
|
|
55
102
|
children?: React.ReactNode;
|
|
103
|
+
fetchThumbnails?: boolean;
|
|
56
104
|
};
|
|
57
105
|
declare const Uploader: React.FC<UploaderProps>;
|
|
58
106
|
|
|
59
|
-
|
|
107
|
+
type ProjectFilesWidgetProps = {
|
|
108
|
+
clientOptions: UpfilesClientOptions;
|
|
109
|
+
folderPath?: string;
|
|
110
|
+
className?: string;
|
|
111
|
+
listClassName?: string;
|
|
112
|
+
itemClassName?: string;
|
|
113
|
+
onSelect: (file: {
|
|
114
|
+
name: string;
|
|
115
|
+
key: string;
|
|
116
|
+
url: string;
|
|
117
|
+
size: number;
|
|
118
|
+
contentType: string;
|
|
119
|
+
}) => void;
|
|
120
|
+
saveUrl?: string;
|
|
121
|
+
onSave?: (file: {
|
|
122
|
+
name: string;
|
|
123
|
+
key: string;
|
|
124
|
+
url: string;
|
|
125
|
+
size: number;
|
|
126
|
+
contentType: string;
|
|
127
|
+
}) => Promise<void> | void;
|
|
128
|
+
onSaved?: (result?: any) => void;
|
|
129
|
+
};
|
|
130
|
+
declare const ProjectFilesWidget: React.FC<ProjectFilesWidgetProps>;
|
|
131
|
+
|
|
132
|
+
export { type FileListItem, type PresignResponse, ProjectFilesWidget, type ProjectFilesWidgetProps, type Thumbnail, UpfilesClient, type UpfilesClientOptions, type UploadedFileResult, Uploader, type UploaderFile, type UploaderProps };
|
package/dist/index.d.ts
CHANGED
|
@@ -4,27 +4,72 @@ type PresignResponse = {
|
|
|
4
4
|
presignedUrl: string;
|
|
5
5
|
fileKey: string;
|
|
6
6
|
publicUrl: string;
|
|
7
|
+
apiKeyId?: string;
|
|
8
|
+
projectId?: string;
|
|
9
|
+
};
|
|
10
|
+
type UploadedFileResult = {
|
|
11
|
+
publicUrl: string;
|
|
12
|
+
fileKey: string;
|
|
13
|
+
fileName: string;
|
|
14
|
+
fileType: string;
|
|
15
|
+
fileSize: number;
|
|
16
|
+
projectId?: string;
|
|
17
|
+
apiKeyId?: string;
|
|
18
|
+
thumbnails?: Thumbnail[];
|
|
19
|
+
};
|
|
20
|
+
type Thumbnail = {
|
|
21
|
+
id: string;
|
|
22
|
+
key: string;
|
|
23
|
+
url: string;
|
|
24
|
+
size: number;
|
|
25
|
+
sizeType?: string;
|
|
26
|
+
createdAt?: string;
|
|
27
|
+
};
|
|
28
|
+
type FileListItem = {
|
|
29
|
+
key: string;
|
|
30
|
+
originalName: string;
|
|
31
|
+
size: number;
|
|
32
|
+
contentType: string;
|
|
33
|
+
uploadedAt: string;
|
|
34
|
+
url: string;
|
|
7
35
|
};
|
|
8
36
|
type UpfilesClientOptions = {
|
|
9
|
-
baseUrl
|
|
37
|
+
baseUrl?: string;
|
|
10
38
|
presignPath?: string;
|
|
39
|
+
presignUrl?: string;
|
|
11
40
|
headers?: Record<string, string>;
|
|
12
41
|
withCredentials?: boolean;
|
|
42
|
+
apiKey?: string;
|
|
43
|
+
apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
|
|
44
|
+
thumbnailsPath?: string;
|
|
13
45
|
};
|
|
14
46
|
declare class UpfilesClient {
|
|
15
|
-
private baseUrl
|
|
47
|
+
private baseUrl?;
|
|
16
48
|
private presignPath;
|
|
49
|
+
private presignUrl?;
|
|
17
50
|
private headers?;
|
|
18
51
|
private withCredentials?;
|
|
52
|
+
private apiKey?;
|
|
53
|
+
private apiKeyHeader;
|
|
54
|
+
private thumbnailsPath;
|
|
19
55
|
constructor(opts: UpfilesClientOptions);
|
|
20
56
|
getPresignedUrl(params: {
|
|
21
57
|
fileName: string;
|
|
22
58
|
fileType: string;
|
|
23
|
-
fileSize
|
|
59
|
+
fileSize: number;
|
|
24
60
|
projectId?: string;
|
|
25
61
|
folderPath?: string;
|
|
26
62
|
}): Promise<PresignResponse>;
|
|
27
63
|
uploadToS3(presignedUrl: string, file: File): Promise<Response>;
|
|
64
|
+
upload(file: File, extras?: {
|
|
65
|
+
projectId?: string;
|
|
66
|
+
folderPath?: string;
|
|
67
|
+
fetchThumbnails?: boolean;
|
|
68
|
+
}): Promise<UploadedFileResult>;
|
|
69
|
+
getThumbnails(fileKey: string): Promise<Thumbnail[]>;
|
|
70
|
+
getProjectFiles(params?: {
|
|
71
|
+
folderPath?: string;
|
|
72
|
+
}): Promise<FileListItem[]>;
|
|
28
73
|
}
|
|
29
74
|
|
|
30
75
|
type UploaderFile = {
|
|
@@ -38,9 +83,11 @@ type UploaderFile = {
|
|
|
38
83
|
error?: string;
|
|
39
84
|
publicUrl?: string;
|
|
40
85
|
fileKey?: string;
|
|
86
|
+
projectId?: string;
|
|
87
|
+
apiKeyId?: string;
|
|
41
88
|
};
|
|
42
89
|
type UploaderProps = {
|
|
43
|
-
clientOptions
|
|
90
|
+
clientOptions?: UpfilesClientOptions;
|
|
44
91
|
multiple?: boolean;
|
|
45
92
|
accept?: string[];
|
|
46
93
|
maxFileSize?: number;
|
|
@@ -53,7 +100,33 @@ type UploaderProps = {
|
|
|
53
100
|
buttonClassName?: string;
|
|
54
101
|
dropzoneClassName?: string;
|
|
55
102
|
children?: React.ReactNode;
|
|
103
|
+
fetchThumbnails?: boolean;
|
|
56
104
|
};
|
|
57
105
|
declare const Uploader: React.FC<UploaderProps>;
|
|
58
106
|
|
|
59
|
-
|
|
107
|
+
type ProjectFilesWidgetProps = {
|
|
108
|
+
clientOptions: UpfilesClientOptions;
|
|
109
|
+
folderPath?: string;
|
|
110
|
+
className?: string;
|
|
111
|
+
listClassName?: string;
|
|
112
|
+
itemClassName?: string;
|
|
113
|
+
onSelect: (file: {
|
|
114
|
+
name: string;
|
|
115
|
+
key: string;
|
|
116
|
+
url: string;
|
|
117
|
+
size: number;
|
|
118
|
+
contentType: string;
|
|
119
|
+
}) => void;
|
|
120
|
+
saveUrl?: string;
|
|
121
|
+
onSave?: (file: {
|
|
122
|
+
name: string;
|
|
123
|
+
key: string;
|
|
124
|
+
url: string;
|
|
125
|
+
size: number;
|
|
126
|
+
contentType: string;
|
|
127
|
+
}) => Promise<void> | void;
|
|
128
|
+
onSaved?: (result?: any) => void;
|
|
129
|
+
};
|
|
130
|
+
declare const ProjectFilesWidget: React.FC<ProjectFilesWidgetProps>;
|
|
131
|
+
|
|
132
|
+
export { type FileListItem, type PresignResponse, ProjectFilesWidget, type ProjectFilesWidgetProps, type Thumbnail, UpfilesClient, type UpfilesClientOptions, type UploadedFileResult, Uploader, type UploaderFile, type UploaderProps };
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ProjectFilesWidget: () => ProjectFilesWidget,
|
|
23
24
|
UpfilesClient: () => UpfilesClient,
|
|
24
25
|
Uploader: () => Uploader
|
|
25
26
|
});
|
|
@@ -28,18 +29,36 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
29
|
// src/client.ts
|
|
29
30
|
var UpfilesClient = class {
|
|
30
31
|
constructor(opts) {
|
|
31
|
-
this.baseUrl = opts.baseUrl
|
|
32
|
-
this.presignPath = opts.presignPath ?? "/api/upload/presigned-url";
|
|
32
|
+
this.baseUrl = opts.baseUrl?.replace(/\/$/, "");
|
|
33
|
+
this.presignPath = opts.presignPath ?? "/api/plugin/upload/presigned-url";
|
|
34
|
+
this.presignUrl = opts.presignUrl;
|
|
33
35
|
this.headers = opts.headers;
|
|
34
36
|
this.withCredentials = opts.withCredentials;
|
|
37
|
+
this.apiKey = opts.apiKey;
|
|
38
|
+
this.apiKeyHeader = opts.apiKeyHeader ?? "authorization";
|
|
39
|
+
this.thumbnailsPath = opts.thumbnailsPath ?? "/api/plugin/thumbnails";
|
|
35
40
|
}
|
|
36
41
|
async getPresignedUrl(params) {
|
|
37
|
-
|
|
42
|
+
if (!params.fileName) throw new Error("fileName is required");
|
|
43
|
+
if (!params.fileType) throw new Error("fileType is required");
|
|
44
|
+
if (!params.fileSize || params.fileSize <= 0) throw new Error("fileSize must be > 0");
|
|
45
|
+
const target = this.presignUrl ? this.presignUrl : this.baseUrl ? `${this.baseUrl}${this.presignPath}` : this.presignPath;
|
|
46
|
+
const headers = {
|
|
47
|
+
"content-type": "application/json",
|
|
48
|
+
...this.headers || {}
|
|
49
|
+
};
|
|
50
|
+
if (this.apiKey) {
|
|
51
|
+
if (this.apiKeyHeader === "authorization") {
|
|
52
|
+
headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
|
|
53
|
+
} else if (this.apiKeyHeader === "x-api-key") {
|
|
54
|
+
headers["x-api-key"] = this.apiKey;
|
|
55
|
+
} else {
|
|
56
|
+
headers["x-up-api-key"] = this.apiKey;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const res = await fetch(target, {
|
|
38
60
|
method: "POST",
|
|
39
|
-
headers
|
|
40
|
-
"Content-Type": "application/json",
|
|
41
|
-
...this.headers || {}
|
|
42
|
-
},
|
|
61
|
+
headers,
|
|
43
62
|
credentials: this.withCredentials ? "include" : "same-origin",
|
|
44
63
|
body: JSON.stringify(params)
|
|
45
64
|
});
|
|
@@ -63,6 +82,92 @@ var UpfilesClient = class {
|
|
|
63
82
|
}
|
|
64
83
|
});
|
|
65
84
|
}
|
|
85
|
+
// Convenience: perform full flow and return URL/path metadata for DB
|
|
86
|
+
async upload(file, extras) {
|
|
87
|
+
const presign = await this.getPresignedUrl({
|
|
88
|
+
fileName: file.name,
|
|
89
|
+
fileType: file.type || "application/octet-stream",
|
|
90
|
+
fileSize: file.size,
|
|
91
|
+
projectId: extras?.projectId,
|
|
92
|
+
folderPath: extras?.folderPath
|
|
93
|
+
});
|
|
94
|
+
const res = await this.uploadToS3(presign.presignedUrl, file);
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
throw new Error(`Upload failed with status ${res.status}`);
|
|
97
|
+
}
|
|
98
|
+
let thumbnails;
|
|
99
|
+
if (extras?.fetchThumbnails) {
|
|
100
|
+
try {
|
|
101
|
+
const t = await this.getThumbnails(presign.fileKey);
|
|
102
|
+
thumbnails = t;
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
publicUrl: presign.publicUrl,
|
|
108
|
+
fileKey: presign.fileKey,
|
|
109
|
+
fileName: file.name,
|
|
110
|
+
fileType: file.type,
|
|
111
|
+
fileSize: file.size,
|
|
112
|
+
projectId: presign.projectId,
|
|
113
|
+
apiKeyId: presign.apiKeyId,
|
|
114
|
+
thumbnails
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async getThumbnails(fileKey) {
|
|
118
|
+
const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
|
|
119
|
+
const url = `${target}?fileKey=${encodeURIComponent(fileKey)}`;
|
|
120
|
+
const headers = { ...this.headers || {} };
|
|
121
|
+
if (this.apiKey) {
|
|
122
|
+
if (this.apiKeyHeader === "authorization") {
|
|
123
|
+
headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
|
|
124
|
+
} else if (this.apiKeyHeader === "x-api-key") {
|
|
125
|
+
headers["x-api-key"] = this.apiKey;
|
|
126
|
+
} else {
|
|
127
|
+
headers["x-up-api-key"] = this.apiKey;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
let msg = "Failed to fetch thumbnails";
|
|
133
|
+
try {
|
|
134
|
+
const data2 = await res.json();
|
|
135
|
+
msg = data2.error || msg;
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
throw new Error(msg);
|
|
139
|
+
}
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
return data?.thumbnails ?? [];
|
|
142
|
+
}
|
|
143
|
+
async getProjectFiles(params) {
|
|
144
|
+
const basePath = this.thumbnailsPath.replace(/\/thumbnails$/, "");
|
|
145
|
+
const filesPath = `${basePath}/files`;
|
|
146
|
+
const target = this.baseUrl ? `${this.baseUrl}${filesPath}` : filesPath;
|
|
147
|
+
const url = params?.folderPath ? `${target}?folderPath=${encodeURIComponent(params.folderPath)}` : target;
|
|
148
|
+
const headers = { ...this.headers || {} };
|
|
149
|
+
if (this.apiKey) {
|
|
150
|
+
if (this.apiKeyHeader === "authorization") {
|
|
151
|
+
headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
|
|
152
|
+
} else if (this.apiKeyHeader === "x-api-key") {
|
|
153
|
+
headers["x-api-key"] = this.apiKey;
|
|
154
|
+
} else {
|
|
155
|
+
headers["x-up-api-key"] = this.apiKey;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
let msg = "Failed to fetch files";
|
|
161
|
+
try {
|
|
162
|
+
const data2 = await res.json();
|
|
163
|
+
msg = data2.error || msg;
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
throw new Error(msg);
|
|
167
|
+
}
|
|
168
|
+
const data = await res.json();
|
|
169
|
+
return data?.files ?? [];
|
|
170
|
+
}
|
|
66
171
|
};
|
|
67
172
|
|
|
68
173
|
// src/Uploader.tsx
|
|
@@ -81,10 +186,11 @@ var Uploader = ({
|
|
|
81
186
|
dropzoneClassName,
|
|
82
187
|
children,
|
|
83
188
|
projectId,
|
|
84
|
-
folderPath
|
|
189
|
+
folderPath,
|
|
190
|
+
fetchThumbnails
|
|
85
191
|
}) => {
|
|
86
192
|
const inputRef = (0, import_react.useRef)(null);
|
|
87
|
-
const client = (0, import_react.useMemo)(() => new UpfilesClient(clientOptions), [clientOptions]);
|
|
193
|
+
const client = (0, import_react.useMemo)(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
|
|
88
194
|
const [files, setFiles] = (0, import_react.useState)([]);
|
|
89
195
|
const [isUploading, setIsUploading] = (0, import_react.useState)(false);
|
|
90
196
|
const [isDragOver, setIsDragOver] = (0, import_react.useState)(false);
|
|
@@ -151,14 +257,31 @@ var Uploader = ({
|
|
|
151
257
|
xhr.setRequestHeader("Content-Type", item.type || "application/octet-stream");
|
|
152
258
|
xhr.send(item.file);
|
|
153
259
|
});
|
|
260
|
+
let thumbs;
|
|
261
|
+
try {
|
|
262
|
+
if (fetchThumbnails) {
|
|
263
|
+
thumbs = await client.getThumbnails(presign.fileKey);
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
154
267
|
update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
|
|
155
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
...item,
|
|
270
|
+
status: "success",
|
|
271
|
+
progress: 100,
|
|
272
|
+
fileKey: presign.fileKey,
|
|
273
|
+
publicUrl: presign.publicUrl,
|
|
274
|
+
projectId: presign.projectId,
|
|
275
|
+
apiKeyId: presign.apiKeyId,
|
|
276
|
+
// @ts-ignore - allow downstream to access thumbs if present
|
|
277
|
+
thumbnails: thumbs
|
|
278
|
+
};
|
|
156
279
|
} catch (err) {
|
|
157
280
|
const message = err instanceof Error ? err.message : "Upload failed";
|
|
158
281
|
update({ status: "error", error: message });
|
|
159
282
|
throw err;
|
|
160
283
|
}
|
|
161
|
-
}, [client]);
|
|
284
|
+
}, [client, projectId, folderPath, fetchThumbnails]);
|
|
162
285
|
const uploadAll = (0, import_react.useCallback)(async () => {
|
|
163
286
|
const targets = files.filter((f) => f.status === "pending");
|
|
164
287
|
if (!targets.length) return;
|
|
@@ -259,8 +382,152 @@ var Uploader = ({
|
|
|
259
382
|
] })
|
|
260
383
|
] });
|
|
261
384
|
};
|
|
385
|
+
|
|
386
|
+
// src/ProjectFilesWidget.tsx
|
|
387
|
+
var import_react2 = require("react");
|
|
388
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
389
|
+
var ProjectFilesWidget = ({
|
|
390
|
+
clientOptions,
|
|
391
|
+
folderPath,
|
|
392
|
+
className,
|
|
393
|
+
listClassName,
|
|
394
|
+
itemClassName,
|
|
395
|
+
onSelect,
|
|
396
|
+
saveUrl,
|
|
397
|
+
onSave,
|
|
398
|
+
onSaved
|
|
399
|
+
}) => {
|
|
400
|
+
const client = (0, import_react2.useMemo)(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
|
|
401
|
+
const [files, setFiles] = (0, import_react2.useState)([]);
|
|
402
|
+
const [loading, setLoading] = (0, import_react2.useState)(false);
|
|
403
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
404
|
+
const [query, setQuery] = (0, import_react2.useState)("");
|
|
405
|
+
const [savingKey, setSavingKey] = (0, import_react2.useState)(null);
|
|
406
|
+
(0, import_react2.useEffect)(() => {
|
|
407
|
+
let cancelled = false;
|
|
408
|
+
const run = async () => {
|
|
409
|
+
setLoading(true);
|
|
410
|
+
setError(null);
|
|
411
|
+
try {
|
|
412
|
+
const data = await client.getProjectFiles({ folderPath });
|
|
413
|
+
if (!cancelled) setFiles(data);
|
|
414
|
+
} catch (e) {
|
|
415
|
+
if (!cancelled) setError(e?.message || "Failed to load files");
|
|
416
|
+
} finally {
|
|
417
|
+
if (!cancelled) setLoading(false);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
run();
|
|
421
|
+
return () => {
|
|
422
|
+
cancelled = true;
|
|
423
|
+
};
|
|
424
|
+
}, [client, folderPath]);
|
|
425
|
+
const visible = files.filter(
|
|
426
|
+
(f) => !query ? true : (f.originalName || "").toLowerCase().includes(query.toLowerCase())
|
|
427
|
+
);
|
|
428
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className, children: [
|
|
429
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 8 }, children: [
|
|
430
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
431
|
+
"input",
|
|
432
|
+
{
|
|
433
|
+
type: "text",
|
|
434
|
+
placeholder: "Search by filename...",
|
|
435
|
+
value: query,
|
|
436
|
+
onChange: (e) => setQuery(e.target.value),
|
|
437
|
+
style: { flex: 1, padding: 8, border: "1px solid #e5e7eb", borderRadius: 6 }
|
|
438
|
+
}
|
|
439
|
+
),
|
|
440
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
441
|
+
"button",
|
|
442
|
+
{
|
|
443
|
+
onClick: () => {
|
|
444
|
+
setQuery("");
|
|
445
|
+
(async () => {
|
|
446
|
+
setLoading(true);
|
|
447
|
+
setError(null);
|
|
448
|
+
try {
|
|
449
|
+
const data = await client.getProjectFiles({ folderPath });
|
|
450
|
+
setFiles(data);
|
|
451
|
+
} catch (e) {
|
|
452
|
+
setError(e?.message || "Failed to load files");
|
|
453
|
+
} finally {
|
|
454
|
+
setLoading(false);
|
|
455
|
+
}
|
|
456
|
+
})();
|
|
457
|
+
},
|
|
458
|
+
style: { padding: "8px 12px", border: "1px solid #e5e7eb", borderRadius: 6, background: "#fff" },
|
|
459
|
+
children: "Refresh"
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
] }),
|
|
463
|
+
loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 12, color: "#666" }, children: "Loading files\u2026" }),
|
|
464
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 12, color: "#b91c1c" }, children: error }),
|
|
465
|
+
!loading && !error && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("ul", { className: listClassName, style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 8 }, children: [
|
|
466
|
+
visible.map((f) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
467
|
+
"li",
|
|
468
|
+
{
|
|
469
|
+
className: itemClassName,
|
|
470
|
+
style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 10, display: "flex", justifyContent: "space-between", alignItems: "center" },
|
|
471
|
+
children: [
|
|
472
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { minWidth: 0 }, children: [
|
|
473
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.originalName }),
|
|
474
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { fontSize: 12, color: "#666" }, children: [
|
|
475
|
+
(f.size / (1024 * 1024)).toFixed(2),
|
|
476
|
+
" MB \u2022 ",
|
|
477
|
+
f.contentType
|
|
478
|
+
] })
|
|
479
|
+
] }),
|
|
480
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
|
|
481
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: f.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
|
|
482
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
483
|
+
"button",
|
|
484
|
+
{
|
|
485
|
+
onClick: async () => {
|
|
486
|
+
const selected = { name: f.originalName, key: f.key, url: f.url, size: f.size, contentType: f.contentType };
|
|
487
|
+
onSelect(selected);
|
|
488
|
+
if (typeof onSave === "function" || !!saveUrl) {
|
|
489
|
+
try {
|
|
490
|
+
setSavingKey(f.key);
|
|
491
|
+
if (typeof onSave === "function") {
|
|
492
|
+
await onSave(selected);
|
|
493
|
+
onSaved?.();
|
|
494
|
+
}
|
|
495
|
+
if (saveUrl) {
|
|
496
|
+
const res = await fetch(saveUrl, {
|
|
497
|
+
method: "POST",
|
|
498
|
+
headers: { "content-type": "application/json" },
|
|
499
|
+
body: JSON.stringify(selected),
|
|
500
|
+
credentials: "same-origin"
|
|
501
|
+
});
|
|
502
|
+
const data = await res.json().catch(() => void 0);
|
|
503
|
+
if (!res.ok) throw new Error(data?.error || "Failed to save");
|
|
504
|
+
onSaved?.(data);
|
|
505
|
+
}
|
|
506
|
+
} catch (e) {
|
|
507
|
+
console.error("Save selection failed", e);
|
|
508
|
+
alert(e?.message || "Failed to save");
|
|
509
|
+
} finally {
|
|
510
|
+
setSavingKey(null);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
disabled: savingKey === f.key,
|
|
515
|
+
style: { padding: "6px 10px", background: "#2563eb", color: "#fff", border: "1px solid #1d4ed8", borderRadius: 6, opacity: savingKey === f.key ? 0.7 : 1 },
|
|
516
|
+
children: savingKey === f.key ? "Saving\u2026" : "Use"
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
] })
|
|
520
|
+
]
|
|
521
|
+
},
|
|
522
|
+
f.key
|
|
523
|
+
)),
|
|
524
|
+
visible.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { style: { fontSize: 12, color: "#666" }, children: "No files found" })
|
|
525
|
+
] })
|
|
526
|
+
] });
|
|
527
|
+
};
|
|
262
528
|
// Annotate the CommonJS export names for ESM import in node:
|
|
263
529
|
0 && (module.exports = {
|
|
530
|
+
ProjectFilesWidget,
|
|
264
531
|
UpfilesClient,
|
|
265
532
|
Uploader
|
|
266
533
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
2
|
var UpfilesClient = class {
|
|
3
3
|
constructor(opts) {
|
|
4
|
-
this.baseUrl = opts.baseUrl
|
|
5
|
-
this.presignPath = opts.presignPath ?? "/api/upload/presigned-url";
|
|
4
|
+
this.baseUrl = opts.baseUrl?.replace(/\/$/, "");
|
|
5
|
+
this.presignPath = opts.presignPath ?? "/api/plugin/upload/presigned-url";
|
|
6
|
+
this.presignUrl = opts.presignUrl;
|
|
6
7
|
this.headers = opts.headers;
|
|
7
8
|
this.withCredentials = opts.withCredentials;
|
|
9
|
+
this.apiKey = opts.apiKey;
|
|
10
|
+
this.apiKeyHeader = opts.apiKeyHeader ?? "authorization";
|
|
11
|
+
this.thumbnailsPath = opts.thumbnailsPath ?? "/api/plugin/thumbnails";
|
|
8
12
|
}
|
|
9
13
|
async getPresignedUrl(params) {
|
|
10
|
-
|
|
14
|
+
if (!params.fileName) throw new Error("fileName is required");
|
|
15
|
+
if (!params.fileType) throw new Error("fileType is required");
|
|
16
|
+
if (!params.fileSize || params.fileSize <= 0) throw new Error("fileSize must be > 0");
|
|
17
|
+
const target = this.presignUrl ? this.presignUrl : this.baseUrl ? `${this.baseUrl}${this.presignPath}` : this.presignPath;
|
|
18
|
+
const headers = {
|
|
19
|
+
"content-type": "application/json",
|
|
20
|
+
...this.headers || {}
|
|
21
|
+
};
|
|
22
|
+
if (this.apiKey) {
|
|
23
|
+
if (this.apiKeyHeader === "authorization") {
|
|
24
|
+
headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
|
|
25
|
+
} else if (this.apiKeyHeader === "x-api-key") {
|
|
26
|
+
headers["x-api-key"] = this.apiKey;
|
|
27
|
+
} else {
|
|
28
|
+
headers["x-up-api-key"] = this.apiKey;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const res = await fetch(target, {
|
|
11
32
|
method: "POST",
|
|
12
|
-
headers
|
|
13
|
-
"Content-Type": "application/json",
|
|
14
|
-
...this.headers || {}
|
|
15
|
-
},
|
|
33
|
+
headers,
|
|
16
34
|
credentials: this.withCredentials ? "include" : "same-origin",
|
|
17
35
|
body: JSON.stringify(params)
|
|
18
36
|
});
|
|
@@ -36,6 +54,92 @@ var UpfilesClient = class {
|
|
|
36
54
|
}
|
|
37
55
|
});
|
|
38
56
|
}
|
|
57
|
+
// Convenience: perform full flow and return URL/path metadata for DB
|
|
58
|
+
async upload(file, extras) {
|
|
59
|
+
const presign = await this.getPresignedUrl({
|
|
60
|
+
fileName: file.name,
|
|
61
|
+
fileType: file.type || "application/octet-stream",
|
|
62
|
+
fileSize: file.size,
|
|
63
|
+
projectId: extras?.projectId,
|
|
64
|
+
folderPath: extras?.folderPath
|
|
65
|
+
});
|
|
66
|
+
const res = await this.uploadToS3(presign.presignedUrl, file);
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
throw new Error(`Upload failed with status ${res.status}`);
|
|
69
|
+
}
|
|
70
|
+
let thumbnails;
|
|
71
|
+
if (extras?.fetchThumbnails) {
|
|
72
|
+
try {
|
|
73
|
+
const t = await this.getThumbnails(presign.fileKey);
|
|
74
|
+
thumbnails = t;
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
publicUrl: presign.publicUrl,
|
|
80
|
+
fileKey: presign.fileKey,
|
|
81
|
+
fileName: file.name,
|
|
82
|
+
fileType: file.type,
|
|
83
|
+
fileSize: file.size,
|
|
84
|
+
projectId: presign.projectId,
|
|
85
|
+
apiKeyId: presign.apiKeyId,
|
|
86
|
+
thumbnails
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async getThumbnails(fileKey) {
|
|
90
|
+
const target = this.baseUrl ? `${this.baseUrl}${this.thumbnailsPath}` : this.thumbnailsPath;
|
|
91
|
+
const url = `${target}?fileKey=${encodeURIComponent(fileKey)}`;
|
|
92
|
+
const headers = { ...this.headers || {} };
|
|
93
|
+
if (this.apiKey) {
|
|
94
|
+
if (this.apiKeyHeader === "authorization") {
|
|
95
|
+
headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
|
|
96
|
+
} else if (this.apiKeyHeader === "x-api-key") {
|
|
97
|
+
headers["x-api-key"] = this.apiKey;
|
|
98
|
+
} else {
|
|
99
|
+
headers["x-up-api-key"] = this.apiKey;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
let msg = "Failed to fetch thumbnails";
|
|
105
|
+
try {
|
|
106
|
+
const data2 = await res.json();
|
|
107
|
+
msg = data2.error || msg;
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
throw new Error(msg);
|
|
111
|
+
}
|
|
112
|
+
const data = await res.json();
|
|
113
|
+
return data?.thumbnails ?? [];
|
|
114
|
+
}
|
|
115
|
+
async getProjectFiles(params) {
|
|
116
|
+
const basePath = this.thumbnailsPath.replace(/\/thumbnails$/, "");
|
|
117
|
+
const filesPath = `${basePath}/files`;
|
|
118
|
+
const target = this.baseUrl ? `${this.baseUrl}${filesPath}` : filesPath;
|
|
119
|
+
const url = params?.folderPath ? `${target}?folderPath=${encodeURIComponent(params.folderPath)}` : target;
|
|
120
|
+
const headers = { ...this.headers || {} };
|
|
121
|
+
if (this.apiKey) {
|
|
122
|
+
if (this.apiKeyHeader === "authorization") {
|
|
123
|
+
headers["authorization"] = this.apiKey.startsWith("upk_") ? `Bearer ${this.apiKey}` : this.apiKey;
|
|
124
|
+
} else if (this.apiKeyHeader === "x-api-key") {
|
|
125
|
+
headers["x-api-key"] = this.apiKey;
|
|
126
|
+
} else {
|
|
127
|
+
headers["x-up-api-key"] = this.apiKey;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const res = await fetch(url, { method: "GET", headers, credentials: this.withCredentials ? "include" : "same-origin" });
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
let msg = "Failed to fetch files";
|
|
133
|
+
try {
|
|
134
|
+
const data2 = await res.json();
|
|
135
|
+
msg = data2.error || msg;
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
throw new Error(msg);
|
|
139
|
+
}
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
return data?.files ?? [];
|
|
142
|
+
}
|
|
39
143
|
};
|
|
40
144
|
|
|
41
145
|
// src/Uploader.tsx
|
|
@@ -54,10 +158,11 @@ var Uploader = ({
|
|
|
54
158
|
dropzoneClassName,
|
|
55
159
|
children,
|
|
56
160
|
projectId,
|
|
57
|
-
folderPath
|
|
161
|
+
folderPath,
|
|
162
|
+
fetchThumbnails
|
|
58
163
|
}) => {
|
|
59
164
|
const inputRef = useRef(null);
|
|
60
|
-
const client = useMemo(() => new UpfilesClient(clientOptions), [clientOptions]);
|
|
165
|
+
const client = useMemo(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
|
|
61
166
|
const [files, setFiles] = useState([]);
|
|
62
167
|
const [isUploading, setIsUploading] = useState(false);
|
|
63
168
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
@@ -124,14 +229,31 @@ var Uploader = ({
|
|
|
124
229
|
xhr.setRequestHeader("Content-Type", item.type || "application/octet-stream");
|
|
125
230
|
xhr.send(item.file);
|
|
126
231
|
});
|
|
232
|
+
let thumbs;
|
|
233
|
+
try {
|
|
234
|
+
if (fetchThumbnails) {
|
|
235
|
+
thumbs = await client.getThumbnails(presign.fileKey);
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
127
239
|
update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
|
|
128
|
-
return {
|
|
240
|
+
return {
|
|
241
|
+
...item,
|
|
242
|
+
status: "success",
|
|
243
|
+
progress: 100,
|
|
244
|
+
fileKey: presign.fileKey,
|
|
245
|
+
publicUrl: presign.publicUrl,
|
|
246
|
+
projectId: presign.projectId,
|
|
247
|
+
apiKeyId: presign.apiKeyId,
|
|
248
|
+
// @ts-ignore - allow downstream to access thumbs if present
|
|
249
|
+
thumbnails: thumbs
|
|
250
|
+
};
|
|
129
251
|
} catch (err) {
|
|
130
252
|
const message = err instanceof Error ? err.message : "Upload failed";
|
|
131
253
|
update({ status: "error", error: message });
|
|
132
254
|
throw err;
|
|
133
255
|
}
|
|
134
|
-
}, [client]);
|
|
256
|
+
}, [client, projectId, folderPath, fetchThumbnails]);
|
|
135
257
|
const uploadAll = useCallback(async () => {
|
|
136
258
|
const targets = files.filter((f) => f.status === "pending");
|
|
137
259
|
if (!targets.length) return;
|
|
@@ -232,7 +354,151 @@ var Uploader = ({
|
|
|
232
354
|
] })
|
|
233
355
|
] });
|
|
234
356
|
};
|
|
357
|
+
|
|
358
|
+
// src/ProjectFilesWidget.tsx
|
|
359
|
+
import { useEffect, useMemo as useMemo2, useState as useState2 } from "react";
|
|
360
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
361
|
+
var ProjectFilesWidget = ({
|
|
362
|
+
clientOptions,
|
|
363
|
+
folderPath,
|
|
364
|
+
className,
|
|
365
|
+
listClassName,
|
|
366
|
+
itemClassName,
|
|
367
|
+
onSelect,
|
|
368
|
+
saveUrl,
|
|
369
|
+
onSave,
|
|
370
|
+
onSaved
|
|
371
|
+
}) => {
|
|
372
|
+
const client = useMemo2(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
|
|
373
|
+
const [files, setFiles] = useState2([]);
|
|
374
|
+
const [loading, setLoading] = useState2(false);
|
|
375
|
+
const [error, setError] = useState2(null);
|
|
376
|
+
const [query, setQuery] = useState2("");
|
|
377
|
+
const [savingKey, setSavingKey] = useState2(null);
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
let cancelled = false;
|
|
380
|
+
const run = async () => {
|
|
381
|
+
setLoading(true);
|
|
382
|
+
setError(null);
|
|
383
|
+
try {
|
|
384
|
+
const data = await client.getProjectFiles({ folderPath });
|
|
385
|
+
if (!cancelled) setFiles(data);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
if (!cancelled) setError(e?.message || "Failed to load files");
|
|
388
|
+
} finally {
|
|
389
|
+
if (!cancelled) setLoading(false);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
run();
|
|
393
|
+
return () => {
|
|
394
|
+
cancelled = true;
|
|
395
|
+
};
|
|
396
|
+
}, [client, folderPath]);
|
|
397
|
+
const visible = files.filter(
|
|
398
|
+
(f) => !query ? true : (f.originalName || "").toLowerCase().includes(query.toLowerCase())
|
|
399
|
+
);
|
|
400
|
+
return /* @__PURE__ */ jsxs2("div", { className, children: [
|
|
401
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 8 }, children: [
|
|
402
|
+
/* @__PURE__ */ jsx2(
|
|
403
|
+
"input",
|
|
404
|
+
{
|
|
405
|
+
type: "text",
|
|
406
|
+
placeholder: "Search by filename...",
|
|
407
|
+
value: query,
|
|
408
|
+
onChange: (e) => setQuery(e.target.value),
|
|
409
|
+
style: { flex: 1, padding: 8, border: "1px solid #e5e7eb", borderRadius: 6 }
|
|
410
|
+
}
|
|
411
|
+
),
|
|
412
|
+
/* @__PURE__ */ jsx2(
|
|
413
|
+
"button",
|
|
414
|
+
{
|
|
415
|
+
onClick: () => {
|
|
416
|
+
setQuery("");
|
|
417
|
+
(async () => {
|
|
418
|
+
setLoading(true);
|
|
419
|
+
setError(null);
|
|
420
|
+
try {
|
|
421
|
+
const data = await client.getProjectFiles({ folderPath });
|
|
422
|
+
setFiles(data);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
setError(e?.message || "Failed to load files");
|
|
425
|
+
} finally {
|
|
426
|
+
setLoading(false);
|
|
427
|
+
}
|
|
428
|
+
})();
|
|
429
|
+
},
|
|
430
|
+
style: { padding: "8px 12px", border: "1px solid #e5e7eb", borderRadius: 6, background: "#fff" },
|
|
431
|
+
children: "Refresh"
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
] }),
|
|
435
|
+
loading && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#666" }, children: "Loading files\u2026" }),
|
|
436
|
+
error && /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "#b91c1c" }, children: error }),
|
|
437
|
+
!loading && !error && /* @__PURE__ */ jsxs2("ul", { className: listClassName, style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 8 }, children: [
|
|
438
|
+
visible.map((f) => /* @__PURE__ */ jsxs2(
|
|
439
|
+
"li",
|
|
440
|
+
{
|
|
441
|
+
className: itemClassName,
|
|
442
|
+
style: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 10, display: "flex", justifyContent: "space-between", alignItems: "center" },
|
|
443
|
+
children: [
|
|
444
|
+
/* @__PURE__ */ jsxs2("div", { style: { minWidth: 0 }, children: [
|
|
445
|
+
/* @__PURE__ */ jsx2("div", { style: { fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }, children: f.originalName }),
|
|
446
|
+
/* @__PURE__ */ jsxs2("div", { style: { fontSize: 12, color: "#666" }, children: [
|
|
447
|
+
(f.size / (1024 * 1024)).toFixed(2),
|
|
448
|
+
" MB \u2022 ",
|
|
449
|
+
f.contentType
|
|
450
|
+
] })
|
|
451
|
+
] }),
|
|
452
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
|
|
453
|
+
/* @__PURE__ */ jsx2("a", { href: f.url, target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "#2563eb" }, children: "View" }),
|
|
454
|
+
/* @__PURE__ */ jsx2(
|
|
455
|
+
"button",
|
|
456
|
+
{
|
|
457
|
+
onClick: async () => {
|
|
458
|
+
const selected = { name: f.originalName, key: f.key, url: f.url, size: f.size, contentType: f.contentType };
|
|
459
|
+
onSelect(selected);
|
|
460
|
+
if (typeof onSave === "function" || !!saveUrl) {
|
|
461
|
+
try {
|
|
462
|
+
setSavingKey(f.key);
|
|
463
|
+
if (typeof onSave === "function") {
|
|
464
|
+
await onSave(selected);
|
|
465
|
+
onSaved?.();
|
|
466
|
+
}
|
|
467
|
+
if (saveUrl) {
|
|
468
|
+
const res = await fetch(saveUrl, {
|
|
469
|
+
method: "POST",
|
|
470
|
+
headers: { "content-type": "application/json" },
|
|
471
|
+
body: JSON.stringify(selected),
|
|
472
|
+
credentials: "same-origin"
|
|
473
|
+
});
|
|
474
|
+
const data = await res.json().catch(() => void 0);
|
|
475
|
+
if (!res.ok) throw new Error(data?.error || "Failed to save");
|
|
476
|
+
onSaved?.(data);
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {
|
|
479
|
+
console.error("Save selection failed", e);
|
|
480
|
+
alert(e?.message || "Failed to save");
|
|
481
|
+
} finally {
|
|
482
|
+
setSavingKey(null);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
disabled: savingKey === f.key,
|
|
487
|
+
style: { padding: "6px 10px", background: "#2563eb", color: "#fff", border: "1px solid #1d4ed8", borderRadius: 6, opacity: savingKey === f.key ? 0.7 : 1 },
|
|
488
|
+
children: savingKey === f.key ? "Saving\u2026" : "Use"
|
|
489
|
+
}
|
|
490
|
+
)
|
|
491
|
+
] })
|
|
492
|
+
]
|
|
493
|
+
},
|
|
494
|
+
f.key
|
|
495
|
+
)),
|
|
496
|
+
visible.length === 0 && /* @__PURE__ */ jsx2("li", { style: { fontSize: 12, color: "#666" }, children: "No files found" })
|
|
497
|
+
] })
|
|
498
|
+
] });
|
|
499
|
+
};
|
|
235
500
|
export {
|
|
501
|
+
ProjectFilesWidget,
|
|
236
502
|
UpfilesClient,
|
|
237
503
|
Uploader
|
|
238
504
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thetechfossil/upfiles",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Lightweight client and React components
|
|
3
|
+
"version": "0.3.0-beta",
|
|
4
|
+
"description": "Lightweight client and React components for Upfiles Plugin API (presigned S3)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "UpFiles",
|
|
7
7
|
"repository": {
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
16
|
"import": "./dist/index.mjs",
|
|
17
|
-
"require": "./dist/index.cjs"
|
|
18
|
-
"types": "./dist/index.d.ts"
|
|
17
|
+
"require": "./dist/index.cjs"
|
|
19
18
|
}
|
|
20
19
|
},
|
|
21
20
|
"files": [
|
|
@@ -29,7 +28,7 @@
|
|
|
29
28
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
30
29
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
31
30
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
32
|
-
"prepublishOnly": "
|
|
31
|
+
"prepublishOnly": "bun run build"
|
|
33
32
|
},
|
|
34
33
|
"peerDependencies": {
|
|
35
34
|
"react": ">=18"
|