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 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[]&#124;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
+ }