fluxfiles 1.22.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/LICENSE +21 -0
- package/README.md +638 -0
- package/fluxfiles.d.ts +53 -0
- package/fluxfiles.js +159 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thai-pc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
# FluxFiles
|
|
2
|
+
|
|
3
|
+
A standalone, embeddable file manager built with PHP 7.4+. Multi-storage support (Local, AWS S3, Cloudflare R2), JWT authentication, and a zero-build-step frontend powered by Alpine.js.
|
|
4
|
+
|
|
5
|
+
Drop it into any web app via iframe + SDK, or use the provided adapters for **Laravel**, **WordPress**, **React**, and **Vue / Nuxt**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Multi-storage** — Local disk, AWS S3, Cloudflare R2 via Flysystem v3
|
|
12
|
+
- **JWT authentication** — HS256 tokens with granular claims (permissions, disk access, path scoping, upload limits, file type whitelist, storage quota)
|
|
13
|
+
- **File operations** — Upload, download, move, copy, rename, delete, create folders
|
|
14
|
+
- **Cross-disk operations** — Copy/move files between different storage backends
|
|
15
|
+
- **Image optimization** — Auto-generates WebP variants (thumb 150px, medium 768px, large 1920px) on upload
|
|
16
|
+
- **Image crop** — Inline crop tool with aspect ratio presets
|
|
17
|
+
- **AI auto-tag** — Claude or OpenAI vision API integration for automatic image tagging, alt text, and captions
|
|
18
|
+
- **Chunk upload** — S3 multipart upload for large files (>10 MB)
|
|
19
|
+
- **Trash / soft delete** — Recoverable deletes with configurable auto-purge
|
|
20
|
+
- **Full-text search** — SQLite FTS5 across file names, titles, alt text, captions, and tags
|
|
21
|
+
- **SEO metadata** — Title, alt text, caption per file (synced to S3 object tags)
|
|
22
|
+
- **Duplicate detection** — MD5 hash check on upload
|
|
23
|
+
- **Rate limiting** — Token bucket per user (60 reads, 10 writes per minute)
|
|
24
|
+
- **Audit log** — All write actions logged with user, IP, and user agent
|
|
25
|
+
- **Storage quota** — Per-user storage limits enforced server-side
|
|
26
|
+
- **Dark mode** — Automatic theme detection with manual toggle
|
|
27
|
+
- **i18n** — 16 languages (EN, VI, ZH, JA, KO, FR, DE, ES, AR, PT, IT, RU, TH, HI, TR, NL) with RTL support
|
|
28
|
+
- **Bulk operations** — Multi-select with bulk move, copy, delete, download
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- PHP >= 7.4
|
|
35
|
+
- Extensions: `pdo`, `pdo_sqlite`, `gd` (for image processing), `curl` (for AI tagging)
|
|
36
|
+
- Composer
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/thai-pc/fluxfiles.git fluxfiles
|
|
46
|
+
cd fluxfiles
|
|
47
|
+
composer install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Configure
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cp .env.example .env
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Edit `.env`:
|
|
57
|
+
|
|
58
|
+
```env
|
|
59
|
+
# REQUIRED — generate a random 32+ character string
|
|
60
|
+
FLUXFILES_SECRET=your-random-secret-key-here
|
|
61
|
+
|
|
62
|
+
# Allowed origins for CORS (comma-separated)
|
|
63
|
+
FLUXFILES_ALLOWED_ORIGINS=http://localhost:3000,https://yourapp.com
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Run
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Development server
|
|
70
|
+
php -S localhost:8080 -t .
|
|
71
|
+
|
|
72
|
+
# The file manager UI is at:
|
|
73
|
+
# http://localhost:8080/public/index.html
|
|
74
|
+
|
|
75
|
+
# The API endpoint is:
|
|
76
|
+
# http://localhost:8080/api/
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For production, point your web server (Nginx/Apache) to the project root.
|
|
80
|
+
|
|
81
|
+
### 4. Generate a token
|
|
82
|
+
|
|
83
|
+
In your host application, generate a JWT token to authenticate users:
|
|
84
|
+
|
|
85
|
+
```php
|
|
86
|
+
require_once 'path/to/fluxfiles/embed.php';
|
|
87
|
+
|
|
88
|
+
$token = fluxfiles_token(
|
|
89
|
+
userId: 'user-123',
|
|
90
|
+
perms: ['read', 'write', 'delete'],
|
|
91
|
+
disks: ['local', 's3'],
|
|
92
|
+
prefix: 'user-123/', // scope to user directory
|
|
93
|
+
maxUploadMb: 10,
|
|
94
|
+
allowedExt: null, // null = allow all
|
|
95
|
+
ttl: 3600 // 1 hour
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Embedding in Your App
|
|
102
|
+
|
|
103
|
+
### JavaScript SDK
|
|
104
|
+
|
|
105
|
+
Include `fluxfiles.js` in your page:
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<script src="https://your-fluxfiles-host/fluxfiles.js"></script>
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
FluxFiles.open({
|
|
112
|
+
endpoint: 'https://your-fluxfiles-host',
|
|
113
|
+
token: 'eyJhbGci...',
|
|
114
|
+
disk: 'local',
|
|
115
|
+
mode: 'picker', // 'picker' (select file) or 'browser' (free browse)
|
|
116
|
+
locale: 'en', // optional — auto-detects if omitted
|
|
117
|
+
allowedTypes: ['image/*', '.pdf'], // optional file type filter
|
|
118
|
+
maxSize: 10485760, // optional max size in bytes
|
|
119
|
+
container: '#my-div', // optional — omit for modal overlay
|
|
120
|
+
onSelect: function(file) {
|
|
121
|
+
console.log('Selected:', file.url, file.path);
|
|
122
|
+
},
|
|
123
|
+
onClose: function() {
|
|
124
|
+
console.log('Closed');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
</script>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### SDK Commands
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
FluxFiles.navigate('/photos/2024');
|
|
134
|
+
FluxFiles.setDisk('s3');
|
|
135
|
+
FluxFiles.refresh();
|
|
136
|
+
FluxFiles.search('invoice');
|
|
137
|
+
FluxFiles.crossCopy('s3', 'backups/');
|
|
138
|
+
FluxFiles.crossMove('r2', 'archive/');
|
|
139
|
+
FluxFiles.aiTag();
|
|
140
|
+
FluxFiles.close();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### SDK Events
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
FluxFiles.on('FM_READY', function(payload) { /* iframe loaded */ });
|
|
147
|
+
FluxFiles.on('FM_SELECT', function(file) { /* file selected */ });
|
|
148
|
+
FluxFiles.on('FM_EVENT', function(event) {
|
|
149
|
+
// event.action: 'upload', 'delete', 'move', 'copy', 'mkdir',
|
|
150
|
+
// 'restore', 'purge', 'trash', 'crop', 'ai_tag'
|
|
151
|
+
});
|
|
152
|
+
FluxFiles.on('FM_CLOSE', function() { /* closed */ });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### PHP Embed Helper
|
|
156
|
+
|
|
157
|
+
```php
|
|
158
|
+
require_once 'path/to/fluxfiles/embed.php';
|
|
159
|
+
|
|
160
|
+
echo fluxfiles_embed(
|
|
161
|
+
endpoint: 'https://your-fluxfiles-host',
|
|
162
|
+
token: $token,
|
|
163
|
+
disk: 'local',
|
|
164
|
+
mode: 'picker',
|
|
165
|
+
width: '100%',
|
|
166
|
+
height: '600px'
|
|
167
|
+
);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Environment Variables
|
|
173
|
+
|
|
174
|
+
| Variable | Required | Default | Description |
|
|
175
|
+
|----------|----------|---------|-------------|
|
|
176
|
+
| `FLUXFILES_SECRET` | Yes | — | JWT signing secret (32+ chars) |
|
|
177
|
+
| `FLUXFILES_ALLOWED_ORIGINS` | Yes | — | Comma-separated CORS origins |
|
|
178
|
+
| `FLUXFILES_LOCALE` | No | auto-detect | UI language (`en`, `vi`, `zh`, `ja`, `ko`, `fr`, `de`, `es`, `ar`, `pt`, `it`, `ru`, `th`, `hi`, `tr`, `nl`) |
|
|
179
|
+
| `AWS_ACCESS_KEY_ID` | No | — | AWS S3 access key |
|
|
180
|
+
| `AWS_SECRET_ACCESS_KEY` | No | — | AWS S3 secret key |
|
|
181
|
+
| `AWS_DEFAULT_REGION` | No | `ap-southeast-1` | AWS region |
|
|
182
|
+
| `AWS_BUCKET` | No | — | S3 bucket name |
|
|
183
|
+
| `R2_ACCESS_KEY_ID` | No | — | Cloudflare R2 access key |
|
|
184
|
+
| `R2_SECRET_ACCESS_KEY` | No | — | Cloudflare R2 secret key |
|
|
185
|
+
| `R2_ACCOUNT_ID` | No | — | Cloudflare account ID |
|
|
186
|
+
| `R2_BUCKET` | No | — | R2 bucket name |
|
|
187
|
+
| `FLUXFILES_AI_PROVIDER` | No | — | `claude` or `openai` (empty = disabled) |
|
|
188
|
+
| `FLUXFILES_AI_API_KEY` | No | — | API key for AI provider |
|
|
189
|
+
| `FLUXFILES_AI_MODEL` | No | auto | Override AI model (e.g. `gpt-4o`, `claude-sonnet-4-20250514`) |
|
|
190
|
+
| `FLUXFILES_AI_AUTO_TAG` | No | `false` | Auto-tag images on upload |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Storage Disks
|
|
195
|
+
|
|
196
|
+
Configured in `config/disks.php`. Three drivers are provided out of the box:
|
|
197
|
+
|
|
198
|
+
```php
|
|
199
|
+
// Local filesystem
|
|
200
|
+
'local' => [
|
|
201
|
+
'driver' => 'local',
|
|
202
|
+
'root' => __DIR__ . '/../storage/uploads',
|
|
203
|
+
'url' => '/storage/uploads',
|
|
204
|
+
],
|
|
205
|
+
|
|
206
|
+
// AWS S3
|
|
207
|
+
's3' => [
|
|
208
|
+
'driver' => 's3',
|
|
209
|
+
'region' => env('AWS_DEFAULT_REGION'),
|
|
210
|
+
'bucket' => env('AWS_BUCKET'),
|
|
211
|
+
'key' => env('AWS_ACCESS_KEY_ID'),
|
|
212
|
+
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
|
213
|
+
],
|
|
214
|
+
|
|
215
|
+
// Cloudflare R2
|
|
216
|
+
'r2' => [
|
|
217
|
+
'driver' => 's3',
|
|
218
|
+
'endpoint' => 'https://' . env('R2_ACCOUNT_ID') . '.r2.cloudflarestorage.com',
|
|
219
|
+
'region' => 'auto',
|
|
220
|
+
'bucket' => env('R2_BUCKET'),
|
|
221
|
+
'key' => env('R2_ACCESS_KEY_ID'),
|
|
222
|
+
'secret' => env('R2_SECRET_ACCESS_KEY'),
|
|
223
|
+
],
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## JWT Token Structure
|
|
229
|
+
|
|
230
|
+
Tokens are signed with HS256. Claims control what each user can do:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"sub": "user-123",
|
|
235
|
+
"iat": 1710500000,
|
|
236
|
+
"exp": 1710503600,
|
|
237
|
+
"jti": "a1b2c3d4e5f6",
|
|
238
|
+
"perms": ["read", "write", "delete"],
|
|
239
|
+
"disks": ["local", "s3"],
|
|
240
|
+
"prefix": "user-123/",
|
|
241
|
+
"max_upload": 10,
|
|
242
|
+
"allowed_ext": ["jpg", "png", "pdf"],
|
|
243
|
+
"max_storage": 1000
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| Claim | Type | Description |
|
|
248
|
+
|-------|------|-------------|
|
|
249
|
+
| `sub` | string | User identifier |
|
|
250
|
+
| `perms` | string[] | Permissions: `read`, `write`, `delete` |
|
|
251
|
+
| `disks` | string[] | Allowed storage disks |
|
|
252
|
+
| `prefix` | string | Path prefix scope (e.g. `user-123/` restricts to that directory) |
|
|
253
|
+
| `max_upload` | int | Maximum upload size in MB |
|
|
254
|
+
| `allowed_ext` | string[]|null | Allowed file extensions (`null` = any) |
|
|
255
|
+
| `max_storage` | int | Storage quota in MB (`0` = unlimited) |
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## API Endpoints
|
|
260
|
+
|
|
261
|
+
Base path: `/api/fm/`
|
|
262
|
+
|
|
263
|
+
### Public (no auth)
|
|
264
|
+
|
|
265
|
+
| Method | Path | Description |
|
|
266
|
+
|--------|------|-------------|
|
|
267
|
+
| `GET` | `/lang` | List available locales |
|
|
268
|
+
| `GET` | `/lang/{code}` | Get translation messages for a locale |
|
|
269
|
+
|
|
270
|
+
### File Operations (JWT required)
|
|
271
|
+
|
|
272
|
+
| Method | Path | Description |
|
|
273
|
+
|--------|------|-------------|
|
|
274
|
+
| `GET` | `/list?disk=&path=` | List directory contents |
|
|
275
|
+
| `POST` | `/upload` | Upload file (multipart form) |
|
|
276
|
+
| `DELETE` | `/delete` | Soft delete (move to trash) |
|
|
277
|
+
| `POST` | `/move` | Move file/folder |
|
|
278
|
+
| `POST` | `/copy` | Copy file/folder |
|
|
279
|
+
| `POST` | `/mkdir` | Create directory |
|
|
280
|
+
| `POST` | `/cross-copy` | Copy between disks |
|
|
281
|
+
| `POST` | `/cross-move` | Move between disks |
|
|
282
|
+
| `POST` | `/presign` | Generate presigned URL |
|
|
283
|
+
| `POST` | `/crop` | Crop image |
|
|
284
|
+
| `POST` | `/ai-tag` | AI-tag an image |
|
|
285
|
+
|
|
286
|
+
### Metadata
|
|
287
|
+
|
|
288
|
+
| Method | Path | Description |
|
|
289
|
+
|--------|------|-------------|
|
|
290
|
+
| `GET` | `/meta?disk=&path=` | File info (size, mime, modified, variants) |
|
|
291
|
+
| `GET` | `/metadata?disk=&key=` | Get SEO metadata |
|
|
292
|
+
| `PUT` | `/metadata` | Save title, alt_text, caption, tags |
|
|
293
|
+
| `DELETE` | `/metadata` | Delete metadata |
|
|
294
|
+
|
|
295
|
+
### Trash
|
|
296
|
+
|
|
297
|
+
| Method | Path | Description |
|
|
298
|
+
|--------|------|-------------|
|
|
299
|
+
| `GET` | `/trash?disk=` | List trashed files |
|
|
300
|
+
| `POST` | `/restore` | Restore from trash |
|
|
301
|
+
| `DELETE` | `/purge` | Permanently delete |
|
|
302
|
+
|
|
303
|
+
### Search, Quota, Audit
|
|
304
|
+
|
|
305
|
+
| Method | Path | Description |
|
|
306
|
+
|--------|------|-------------|
|
|
307
|
+
| `GET` | `/search?disk=&q=&limit=` | Full-text search |
|
|
308
|
+
| `GET` | `/quota?disk=` | Storage usage |
|
|
309
|
+
| `GET` | `/audit?limit=&offset=&user_id=` | Audit log |
|
|
310
|
+
|
|
311
|
+
### Chunk Upload (S3 multipart)
|
|
312
|
+
|
|
313
|
+
| Method | Path | Description |
|
|
314
|
+
|--------|------|-------------|
|
|
315
|
+
| `POST` | `/chunk/init` | Initiate multipart upload |
|
|
316
|
+
| `POST` | `/chunk/presign` | Get presigned URL for a part |
|
|
317
|
+
| `POST` | `/chunk/complete` | Complete multipart upload |
|
|
318
|
+
| `POST` | `/chunk/abort` | Abort multipart upload |
|
|
319
|
+
|
|
320
|
+
All responses follow the format:
|
|
321
|
+
```json
|
|
322
|
+
{ "data": { ... }, "error": null }
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Framework Adapters
|
|
328
|
+
|
|
329
|
+
### Laravel
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# In your Laravel project
|
|
333
|
+
composer require fluxfiles/laravel
|
|
334
|
+
|
|
335
|
+
# Publish config
|
|
336
|
+
php artisan vendor:publish --tag=fluxfiles-config
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Blade component:**
|
|
340
|
+
```blade
|
|
341
|
+
<x-fluxfiles
|
|
342
|
+
disk="local"
|
|
343
|
+
mode="picker"
|
|
344
|
+
width="100%"
|
|
345
|
+
height="600px"
|
|
346
|
+
@select="handleFileSelect"
|
|
347
|
+
/>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Generate token:**
|
|
351
|
+
```php
|
|
352
|
+
use FluxFiles\Laravel\FluxFilesFacade as FluxFiles;
|
|
353
|
+
|
|
354
|
+
$token = FluxFiles::token(
|
|
355
|
+
userId: auth()->id(),
|
|
356
|
+
perms: ['read', 'write'],
|
|
357
|
+
disks: ['local', 's3']
|
|
358
|
+
);
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Config: `config/fluxfiles.php`
|
|
362
|
+
|
|
363
|
+
### WordPress
|
|
364
|
+
|
|
365
|
+
1. Copy `adapters/wordpress/` to `wp-content/plugins/fluxfiles/`
|
|
366
|
+
2. Activate the plugin in WP Admin
|
|
367
|
+
3. Configure at **Settings > FluxFiles**
|
|
368
|
+
|
|
369
|
+
**Shortcode:**
|
|
370
|
+
```
|
|
371
|
+
[fluxfiles disk="local" mode="browser" height="600px"]
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Media button:** A "FluxFiles" button is automatically added to the classic editor toolbar.
|
|
375
|
+
|
|
376
|
+
### React
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
npm install @fluxfiles/react
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Picker component:**
|
|
383
|
+
```tsx
|
|
384
|
+
import { FluxFilesModal } from '@fluxfiles/react';
|
|
385
|
+
|
|
386
|
+
function App() {
|
|
387
|
+
const [open, setOpen] = useState(false);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<FluxFilesModal
|
|
391
|
+
open={open}
|
|
392
|
+
endpoint="https://your-fluxfiles-host"
|
|
393
|
+
token={token}
|
|
394
|
+
disk="local"
|
|
395
|
+
locale="en"
|
|
396
|
+
onSelect={(file) => console.log(file)}
|
|
397
|
+
onClose={() => setOpen(false)}
|
|
398
|
+
/>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Hook for full control:**
|
|
404
|
+
```tsx
|
|
405
|
+
import { useFluxFiles } from '@fluxfiles/react';
|
|
406
|
+
|
|
407
|
+
const { iframeRef, iframeSrc, navigate, setDisk, refresh, search, aiTag } =
|
|
408
|
+
useFluxFiles({
|
|
409
|
+
endpoint: 'https://your-fluxfiles-host',
|
|
410
|
+
token,
|
|
411
|
+
onSelect: (file) => console.log(file),
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Vue / Nuxt
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
npm install @fluxfiles/vue
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Modal component:**
|
|
422
|
+
```vue
|
|
423
|
+
<script setup>
|
|
424
|
+
import { ref } from 'vue';
|
|
425
|
+
import { FluxFilesModal } from '@fluxfiles/vue';
|
|
426
|
+
|
|
427
|
+
const open = ref(false);
|
|
428
|
+
|
|
429
|
+
function onSelect(file) {
|
|
430
|
+
console.log(file);
|
|
431
|
+
open.value = false;
|
|
432
|
+
}
|
|
433
|
+
</script>
|
|
434
|
+
|
|
435
|
+
<template>
|
|
436
|
+
<button @click="open = true">Pick file</button>
|
|
437
|
+
|
|
438
|
+
<FluxFilesModal
|
|
439
|
+
v-model:open="open"
|
|
440
|
+
endpoint="https://your-fluxfiles-host"
|
|
441
|
+
:token="token"
|
|
442
|
+
disk="local"
|
|
443
|
+
locale="en"
|
|
444
|
+
@select="onSelect"
|
|
445
|
+
@close="open = false"
|
|
446
|
+
/>
|
|
447
|
+
</template>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Embedded component:**
|
|
451
|
+
```vue
|
|
452
|
+
<script setup>
|
|
453
|
+
import { ref } from 'vue';
|
|
454
|
+
import { FluxFiles } from '@fluxfiles/vue';
|
|
455
|
+
|
|
456
|
+
const fm = ref();
|
|
457
|
+
|
|
458
|
+
// Programmatic control:
|
|
459
|
+
// fm.value?.navigate('/uploads');
|
|
460
|
+
// fm.value?.setDisk('s3');
|
|
461
|
+
// fm.value?.refresh();
|
|
462
|
+
</script>
|
|
463
|
+
|
|
464
|
+
<template>
|
|
465
|
+
<FluxFiles
|
|
466
|
+
ref="fm"
|
|
467
|
+
endpoint="https://your-fluxfiles-host"
|
|
468
|
+
:token="token"
|
|
469
|
+
disk="local"
|
|
470
|
+
width="100%"
|
|
471
|
+
height="600px"
|
|
472
|
+
@select="(file) => console.log(file)"
|
|
473
|
+
/>
|
|
474
|
+
</template>
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Composable for full control:**
|
|
478
|
+
```ts
|
|
479
|
+
import { useFluxFiles } from '@fluxfiles/vue';
|
|
480
|
+
|
|
481
|
+
const { iframeRef, iframeSrc, navigate, setDisk, refresh, search, aiTag } =
|
|
482
|
+
useFluxFiles({
|
|
483
|
+
endpoint: 'https://your-fluxfiles-host',
|
|
484
|
+
token,
|
|
485
|
+
onSelect: (file) => console.log(file),
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Nuxt 3 auto-import:** Add the plugin to your `nuxt.config.ts`:
|
|
490
|
+
```ts
|
|
491
|
+
export default defineNuxtConfig({
|
|
492
|
+
plugins: ['@fluxfiles/vue/nuxt'],
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Components `<FluxFiles>` and `<FluxFilesModal>` are then globally available without explicit imports.
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Internationalization
|
|
501
|
+
|
|
502
|
+
16 languages included. Translation files are in `lang/`.
|
|
503
|
+
|
|
504
|
+
| Code | Language | Direction |
|
|
505
|
+
|------|----------|-----------|
|
|
506
|
+
| `en` | English | LTR |
|
|
507
|
+
| `vi` | Tieng Viet | LTR |
|
|
508
|
+
| `zh` | Chinese | LTR |
|
|
509
|
+
| `ja` | Japanese | LTR |
|
|
510
|
+
| `ko` | Korean | LTR |
|
|
511
|
+
| `fr` | Francais | LTR |
|
|
512
|
+
| `de` | Deutsch | LTR |
|
|
513
|
+
| `es` | Espanol | LTR |
|
|
514
|
+
| `pt` | Portugues | LTR |
|
|
515
|
+
| `ar` | Arabic | RTL |
|
|
516
|
+
| `it` | Italiano | LTR |
|
|
517
|
+
| `ru` | Русский | LTR |
|
|
518
|
+
| `th` | ไทย | LTR |
|
|
519
|
+
| `hi` | हिन्दी | LTR |
|
|
520
|
+
| `tr` | Türkçe | LTR |
|
|
521
|
+
| `nl` | Nederlands | LTR |
|
|
522
|
+
|
|
523
|
+
**Set locale via SDK:**
|
|
524
|
+
```js
|
|
525
|
+
FluxFiles.open({ locale: 'ar', ... });
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Set locale via env:**
|
|
529
|
+
```env
|
|
530
|
+
FLUXFILES_LOCALE=vi
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Auto-detection order:** FM_CONFIG locale > `?lang=` query param > `Accept-Language` header > `en`
|
|
534
|
+
|
|
535
|
+
**Adding a new language:** See [`lang/CONTRIBUTING.md`](lang/CONTRIBUTING.md).
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Project Structure
|
|
540
|
+
|
|
541
|
+
```
|
|
542
|
+
FluxFiles/
|
|
543
|
+
├── api/ # PHP backend
|
|
544
|
+
│ ├── index.php # Main router (CORS, auth, routing)
|
|
545
|
+
│ ├── FileManager.php # Core file operations
|
|
546
|
+
│ ├── MetadataRepository.php # SQLite CRUD + FTS5 search
|
|
547
|
+
│ ├── DiskManager.php # Flysystem factory (local/s3/r2)
|
|
548
|
+
│ ├── Claims.php # JWT claims value object
|
|
549
|
+
│ ├── JwtMiddleware.php # JWT extraction + verification
|
|
550
|
+
│ ├── ImageOptimizer.php # Resize + WebP variant generation
|
|
551
|
+
│ ├── AiTagger.php # Claude/OpenAI vision integration
|
|
552
|
+
│ ├── ChunkUploader.php # S3 multipart upload
|
|
553
|
+
│ ├── RateLimiter.php # Token bucket rate limiting
|
|
554
|
+
│ ├── AuditLog.php # Write action logging
|
|
555
|
+
│ ├── QuotaManager.php # Storage quota enforcement
|
|
556
|
+
│ ├── I18n.php # Internationalization
|
|
557
|
+
│ └── ApiException.php # HTTP error exceptions
|
|
558
|
+
├── assets/
|
|
559
|
+
│ ├── fm.js # Alpine.js UI component
|
|
560
|
+
│ └── fm.css # Styles (dark mode, RTL)
|
|
561
|
+
├── config/
|
|
562
|
+
│ └── disks.php # Storage disk definitions
|
|
563
|
+
├── lang/ # Translation JSON files
|
|
564
|
+
├── public/
|
|
565
|
+
│ └── index.html # Iframe entry point
|
|
566
|
+
├── storage/ # SQLite DB + local uploads
|
|
567
|
+
├── adapters/
|
|
568
|
+
│ ├── laravel/ # Laravel package
|
|
569
|
+
│ ├── wordpress/ # WordPress plugin
|
|
570
|
+
│ ├── react/ # React component library
|
|
571
|
+
│ └── vue/ # Vue 3 / Nuxt 3 component library
|
|
572
|
+
├── fluxfiles.js # Host app SDK (UMD)
|
|
573
|
+
├── fluxfiles.d.ts # TypeScript declarations for SDK
|
|
574
|
+
├── embed.php # PHP helper (token + embed)
|
|
575
|
+
├── composer.json
|
|
576
|
+
├── package.json # npm package for SDK (CDN access)
|
|
577
|
+
├── scripts/
|
|
578
|
+
│ └── build-wordpress.sh # Bundle WordPress plugin to ZIP
|
|
579
|
+
└── .env.example
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Security
|
|
585
|
+
|
|
586
|
+
- **JWT HS256** — All API requests require a signed token
|
|
587
|
+
- **CORS whitelist** — Only specified origins can access the API
|
|
588
|
+
- **Path scoping** — Users can be restricted to a directory prefix via `prefix` claim
|
|
589
|
+
- **Permission model** — Granular `read`, `write`, `delete` permissions per token
|
|
590
|
+
- **Disk whitelist** — Per-token disk access control
|
|
591
|
+
- **File type restrictions** — Optional extension whitelist per token
|
|
592
|
+
- **Rate limiting** — Token bucket algorithm prevents abuse
|
|
593
|
+
- **Quota enforcement** — Per-user storage limits
|
|
594
|
+
- **Soft delete** — Files go to trash before permanent deletion
|
|
595
|
+
- **Audit trail** — All write actions are logged
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## Fork / Customize
|
|
600
|
+
|
|
601
|
+
If you fork FluxFiles, the table below lists the key files you'll need to review and modify:
|
|
602
|
+
|
|
603
|
+
### Files to Change
|
|
604
|
+
|
|
605
|
+
| Category | File(s) | What to Change |
|
|
606
|
+
|----------|---------|----------------|
|
|
607
|
+
| **Secrets & CORS** | `.env` | `FLUXFILES_SECRET`, `FLUXFILES_ALLOWED_ORIGINS` — generate your own secret and set your domains |
|
|
608
|
+
| **Storage drivers** | `config/disks.php` | Add, remove, or reconfigure disk definitions (local / S3 / R2) |
|
|
609
|
+
| **Cloud credentials** | `.env` | `AWS_*` and `R2_*` variables for your own buckets |
|
|
610
|
+
| **AI tagging** | `.env` | `FLUXFILES_AI_PROVIDER`, `FLUXFILES_AI_API_KEY`, `FLUXFILES_AI_MODEL` |
|
|
611
|
+
| **Branding — colors** | `assets/fm.css` | CSS custom properties (`--ff-primary`, `--ff-bg`, `--ff-text`, etc.) |
|
|
612
|
+
| **Branding — title** | `public/index.html` | `<title>` tag and any visible product name |
|
|
613
|
+
| **Frontend logic** | `assets/fm.js` | Alpine.js component — add features or change UI behavior |
|
|
614
|
+
| **SDK** | `fluxfiles.js` | Event names, default options, iframe communication protocol |
|
|
615
|
+
| **Token helper** | `embed.php` | Default TTL, claims, or signing algorithm |
|
|
616
|
+
| **Laravel adapter** | `adapters/laravel/config/fluxfiles.php` | Endpoint, default disks, mode, AI settings |
|
|
617
|
+
| **WordPress adapter** | `adapters/wordpress/fluxfiles.php` | Plugin header (name, author, URI) |
|
|
618
|
+
| **React adapter** | `adapters/react/package.json` | Package name, author, repository URL |
|
|
619
|
+
| **Vue adapter** | `adapters/vue/package.json` | Package name, author, repository URL |
|
|
620
|
+
| **Translations** | `lang/*.json` | Edit existing strings or add a new locale (see `lang/CONTRIBUTING.md`) |
|
|
621
|
+
| **Rate limits** | `api/RateLimiter.php` | Bucket size and refill rate constants |
|
|
622
|
+
| **Image variants** | `api/ImageOptimizer.php` | Thumbnail / medium / large dimensions and quality |
|
|
623
|
+
|
|
624
|
+
### Attribution
|
|
625
|
+
|
|
626
|
+
FluxFiles was created and maintained by **thai-pc**.
|
|
627
|
+
|
|
628
|
+
If you fork or redistribute this project, please retain the original copyright notice and give appropriate credit. A link back to the original repository is appreciated:
|
|
629
|
+
|
|
630
|
+
```
|
|
631
|
+
Based on FluxFiles by thai-pc — https://github.com/thai-pc/fluxfiles
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## License
|
|
637
|
+
|
|
638
|
+
MIT
|
package/fluxfiles.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
interface FluxFilesOpenOptions {
|
|
2
|
+
endpoint: string;
|
|
3
|
+
token: string;
|
|
4
|
+
disk?: string;
|
|
5
|
+
mode?: 'picker' | 'browser';
|
|
6
|
+
locale?: string;
|
|
7
|
+
allowedTypes?: string[];
|
|
8
|
+
maxSize?: number;
|
|
9
|
+
container?: string;
|
|
10
|
+
onSelect?: (file: FluxFile) => void;
|
|
11
|
+
onClose?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FluxFile {
|
|
15
|
+
path: string;
|
|
16
|
+
basename: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
size?: number;
|
|
19
|
+
mime?: string;
|
|
20
|
+
disk?: string;
|
|
21
|
+
is_dir?: boolean;
|
|
22
|
+
modified?: string;
|
|
23
|
+
variants?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FluxEvent {
|
|
27
|
+
action: 'upload' | 'delete' | 'move' | 'copy' | 'mkdir' | 'restore' | 'purge' | 'trash' | 'crop' | 'ai_tag';
|
|
28
|
+
disk: string;
|
|
29
|
+
path: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type FluxFilesEventType = 'FM_READY' | 'FM_SELECT' | 'FM_EVENT' | 'FM_CLOSE';
|
|
34
|
+
|
|
35
|
+
interface FluxFilesSDK {
|
|
36
|
+
open(options: FluxFilesOpenOptions): void;
|
|
37
|
+
close(): void;
|
|
38
|
+
command(action: string, data?: Record<string, unknown>): void;
|
|
39
|
+
navigate(path: string): void;
|
|
40
|
+
setDisk(disk: string): void;
|
|
41
|
+
refresh(): void;
|
|
42
|
+
search(query: string): void;
|
|
43
|
+
crossCopy(dstDisk: string, dstPath?: string): void;
|
|
44
|
+
crossMove(dstDisk: string, dstPath?: string): void;
|
|
45
|
+
aiTag(): void;
|
|
46
|
+
on(event: FluxFilesEventType, callback: (data: unknown) => void): () => void;
|
|
47
|
+
off(event: FluxFilesEventType, callback: (data: unknown) => void): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare const FluxFiles: FluxFilesSDK;
|
|
51
|
+
|
|
52
|
+
export = FluxFiles;
|
|
53
|
+
export as namespace FluxFiles;
|
package/fluxfiles.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
(function(root) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var VERSION = 1;
|
|
5
|
+
var SOURCE = 'fluxfiles';
|
|
6
|
+
var iframe = null;
|
|
7
|
+
var listeners = {};
|
|
8
|
+
var config = {};
|
|
9
|
+
var ready = false;
|
|
10
|
+
|
|
11
|
+
function uuid() {
|
|
12
|
+
return 'ff-' + Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function postToIframe(type, payload) {
|
|
16
|
+
if (!iframe || !iframe.contentWindow) return;
|
|
17
|
+
iframe.contentWindow.postMessage({
|
|
18
|
+
source: SOURCE,
|
|
19
|
+
type: type,
|
|
20
|
+
v: VERSION,
|
|
21
|
+
id: uuid(),
|
|
22
|
+
payload: payload || {}
|
|
23
|
+
}, '*');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function handleMessage(e) {
|
|
27
|
+
var msg = e.data;
|
|
28
|
+
if (!msg || msg.source !== SOURCE) return;
|
|
29
|
+
|
|
30
|
+
switch (msg.type) {
|
|
31
|
+
case 'FM_READY':
|
|
32
|
+
ready = true;
|
|
33
|
+
postToIframe('FM_CONFIG', {
|
|
34
|
+
disk: config.disk || 'local',
|
|
35
|
+
token: config.token || '',
|
|
36
|
+
mode: config.mode || 'picker',
|
|
37
|
+
allowedTypes: config.allowedTypes || null,
|
|
38
|
+
maxSize: config.maxSize || null,
|
|
39
|
+
endpoint: config.endpoint || '',
|
|
40
|
+
locale: config.locale || null
|
|
41
|
+
});
|
|
42
|
+
emit('FM_READY', msg.payload);
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case 'FM_SELECT':
|
|
46
|
+
if (typeof config.onSelect === 'function') {
|
|
47
|
+
config.onSelect(msg.payload);
|
|
48
|
+
}
|
|
49
|
+
emit('FM_SELECT', msg.payload);
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case 'FM_EVENT':
|
|
53
|
+
emit('FM_EVENT', msg.payload);
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case 'FM_CLOSE':
|
|
57
|
+
if (typeof config.onClose === 'function') {
|
|
58
|
+
config.onClose();
|
|
59
|
+
}
|
|
60
|
+
emit('FM_CLOSE', msg.payload);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function emit(type, data) {
|
|
66
|
+
var cbs = listeners[type] || [];
|
|
67
|
+
for (var i = 0; i < cbs.length; i++) {
|
|
68
|
+
try { cbs[i](data); } catch(ex) { console.error('FluxFiles listener error:', ex); }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var FluxFiles = {
|
|
73
|
+
open: function(options) {
|
|
74
|
+
config = options || {};
|
|
75
|
+
var endpoint = (config.endpoint || '').replace(/\/+$/, '');
|
|
76
|
+
var container = config.container
|
|
77
|
+
? document.querySelector(config.container)
|
|
78
|
+
: document.body;
|
|
79
|
+
|
|
80
|
+
// Clean up existing
|
|
81
|
+
this.close();
|
|
82
|
+
|
|
83
|
+
// Create iframe
|
|
84
|
+
iframe = document.createElement('iframe');
|
|
85
|
+
iframe.id = 'fluxfiles-iframe';
|
|
86
|
+
iframe.src = endpoint + '/public/index.html';
|
|
87
|
+
iframe.style.cssText = 'width:100%;height:100%;border:none;';
|
|
88
|
+
iframe.setAttribute('allow', 'clipboard-write');
|
|
89
|
+
|
|
90
|
+
if (!config.container) {
|
|
91
|
+
// Modal overlay
|
|
92
|
+
var overlay = document.createElement('div');
|
|
93
|
+
overlay.id = 'fluxfiles-overlay';
|
|
94
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;display:flex;align-items:center;justify-content:center;';
|
|
95
|
+
|
|
96
|
+
var modal = document.createElement('div');
|
|
97
|
+
modal.id = 'fluxfiles-modal';
|
|
98
|
+
modal.style.cssText = 'width:90vw;height:85vh;max-width:1200px;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 25px 50px rgba(0,0,0,0.25);';
|
|
99
|
+
|
|
100
|
+
modal.appendChild(iframe);
|
|
101
|
+
overlay.appendChild(modal);
|
|
102
|
+
document.body.appendChild(overlay);
|
|
103
|
+
|
|
104
|
+
overlay.addEventListener('click', function(e) {
|
|
105
|
+
if (e.target === overlay) FluxFiles.close();
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
container.appendChild(iframe);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
window.addEventListener('message', handleMessage);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
close: function() {
|
|
115
|
+
ready = false;
|
|
116
|
+
window.removeEventListener('message', handleMessage);
|
|
117
|
+
|
|
118
|
+
var overlay = document.getElementById('fluxfiles-overlay');
|
|
119
|
+
if (overlay) overlay.remove();
|
|
120
|
+
|
|
121
|
+
var existing = document.getElementById('fluxfiles-iframe');
|
|
122
|
+
if (existing) existing.remove();
|
|
123
|
+
|
|
124
|
+
iframe = null;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
command: function(action, data) {
|
|
128
|
+
postToIframe('FM_COMMAND', Object.assign({ action: action }, data || {}));
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
navigate: function(path) { this.command('navigate', { path: path }); },
|
|
132
|
+
setDisk: function(disk) { this.command('setDisk', { disk: disk }); },
|
|
133
|
+
refresh: function() { this.command('refresh'); },
|
|
134
|
+
search: function(q) { this.command('search', { q: q }); },
|
|
135
|
+
crossCopy: function(dstDisk, dstPath) { this.command('crossCopy', { dst_disk: dstDisk, dst_path: dstPath || '' }); },
|
|
136
|
+
crossMove: function(dstDisk, dstPath) { this.command('crossMove', { dst_disk: dstDisk, dst_path: dstPath || '' }); },
|
|
137
|
+
aiTag: function() { this.command('aiTag'); },
|
|
138
|
+
|
|
139
|
+
on: function(type, cb) {
|
|
140
|
+
if (!listeners[type]) listeners[type] = [];
|
|
141
|
+
listeners[type].push(cb);
|
|
142
|
+
return function() { FluxFiles.off(type, cb); };
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
off: function(type, cb) {
|
|
146
|
+
if (!listeners[type]) return;
|
|
147
|
+
listeners[type] = listeners[type].filter(function(fn) { return fn !== cb; });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// UMD export
|
|
152
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
153
|
+
module.exports = FluxFiles;
|
|
154
|
+
} else if (typeof define === 'function' && define.amd) {
|
|
155
|
+
define(function() { return FluxFiles; });
|
|
156
|
+
} else {
|
|
157
|
+
root.FluxFiles = FluxFiles;
|
|
158
|
+
}
|
|
159
|
+
})(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fluxfiles",
|
|
3
|
+
"version": "1.22.0",
|
|
4
|
+
"description": "FluxFiles JavaScript SDK — embed the file manager in any web app",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "fluxfiles.js",
|
|
7
|
+
"types": "fluxfiles.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"fluxfiles.js",
|
|
10
|
+
"fluxfiles.d.ts"
|
|
11
|
+
],
|
|
12
|
+
"author": "thai-pc",
|
|
13
|
+
"homepage": "https://github.com/thai-pc/fluxfiles",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/thai-pc/fluxfiles.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/thai-pc/fluxfiles/issues"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"fluxfiles",
|
|
23
|
+
"file-manager",
|
|
24
|
+
"sdk",
|
|
25
|
+
"iframe",
|
|
26
|
+
"s3",
|
|
27
|
+
"r2",
|
|
28
|
+
"upload",
|
|
29
|
+
"media"
|
|
30
|
+
]
|
|
31
|
+
}
|