@thetechfossil/upfiles 0.1.1 → 0.3.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 +102 -17
- package/dist/index.d.mts +73 -2
- package/dist/index.d.ts +73 -2
- package/dist/index.js +273 -8
- package/dist/index.mjs +272 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ Lightweight JavaScript client and React component to upload files via a presigne
|
|
|
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
|
|
|
@@ -23,16 +24,16 @@ yarn add @thetechfossil/upfiles
|
|
|
23
24
|
|
|
24
25
|
## Server prerequisites
|
|
25
26
|
|
|
26
|
-
Your
|
|
27
|
+
Your Upfiles app must expose the Plugin API presign endpoint:
|
|
27
28
|
|
|
28
29
|
- Method: `POST`
|
|
29
|
-
- Path: `/api/upload/presigned-url` (default; configurable)
|
|
30
|
-
- Body: `{ fileName, fileType, fileSize? }`
|
|
31
|
-
- 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? }`
|
|
32
33
|
|
|
33
|
-
This repository already provides a Next.js route at `src/app/api/upload/presigned-url/route.ts`.
|
|
34
|
+
This repository already provides a Next.js route at `src/app/api/plugin/upload/presigned-url/route.ts`.
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
For plugins/packages calling across origins, authenticate using an API key header as documented below. CORS must allow your plugin origin.
|
|
36
37
|
|
|
37
38
|
## Basic usage (React)
|
|
38
39
|
|
|
@@ -53,6 +54,13 @@ export default function Page() {
|
|
|
53
54
|
}}
|
|
54
55
|
dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
|
|
55
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
|
+
// }}
|
|
56
64
|
/>
|
|
57
65
|
);
|
|
58
66
|
}
|
|
@@ -65,17 +73,21 @@ import { UpfilesClient } from '@thetechfossil/upfiles';
|
|
|
65
73
|
|
|
66
74
|
// Same-origin by default; only set these when needed:
|
|
67
75
|
const client = new UpfilesClient({
|
|
68
|
-
// presignUrl: 'https://
|
|
69
|
-
// baseUrl: 'https://
|
|
70
|
-
// presignPath: '/api/upload/presigned-url', // default
|
|
71
|
-
// withCredentials: true,
|
|
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'
|
|
72
83
|
});
|
|
73
84
|
|
|
74
85
|
async function upload(file) {
|
|
75
86
|
const { presignedUrl, publicUrl } = await client.getPresignedUrl({
|
|
76
87
|
fileName: file.name,
|
|
77
88
|
fileType: file.type,
|
|
78
|
-
fileSize: file.size,
|
|
89
|
+
fileSize: file.size, // required
|
|
90
|
+
folderPath: 'my-plugin/uploads/',
|
|
79
91
|
});
|
|
80
92
|
const res = await client.uploadToS3(presignedUrl, file);
|
|
81
93
|
if (!res.ok) throw new Error('Upload failed');
|
|
@@ -88,12 +100,17 @@ async function upload(file) {
|
|
|
88
100
|
- `new UpfilesClient(options)`
|
|
89
101
|
- `presignUrl` (optional): full absolute URL to presign endpoint. Highest priority.
|
|
90
102
|
- `baseUrl` (optional): API base origin. Used with `presignPath`.
|
|
91
|
-
- `presignPath` (optional): relative path, default `/api/upload/presigned-url`.
|
|
92
|
-
- `headers` (optional): headers
|
|
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_...`.
|
|
93
107
|
- `withCredentials` (optional): `true` to send cookies.
|
|
94
|
-
- Defaults: same-origin requests to `/api/upload/presigned-url`.
|
|
95
|
-
- `client.getPresignedUrl({ fileName, fileType, fileSize? })`
|
|
108
|
+
- Defaults: same-origin requests to `/api/plugin/upload/presigned-url`.
|
|
109
|
+
- `client.getPresignedUrl({ fileName, fileType, fileSize, projectId?, folderPath? })`
|
|
96
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[]`
|
|
97
114
|
|
|
98
115
|
- `<Uploader />` props
|
|
99
116
|
- `clientOptions` (optional): same as `UpfilesClient` options (only needed for cross-origin or auth customization)
|
|
@@ -101,13 +118,81 @@ async function upload(file) {
|
|
|
101
118
|
- `accept` (array of MIME patterns)
|
|
102
119
|
- `maxFileSize` (bytes, default 100MB)
|
|
103
120
|
- `maxFiles` (default 10)
|
|
121
|
+
- `fetchThumbnails` (boolean): if true, fetch thumbnails after upload via Plugin API
|
|
104
122
|
- `onComplete(files)` and `onError(error)`
|
|
105
123
|
- `className`, `buttonClassName`, `dropzoneClassName`
|
|
106
124
|
- `children`: custom dropzone inner content
|
|
107
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
|
+
|
|
108
187
|
## Notes
|
|
109
188
|
|
|
110
189
|
- Dev with Vite: configure a proxy so `/api/*` maps to your backend (e.g., `http://localhost:4000`), then `<Uploader />` works with no config.
|
|
111
|
-
- Ensure CORS where applicable (S3 bucket CORS;
|
|
190
|
+
- Ensure CORS where applicable (S3 bucket CORS; and set `PLUGIN_ALLOWED_ORIGINS` on your Upfiles app for cross-origin Plugin API calls).
|
|
112
191
|
- For cross-origin with cookies (NextAuth), set `withCredentials: true` and configure your server to allow credentials.
|
|
113
|
-
|
|
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,6 +4,34 @@ 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
37
|
baseUrl?: string;
|
|
@@ -11,6 +39,9 @@ type UpfilesClientOptions = {
|
|
|
11
39
|
presignUrl?: string;
|
|
12
40
|
headers?: Record<string, string>;
|
|
13
41
|
withCredentials?: boolean;
|
|
42
|
+
apiKey?: string;
|
|
43
|
+
apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
|
|
44
|
+
thumbnailsPath?: string;
|
|
14
45
|
};
|
|
15
46
|
declare class UpfilesClient {
|
|
16
47
|
private baseUrl?;
|
|
@@ -18,15 +49,27 @@ declare class UpfilesClient {
|
|
|
18
49
|
private presignUrl?;
|
|
19
50
|
private headers?;
|
|
20
51
|
private withCredentials?;
|
|
52
|
+
private apiKey?;
|
|
53
|
+
private apiKeyHeader;
|
|
54
|
+
private thumbnailsPath;
|
|
21
55
|
constructor(opts: UpfilesClientOptions);
|
|
22
56
|
getPresignedUrl(params: {
|
|
23
57
|
fileName: string;
|
|
24
58
|
fileType: string;
|
|
25
|
-
fileSize
|
|
59
|
+
fileSize: number;
|
|
26
60
|
projectId?: string;
|
|
27
61
|
folderPath?: string;
|
|
28
62
|
}): Promise<PresignResponse>;
|
|
29
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[]>;
|
|
30
73
|
}
|
|
31
74
|
|
|
32
75
|
type UploaderFile = {
|
|
@@ -40,6 +83,8 @@ type UploaderFile = {
|
|
|
40
83
|
error?: string;
|
|
41
84
|
publicUrl?: string;
|
|
42
85
|
fileKey?: string;
|
|
86
|
+
projectId?: string;
|
|
87
|
+
apiKeyId?: string;
|
|
43
88
|
};
|
|
44
89
|
type UploaderProps = {
|
|
45
90
|
clientOptions?: UpfilesClientOptions;
|
|
@@ -55,7 +100,33 @@ type UploaderProps = {
|
|
|
55
100
|
buttonClassName?: string;
|
|
56
101
|
dropzoneClassName?: string;
|
|
57
102
|
children?: React.ReactNode;
|
|
103
|
+
fetchThumbnails?: boolean;
|
|
58
104
|
};
|
|
59
105
|
declare const Uploader: React.FC<UploaderProps>;
|
|
60
106
|
|
|
61
|
-
|
|
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,6 +4,34 @@ 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
37
|
baseUrl?: string;
|
|
@@ -11,6 +39,9 @@ type UpfilesClientOptions = {
|
|
|
11
39
|
presignUrl?: string;
|
|
12
40
|
headers?: Record<string, string>;
|
|
13
41
|
withCredentials?: boolean;
|
|
42
|
+
apiKey?: string;
|
|
43
|
+
apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
|
|
44
|
+
thumbnailsPath?: string;
|
|
14
45
|
};
|
|
15
46
|
declare class UpfilesClient {
|
|
16
47
|
private baseUrl?;
|
|
@@ -18,15 +49,27 @@ declare class UpfilesClient {
|
|
|
18
49
|
private presignUrl?;
|
|
19
50
|
private headers?;
|
|
20
51
|
private withCredentials?;
|
|
52
|
+
private apiKey?;
|
|
53
|
+
private apiKeyHeader;
|
|
54
|
+
private thumbnailsPath;
|
|
21
55
|
constructor(opts: UpfilesClientOptions);
|
|
22
56
|
getPresignedUrl(params: {
|
|
23
57
|
fileName: string;
|
|
24
58
|
fileType: string;
|
|
25
|
-
fileSize
|
|
59
|
+
fileSize: number;
|
|
26
60
|
projectId?: string;
|
|
27
61
|
folderPath?: string;
|
|
28
62
|
}): Promise<PresignResponse>;
|
|
29
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[]>;
|
|
30
73
|
}
|
|
31
74
|
|
|
32
75
|
type UploaderFile = {
|
|
@@ -40,6 +83,8 @@ type UploaderFile = {
|
|
|
40
83
|
error?: string;
|
|
41
84
|
publicUrl?: string;
|
|
42
85
|
fileKey?: string;
|
|
86
|
+
projectId?: string;
|
|
87
|
+
apiKeyId?: string;
|
|
43
88
|
};
|
|
44
89
|
type UploaderProps = {
|
|
45
90
|
clientOptions?: UpfilesClientOptions;
|
|
@@ -55,7 +100,33 @@ type UploaderProps = {
|
|
|
55
100
|
buttonClassName?: string;
|
|
56
101
|
dropzoneClassName?: string;
|
|
57
102
|
children?: React.ReactNode;
|
|
103
|
+
fetchThumbnails?: boolean;
|
|
58
104
|
};
|
|
59
105
|
declare const Uploader: React.FC<UploaderProps>;
|
|
60
106
|
|
|
61
|
-
|
|
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
|
});
|
|
@@ -29,19 +30,35 @@ module.exports = __toCommonJS(index_exports);
|
|
|
29
30
|
var UpfilesClient = class {
|
|
30
31
|
constructor(opts) {
|
|
31
32
|
this.baseUrl = opts.baseUrl?.replace(/\/$/, "");
|
|
32
|
-
this.presignPath = opts.presignPath ?? "/api/upload/presigned-url";
|
|
33
|
+
this.presignPath = opts.presignPath ?? "/api/plugin/upload/presigned-url";
|
|
33
34
|
this.presignUrl = opts.presignUrl;
|
|
34
35
|
this.headers = opts.headers;
|
|
35
36
|
this.withCredentials = opts.withCredentials;
|
|
37
|
+
this.apiKey = opts.apiKey;
|
|
38
|
+
this.apiKeyHeader = opts.apiKeyHeader ?? "authorization";
|
|
39
|
+
this.thumbnailsPath = opts.thumbnailsPath ?? "/api/plugin/thumbnails";
|
|
36
40
|
}
|
|
37
41
|
async getPresignedUrl(params) {
|
|
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");
|
|
38
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
|
+
}
|
|
39
59
|
const res = await fetch(target, {
|
|
40
60
|
method: "POST",
|
|
41
|
-
headers
|
|
42
|
-
"Content-Type": "application/json",
|
|
43
|
-
...this.headers || {}
|
|
44
|
-
},
|
|
61
|
+
headers,
|
|
45
62
|
credentials: this.withCredentials ? "include" : "same-origin",
|
|
46
63
|
body: JSON.stringify(params)
|
|
47
64
|
});
|
|
@@ -65,6 +82,92 @@ var UpfilesClient = class {
|
|
|
65
82
|
}
|
|
66
83
|
});
|
|
67
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
|
+
}
|
|
68
171
|
};
|
|
69
172
|
|
|
70
173
|
// src/Uploader.tsx
|
|
@@ -83,7 +186,8 @@ var Uploader = ({
|
|
|
83
186
|
dropzoneClassName,
|
|
84
187
|
children,
|
|
85
188
|
projectId,
|
|
86
|
-
folderPath
|
|
189
|
+
folderPath,
|
|
190
|
+
fetchThumbnails
|
|
87
191
|
}) => {
|
|
88
192
|
const inputRef = (0, import_react.useRef)(null);
|
|
89
193
|
const client = (0, import_react.useMemo)(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
|
|
@@ -153,14 +257,31 @@ var Uploader = ({
|
|
|
153
257
|
xhr.setRequestHeader("Content-Type", item.type || "application/octet-stream");
|
|
154
258
|
xhr.send(item.file);
|
|
155
259
|
});
|
|
260
|
+
let thumbs;
|
|
261
|
+
try {
|
|
262
|
+
if (fetchThumbnails) {
|
|
263
|
+
thumbs = await client.getThumbnails(presign.fileKey);
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
156
267
|
update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
|
|
157
|
-
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
|
+
};
|
|
158
279
|
} catch (err) {
|
|
159
280
|
const message = err instanceof Error ? err.message : "Upload failed";
|
|
160
281
|
update({ status: "error", error: message });
|
|
161
282
|
throw err;
|
|
162
283
|
}
|
|
163
|
-
}, [client]);
|
|
284
|
+
}, [client, projectId, folderPath, fetchThumbnails]);
|
|
164
285
|
const uploadAll = (0, import_react.useCallback)(async () => {
|
|
165
286
|
const targets = files.filter((f) => f.status === "pending");
|
|
166
287
|
if (!targets.length) return;
|
|
@@ -261,8 +382,152 @@ var Uploader = ({
|
|
|
261
382
|
] })
|
|
262
383
|
] });
|
|
263
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
|
+
};
|
|
264
528
|
// Annotate the CommonJS export names for ESM import in node:
|
|
265
529
|
0 && (module.exports = {
|
|
530
|
+
ProjectFilesWidget,
|
|
266
531
|
UpfilesClient,
|
|
267
532
|
Uploader
|
|
268
533
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -2,19 +2,35 @@
|
|
|
2
2
|
var UpfilesClient = class {
|
|
3
3
|
constructor(opts) {
|
|
4
4
|
this.baseUrl = opts.baseUrl?.replace(/\/$/, "");
|
|
5
|
-
this.presignPath = opts.presignPath ?? "/api/upload/presigned-url";
|
|
5
|
+
this.presignPath = opts.presignPath ?? "/api/plugin/upload/presigned-url";
|
|
6
6
|
this.presignUrl = opts.presignUrl;
|
|
7
7
|
this.headers = opts.headers;
|
|
8
8
|
this.withCredentials = opts.withCredentials;
|
|
9
|
+
this.apiKey = opts.apiKey;
|
|
10
|
+
this.apiKeyHeader = opts.apiKeyHeader ?? "authorization";
|
|
11
|
+
this.thumbnailsPath = opts.thumbnailsPath ?? "/api/plugin/thumbnails";
|
|
9
12
|
}
|
|
10
13
|
async getPresignedUrl(params) {
|
|
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");
|
|
11
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
|
+
}
|
|
12
31
|
const res = await fetch(target, {
|
|
13
32
|
method: "POST",
|
|
14
|
-
headers
|
|
15
|
-
"Content-Type": "application/json",
|
|
16
|
-
...this.headers || {}
|
|
17
|
-
},
|
|
33
|
+
headers,
|
|
18
34
|
credentials: this.withCredentials ? "include" : "same-origin",
|
|
19
35
|
body: JSON.stringify(params)
|
|
20
36
|
});
|
|
@@ -38,6 +54,92 @@ var UpfilesClient = class {
|
|
|
38
54
|
}
|
|
39
55
|
});
|
|
40
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
|
+
}
|
|
41
143
|
};
|
|
42
144
|
|
|
43
145
|
// src/Uploader.tsx
|
|
@@ -56,7 +158,8 @@ var Uploader = ({
|
|
|
56
158
|
dropzoneClassName,
|
|
57
159
|
children,
|
|
58
160
|
projectId,
|
|
59
|
-
folderPath
|
|
161
|
+
folderPath,
|
|
162
|
+
fetchThumbnails
|
|
60
163
|
}) => {
|
|
61
164
|
const inputRef = useRef(null);
|
|
62
165
|
const client = useMemo(() => new UpfilesClient(clientOptions ?? {}), [clientOptions]);
|
|
@@ -126,14 +229,31 @@ var Uploader = ({
|
|
|
126
229
|
xhr.setRequestHeader("Content-Type", item.type || "application/octet-stream");
|
|
127
230
|
xhr.send(item.file);
|
|
128
231
|
});
|
|
232
|
+
let thumbs;
|
|
233
|
+
try {
|
|
234
|
+
if (fetchThumbnails) {
|
|
235
|
+
thumbs = await client.getThumbnails(presign.fileKey);
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
129
239
|
update({ status: "success", progress: 100, fileKey: presign.fileKey, publicUrl: presign.publicUrl });
|
|
130
|
-
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
|
+
};
|
|
131
251
|
} catch (err) {
|
|
132
252
|
const message = err instanceof Error ? err.message : "Upload failed";
|
|
133
253
|
update({ status: "error", error: message });
|
|
134
254
|
throw err;
|
|
135
255
|
}
|
|
136
|
-
}, [client]);
|
|
256
|
+
}, [client, projectId, folderPath, fetchThumbnails]);
|
|
137
257
|
const uploadAll = useCallback(async () => {
|
|
138
258
|
const targets = files.filter((f) => f.status === "pending");
|
|
139
259
|
if (!targets.length) return;
|
|
@@ -234,7 +354,151 @@ var Uploader = ({
|
|
|
234
354
|
] })
|
|
235
355
|
] });
|
|
236
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
|
+
};
|
|
237
500
|
export {
|
|
501
|
+
ProjectFilesWidget,
|
|
238
502
|
UpfilesClient,
|
|
239
503
|
Uploader
|
|
240
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",
|
|
4
|
+
"description": "Lightweight client and React components for Upfiles Plugin API (presigned S3)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "UpFiles",
|
|
7
7
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
30
30
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
31
31
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
32
|
-
"prepublishOnly": "
|
|
32
|
+
"prepublishOnly": "bun run build"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": ">=18"
|