fluxfiles 1.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1163 @@
1
+ # FluxFiles
2
+
3
+ [![Packagist Version](https://img.shields.io/packagist/v/fluxfiles/fluxfiles?label=packagist&color=f28d1a)](https://packagist.org/packages/fluxfiles/fluxfiles)
4
+ [![Laravel](https://img.shields.io/packagist/v/fluxfiles/laravel?label=laravel&color=ff2d20)](https://packagist.org/packages/fluxfiles/laravel)
5
+ [![npm](https://img.shields.io/npm/v/fluxfiles?label=sdk&color=cb3837)](https://www.npmjs.com/package/fluxfiles)
6
+ [![npm](https://img.shields.io/npm/v/@fluxfiles/react?label=react&color=61dafb)](https://www.npmjs.com/package/@fluxfiles/react)
7
+ [![npm](https://img.shields.io/npm/v/@fluxfiles/vue?label=vue&color=42b883)](https://www.npmjs.com/package/@fluxfiles/vue)
8
+ [![PHP](https://img.shields.io/packagist/php-v/fluxfiles/fluxfiles?color=777bb4)](https://packagist.org/packages/fluxfiles/fluxfiles)
9
+ [![License](https://img.shields.io/github/license/thai-pc/fluxfiles)](LICENSE)
10
+
11
+ 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.
12
+
13
+ Drop it into any web app via iframe + SDK, or use the provided adapters for **Laravel**, **WordPress**, **React**, **Vue / Nuxt**, **CKEditor 4**, and **TinyMCE**.
14
+
15
+ ---
16
+
17
+ ## Table of Contents
18
+
19
+ - [Features](#features)
20
+ - [Requirements](#requirements)
21
+ - [Quick Start](#quick-start)
22
+ - [Production Deployment](#production-deployment)
23
+ - [Embedding in Your App](#embedding-in-your-app)
24
+ - [Storage Disks](#storage-disks)
25
+ - [JWT Token Structure](#jwt-token-structure)
26
+ - [API Reference](#api-reference)
27
+ - [Framework Adapters](#framework-adapters)
28
+ - [Internationalization](#internationalization)
29
+ - [Security](#security)
30
+ - [Testing](#testing)
31
+ - [Environment Variables](#environment-variables)
32
+ - [Project Structure](#project-structure)
33
+ - [Customization](#customization)
34
+ - [License](#license)
35
+
36
+ ---
37
+
38
+ ## Features
39
+
40
+ | Category | Details |
41
+ |----------|---------|
42
+ | **Storage** | Local disk, AWS S3, Cloudflare R2 via Flysystem v3. Cross-disk copy/move with stream transfer. |
43
+ | **Auth** | JWT HS256 with granular claims — permissions, disk access, path scoping, upload limits, file type whitelist, storage quota. BYOB (Bring Your Own Bucket) support. |
44
+ | **File ops** | Upload, download (presigned URL), move, copy, rename, delete, create folders. Chunk upload (S3 multipart) for large files. Bulk operations (multi-select). |
45
+ | **Images** | Auto WebP variants on upload (thumb 150px / medium 768px / large 1920px). Inline crop tool with aspect ratio presets. Variants regenerated after crop. |
46
+ | **AI** | Claude or OpenAI vision API — auto-tag, alt text, title, caption on upload or manual trigger. |
47
+ | **Metadata** | Title, alt text, caption, tags per file. Stored as S3 object metadata (cloud) or sidecar JSON (local). Full-text search. |
48
+ | **Safety** | Duplicate detection (SHA-256). Rate limiting per user. Audit log with rotation. Per-user storage quota. Origin validation. Dangerous extension blocking. |
49
+ | **UI** | Dark mode (auto/manual). 16 languages with RTL support. Responsive. Bulk operations (multi-select, shift-select). |
50
+ | **Adapters** | Laravel, WordPress, React, Vue/Nuxt, CKEditor 4, TinyMCE |
51
+
52
+ ---
53
+
54
+ ## Requirements
55
+
56
+ - **PHP** >= 7.4 (tested with 7.4 — 8.3)
57
+ - **Extensions:** `gd`, `curl`, `json`, `openssl`, `mbstring`, `fileinfo`
58
+ - **Composer** >= 2.0
59
+
60
+ ---
61
+
62
+ ## Quick Start
63
+
64
+ ### 1. Install
65
+
66
+ ```bash
67
+ git clone https://github.com/thai-pc/fluxfiles.git
68
+ cd fluxfiles
69
+ composer install
70
+ ```
71
+
72
+ ### 2. Configure
73
+
74
+ ```bash
75
+ cp .env.example .env
76
+ ```
77
+
78
+ Edit `.env` — at minimum, set these two:
79
+
80
+ ```env
81
+ FLUXFILES_SECRET=your-random-secret-key-min-32-chars
82
+ FLUXFILES_ALLOWED_ORIGINS=http://localhost:3000,https://yourapp.com
83
+ ```
84
+
85
+ ### 3. Run
86
+
87
+ ```bash
88
+ php -S localhost:8080 router.php
89
+ ```
90
+
91
+ Open in browser:
92
+ - **UI:** http://localhost:8080/public/index.html
93
+ - **API:** http://localhost:8080/api/fm/list?disk=local&path=
94
+
95
+ ### URL Parameters (Standalone Mode)
96
+
97
+ When opening FluxFiles directly via `/public/index.html`, configure it with URL parameters:
98
+
99
+ ```
100
+ /public/index.html?token=JWT&disk=local&path=photos/&locale=vi&theme=dark
101
+ ```
102
+
103
+ | Parameter | Required | Default | Description |
104
+ |-----------|----------|---------|-------------|
105
+ | `token` | **Yes** | — | JWT authentication token |
106
+ | `disk` | No | `local` | Active disk |
107
+ | `disks` | No | `local` | Comma-separated available disks (e.g. `local,s3,r2`) |
108
+ | `path` | No | `` (root) | Initial directory path |
109
+ | `locale` | No | `en` | UI language (`en`, `vi`, `zh`, `ja`, `ko`, `fr`, `de`, `es`, `ar`, `pt`, `it`, `ru`, `th`, `hi`, `tr`, `nl`) |
110
+ | `lang` | No | `en` | Alias for `locale` |
111
+ | `theme` | No | auto | `light`, `dark`, or auto-detect |
112
+ | `multiple` | No | `false` | `1` or `true` to enable multi-select |
113
+
114
+ ### 4. Generate a Token
115
+
116
+ ```php
117
+ require_once 'path/to/fluxfiles/embed.php';
118
+
119
+ $token = fluxfiles_token(
120
+ userId: 'user-123',
121
+ perms: ['read', 'write', 'delete'],
122
+ disks: ['local', 's3', 'r2'],
123
+ prefix: 'user-123/', // scope user to their own directory
124
+ maxUploadMb: 10,
125
+ allowedExt: null, // null = allow all safe extensions
126
+ ttl: 3600 // 1 hour
127
+ );
128
+ ```
129
+
130
+ Or generate via CLI for testing:
131
+
132
+ ```bash
133
+ php tests/generate-token.php
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Production Deployment
139
+
140
+ ### Nginx
141
+
142
+ ```nginx
143
+ server {
144
+ listen 443 ssl http2;
145
+ server_name fm.yourdomain.com;
146
+ root /var/www/fluxfiles;
147
+
148
+ # SSL
149
+ ssl_certificate /etc/ssl/certs/fm.yourdomain.com.pem;
150
+ ssl_certificate_key /etc/ssl/private/fm.yourdomain.com.key;
151
+
152
+ # API — rewrite to PHP router
153
+ location /api/ {
154
+ try_files $uri /api/index.php?$query_string;
155
+ }
156
+
157
+ # Public HTML — served via PHP for locale injection
158
+ location /public/ {
159
+ try_files $uri /api/index.php?$query_string;
160
+ }
161
+
162
+ # Static assets (JS, CSS)
163
+ location /assets/ {
164
+ expires 30d;
165
+ add_header Cache-Control "public, immutable";
166
+ }
167
+
168
+ # SDK file
169
+ location = /fluxfiles.js {
170
+ expires 7d;
171
+ add_header Cache-Control "public";
172
+ }
173
+
174
+ # Uploaded files (local disk only)
175
+ location /storage/uploads/ {
176
+ alias /var/www/fluxfiles/storage/uploads/;
177
+ expires 7d;
178
+ add_header Cache-Control "public";
179
+ }
180
+
181
+ # PHP-FPM
182
+ location ~ \.php$ {
183
+ fastcgi_pass unix:/run/php/php8.2-fpm.sock;
184
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
185
+ include fastcgi_params;
186
+ fastcgi_read_timeout 120;
187
+ }
188
+
189
+ # Block dotfiles and sensitive paths
190
+ location ~ /\. { deny all; }
191
+ location ~ ^/(\.env|composer\.|vendor/) { deny all; }
192
+ location /storage/rate_limit.json { deny all; }
193
+ location /_fluxfiles/ { deny all; }
194
+ }
195
+ ```
196
+
197
+ ### Apache (.htaccess)
198
+
199
+ ```apache
200
+ RewriteEngine On
201
+
202
+ # API routes
203
+ RewriteRule ^api/(.*)$ api/index.php [QSA,L]
204
+
205
+ # Public HTML through PHP for locale injection
206
+ RewriteRule ^public/(index\.html)?$ api/index.php [QSA,L]
207
+
208
+ # Block sensitive files
209
+ <FilesMatch "^\.env|composer\.(json|lock)">
210
+ Require all denied
211
+ </FilesMatch>
212
+ ```
213
+
214
+ ### Directory Permissions
215
+
216
+ ```bash
217
+ # Set ownership
218
+ chown -R www-data:www-data /var/www/fluxfiles/storage/
219
+
220
+ # Writable directories
221
+ chmod -R 755 storage/
222
+ chmod 600 .env
223
+ chmod 600 storage/rate_limit.json # if exists
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Embedding in Your App
229
+
230
+ ### JavaScript SDK (Vanilla)
231
+
232
+ Include `fluxfiles.js` on your page — zero dependencies, works with any framework:
233
+
234
+ ```html
235
+ <script src="https://fm.yourdomain.com/fluxfiles.js"></script>
236
+
237
+ <button onclick="openFilePicker()">Choose File</button>
238
+
239
+ <script>
240
+ function openFilePicker() {
241
+ FluxFiles.open({
242
+ endpoint: 'https://fm.yourdomain.com',
243
+ token: 'eyJhbGci...', // JWT token from your backend
244
+ disk: 'local', // default disk
245
+ disks: ['local', 'r2'], // available disks in sidebar
246
+ mode: 'picker', // 'picker' = select & close, 'browser' = stay open
247
+ multiple: false, // true = multi-select returns array
248
+ locale: 'en', // default 'en' if omitted
249
+ theme: 'auto', // 'light', 'dark', or 'auto'
250
+ allowedTypes: ['image/*', '.pdf'],
251
+ maxSize: 10485760, // 10MB in bytes
252
+ container: '#my-div', // CSS selector — omit for modal overlay
253
+
254
+ onSelect(file) {
255
+ // file = { url, key, name, size, mime, meta }
256
+ console.log('Selected:', file.url);
257
+ document.getElementById('image').src = file.url;
258
+ },
259
+ onClose() {
260
+ console.log('File picker closed');
261
+ },
262
+
263
+ // Token refresh — called automatically on 401
264
+ async onTokenRefresh({ reason, disk, path }) {
265
+ const res = await fetch('/api/auth/refresh-fluxfiles-token');
266
+ const { token } = await res.json();
267
+ return token; // return new JWT string, or null to fail
268
+ }
269
+ });
270
+ }
271
+ </script>
272
+ ```
273
+
274
+ ### SDK Commands
275
+
276
+ Control the file manager programmatically after opening:
277
+
278
+ ```js
279
+ FluxFiles.navigate('/photos/2024'); // Navigate to path
280
+ FluxFiles.setDisk('s3'); // Switch disk
281
+ FluxFiles.refresh(); // Reload current directory
282
+ FluxFiles.search('invoice'); // Trigger search
283
+ FluxFiles.crossCopy('s3', 'backups/'); // Copy selected file to another disk
284
+ FluxFiles.crossMove('r2', 'archive/'); // Move selected file to another disk
285
+ FluxFiles.aiTag(); // AI-tag selected image
286
+ FluxFiles.setLocale('vi'); // Change language
287
+ FluxFiles.close(); // Close file manager
288
+ FluxFiles.updateToken('eyJ...'); // Push new token (e.g. background refresh)
289
+ ```
290
+
291
+ ### SDK Events
292
+
293
+ ```js
294
+ FluxFiles.on('FM_READY', (payload) => {
295
+ console.log('Version:', payload.version);
296
+ console.log('Capabilities:', payload.capabilities);
297
+ });
298
+
299
+ FluxFiles.on('FM_SELECT', (file) => {
300
+ // Single file: { url, key, name, size, mime, meta, variants }
301
+ // Multiple: [{ url, key, ... }, ...]
302
+ });
303
+
304
+ FluxFiles.on('FM_EVENT', (event) => {
305
+ // event.event: 'upload:done', 'delete:done', 'rename:done',
306
+ // 'move:done', 'copy:done', 'folder:created',
307
+ // 'crop:done', 'ai_tag:done'
308
+ console.log(event.event, event.key);
309
+ });
310
+
311
+ FluxFiles.on('FM_CLOSE', () => {
312
+ console.log('Closed');
313
+ });
314
+
315
+ // Token refresh events
316
+ FluxFiles.on('FM_TOKEN_REFRESH', (ctx) => {
317
+ console.log('Token refresh requested:', ctx.reason);
318
+ });
319
+
320
+ // Unsubscribe
321
+ const unsub = FluxFiles.on('FM_SELECT', handler);
322
+ unsub(); // remove listener
323
+ ```
324
+
325
+ ### Token Refresh
326
+
327
+ FluxFiles automatically handles JWT expiration. When the API returns 401:
328
+
329
+ 1. The iframe sends `FM_TOKEN_REFRESH` to the host app
330
+ 2. The SDK calls your `onTokenRefresh` callback
331
+ 3. You fetch a new JWT from your backend and return it
332
+ 4. The SDK sends `FM_TOKEN_UPDATED` back to the iframe
333
+ 5. The failed request is automatically retried with the new token
334
+
335
+ **Behavior details:**
336
+ - Multiple concurrent 401s are coalesced into a single refresh request
337
+ - After 2 consecutive refresh failures, the auth expired screen is shown
338
+ - 10-second timeout — if no response, falls back to expired screen
339
+ - `auth:refreshed` and `auth:expired` events are emitted via `FM_EVENT`
340
+
341
+ **Proactive refresh:** Call `FluxFiles.updateToken(newJwt)` to push a new token before it expires (e.g. on a timer).
342
+
343
+ ### PHP Embed Helper
344
+
345
+ For server-rendered pages, use the PHP helper to generate the iframe HTML:
346
+
347
+ ```php
348
+ require_once 'path/to/fluxfiles/embed.php';
349
+
350
+ // Generate token
351
+ $token = fluxfiles_token(
352
+ userId: (string) $currentUser->id,
353
+ perms: ['read', 'write', 'delete'],
354
+ disks: ['local', 'r2'],
355
+ prefix: 'users/' . $currentUser->id . '/'
356
+ );
357
+
358
+ // Render inline embed
359
+ echo fluxfiles_embed(
360
+ endpoint: 'https://fm.yourdomain.com',
361
+ token: $token,
362
+ disk: 'local',
363
+ mode: 'browser',
364
+ width: '100%',
365
+ height: '600px'
366
+ );
367
+ ```
368
+
369
+ ### TypeScript Support
370
+
371
+ TypeScript declarations are included in `fluxfiles.d.ts`:
372
+
373
+ ```ts
374
+ import type { FluxFilesInstance, FluxFilesOpenOptions, FluxFile } from './fluxfiles';
375
+ ```
376
+
377
+ ---
378
+
379
+ ## Storage Disks
380
+
381
+ ### Configuration
382
+
383
+ Disks are defined in `config/disks.php`:
384
+
385
+ ```php
386
+ return [
387
+ // Local filesystem
388
+ 'local' => [
389
+ 'driver' => 'local',
390
+ 'root' => __DIR__ . '/../storage/uploads',
391
+ 'url' => '/storage/uploads', // public URL prefix
392
+ ],
393
+
394
+ // AWS S3
395
+ 's3' => [
396
+ 'driver' => 's3',
397
+ 'region' => $_ENV['AWS_DEFAULT_REGION'],
398
+ 'bucket' => $_ENV['AWS_BUCKET'],
399
+ 'key' => $_ENV['AWS_ACCESS_KEY_ID'],
400
+ 'secret' => $_ENV['AWS_SECRET_ACCESS_KEY'],
401
+ ],
402
+
403
+ // Cloudflare R2 (S3-compatible)
404
+ 'r2' => [
405
+ 'driver' => 's3',
406
+ 'endpoint' => 'https://' . $_ENV['R2_ACCOUNT_ID'] . '.r2.cloudflarestorage.com',
407
+ 'region' => 'auto',
408
+ 'bucket' => $_ENV['R2_BUCKET'],
409
+ 'key' => $_ENV['R2_ACCESS_KEY_ID'],
410
+ 'secret' => $_ENV['R2_SECRET_ACCESS_KEY'],
411
+ ],
412
+ ];
413
+ ```
414
+
415
+ > **Note:** R2 uses the S3-compatible API. ACL operations are not supported — FluxFiles automatically disables `retain_visibility` for endpoint-based disks.
416
+
417
+ ### Adding a Custom Disk
418
+
419
+ Add a new entry to `config/disks.php`:
420
+
421
+ ```php
422
+ 'minio' => [
423
+ 'driver' => 's3',
424
+ 'endpoint' => 'http://minio.local:9000',
425
+ 'region' => 'us-east-1',
426
+ 'bucket' => 'my-bucket',
427
+ 'key' => 'minioadmin',
428
+ 'secret' => 'minioadmin',
429
+ ],
430
+ ```
431
+
432
+ Then include `'minio'` in the JWT `disks` claim.
433
+
434
+ ### BYOB (Bring Your Own Bucket)
435
+
436
+ Users can connect their own S3/R2 buckets. Credentials are AES-256-GCM encrypted inside the JWT (derived key via HKDF, separate from signing key):
437
+
438
+ ```php
439
+ $token = fluxfiles_byob_token(
440
+ userId: 'user-123',
441
+ byobDisks: [
442
+ 'my-bucket' => [
443
+ 'driver' => 's3',
444
+ 'region' => 'us-west-2',
445
+ 'bucket' => 'user-personal-bucket',
446
+ 'key' => 'AKIAIOSFODNN7EXAMPLE',
447
+ 'secret' => 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
448
+ ],
449
+ ],
450
+ perms: ['read', 'write', 'delete'],
451
+ ttl: 1800 // shorter TTL for BYOB tokens
452
+ );
453
+ ```
454
+
455
+ Security: BYOB only allows `s3` driver — `local` driver is blocked to prevent path traversal.
456
+
457
+ ### Cross-Disk Operations
458
+
459
+ Copy or move files between any two disks (e.g., local to R2):
460
+
461
+ ```bash
462
+ # API
463
+ POST /api/fm/cross-copy
464
+ {"src_disk":"local","src_path":"photo.jpg","dst_disk":"r2","dst_path":"backups/photo.jpg"}
465
+
466
+ # SDK
467
+ FluxFiles.crossCopy('r2', 'backups/');
468
+ FluxFiles.crossMove('s3', 'archive/');
469
+ ```
470
+
471
+ Metadata and image variants are transferred together. Quota is checked on the destination disk.
472
+
473
+ ---
474
+
475
+ ## JWT Token Structure
476
+
477
+ ```json
478
+ {
479
+ "sub": "user-123",
480
+ "iat": 1710500000,
481
+ "exp": 1710503600,
482
+ "jti": "a1b2c3d4e5f6",
483
+ "perms": ["read", "write", "delete"],
484
+ "disks": ["local", "s3", "r2"],
485
+ "prefix": "user-123/",
486
+ "max_upload": 10,
487
+ "allowed_ext": ["jpg", "png", "pdf"],
488
+ "max_storage": 1000,
489
+ "byob_disks": {}
490
+ }
491
+ ```
492
+
493
+ | Claim | Type | Default | Description |
494
+ |-------|------|---------|-------------|
495
+ | `sub` | string | `"0"` | User identifier |
496
+ | `perms` | string[] | `["read"]` | Permissions: `read`, `write`, `delete` |
497
+ | `disks` | string[] | `["local"]` | Allowed storage disks |
498
+ | `prefix` | string | `""` | Path scope — user can only access files under this prefix |
499
+ | `max_upload` | int | `10` | Max file upload size in MB |
500
+ | `allowed_ext` | string[]&#124;null | `null` | File extension whitelist (`null` = allow all safe types) |
501
+ | `max_storage` | int | `0` | Storage quota in MB (`0` = unlimited) |
502
+ | `owner_only` | bool | `false` | When `true`, users can only delete/rename/move files they uploaded |
503
+ | `byob_disks` | object | — | Encrypted BYOB credentials (optional) |
504
+
505
+ ### Permissions explained
506
+
507
+ | Permission | Allows |
508
+ |-----------|--------|
509
+ | `read` | List files, view metadata, search, download (presign), get quota |
510
+ | `write` | Upload, rename, copy, move, mkdir, save metadata, crop, AI-tag |
511
+ | `delete` | Delete files and directories |
512
+
513
+ ### Path prefix
514
+
515
+ The `prefix` claim isolates users to their own directory:
516
+
517
+ ```
518
+ prefix: "users/42/"
519
+ → User can only access: users/42/*, users/42/photos/*, etc.
520
+ → Path traversal (../) is stripped before prefix is applied
521
+ → Null bytes are removed
522
+ ```
523
+
524
+ ### User isolation
525
+
526
+ FluxFiles provides two layers of user isolation that can be used independently or combined:
527
+
528
+ **Layer 1: Path prefix (recommended)** — Each user gets a unique `prefix` so they physically cannot see or touch other users' files:
529
+
530
+ ```php
531
+ $token = fluxfiles_token(
532
+ userId: $user->id,
533
+ perms: ['read', 'write', 'delete'],
534
+ prefix: 'users/' . $user->id . '/', // user 42 → users/42/*
535
+ );
536
+ ```
537
+
538
+ **Layer 2: Owner-only mode** — When multiple users share the same prefix (e.g., a shared team folder), `owner_only` restricts destructive operations (delete, rename, move, crop) to the user who uploaded the file:
539
+
540
+ ```php
541
+ $token = fluxfiles_token(
542
+ userId: $user->id,
543
+ perms: ['read', 'write', 'delete'],
544
+ prefix: 'team/shared/',
545
+ ownerOnly: true, // can only delete/rename own files
546
+ );
547
+ ```
548
+
549
+ | Scenario | Use |
550
+ |----------|-----|
551
+ | Each user has their own space | `prefix: 'users/{id}/'` |
552
+ | Shared folder, users can only manage own files | `prefix: 'shared/'` + `owner_only: true` |
553
+ | Admin with full access | `prefix: ''` (no prefix, no owner_only) |
554
+ | Shared folder, everyone can manage all files | `prefix: 'shared/'` (no owner_only) |
555
+
556
+ > **Warning:** `owner_only` is a safety layer, NOT a replacement for `prefix` isolation. Always use `prefix` to scope users to their own directory. `owner_only` only protects against delete/rename/move — it does NOT prevent users from reading or downloading each other's files.
557
+
558
+ > **Note:** Files uploaded before `owner_only` was enabled lack ownership metadata and will be accessible to all users. Ownership is recorded from the moment the feature is enabled.
559
+
560
+ ---
561
+
562
+ ## API Reference
563
+
564
+ Base path: `/api/fm/`
565
+
566
+ All responses follow the format: `{ "data": { ... }, "error": null }`
567
+ On error: `{ "data": null, "error": "Error message" }` with appropriate HTTP status.
568
+
569
+ ### Public Endpoints (no auth)
570
+
571
+ | Method | Path | Description |
572
+ |--------|------|-------------|
573
+ | `GET` | `/api/fm/lang` | List available locales → `[{code, name, dir}]` |
574
+ | `GET` | `/api/fm/lang/{code}` | Get translation messages for a locale |
575
+
576
+ ### File Operations (JWT required)
577
+
578
+ | Method | Path | Body / Params | Description |
579
+ |--------|------|---------------|-------------|
580
+ | `GET` | `/list?disk=&path=` | — | List directory contents |
581
+ | `POST` | `/upload` | `multipart: disk, path, file, force_upload?` | Upload file |
582
+ | `DELETE` | `/delete` | `{disk, path}` | Delete file or directory (recursive) |
583
+ | `POST` | `/rename` | `{disk, path, name}` | Rename file or directory |
584
+ | `POST` | `/move` | `{disk, from, to}` | Move within same disk |
585
+ | `POST` | `/copy` | `{disk, from, to}` | Copy within same disk |
586
+ | `POST` | `/mkdir` | `{disk, path}` | Create directory |
587
+ | `POST` | `/cross-copy` | `{src_disk, src_path, dst_disk, dst_path}` | Copy between disks |
588
+ | `POST` | `/cross-move` | `{src_disk, src_path, dst_disk, dst_path}` | Move between disks |
589
+ | `POST` | `/presign` | `{disk, path, method, ttl}` | Generate presigned URL (GET or PUT, max 86400s) |
590
+ | `POST` | `/crop` | `{disk, path, x, y, width, height, save_path?}` | Crop image |
591
+ | `POST` | `/ai-tag` | `{disk, path}` | AI-analyze image (requires AI config) |
592
+
593
+ ### Metadata
594
+
595
+ | Method | Path | Body / Params | Description |
596
+ |--------|------|---------------|-------------|
597
+ | `GET` | `/meta?disk=&path=` | — | File info: size, mime, modified |
598
+ | `GET` | `/metadata?disk=&key=` | — | SEO metadata: title, alt_text, caption, tags |
599
+ | `PUT` | `/metadata` | `{disk, key, title, alt_text, caption, tags}` | Save metadata |
600
+ | `DELETE` | `/metadata` | `{disk, key}` | Delete metadata |
601
+
602
+ ### Search, Quota, Audit
603
+
604
+ | Method | Path | Params | Description |
605
+ |--------|------|--------|-------------|
606
+ | `GET` | `/search?disk=&q=&limit=` | `limit` default 50 | Full-text search across file names + metadata |
607
+ | `GET` | `/quota?disk=` | — | Storage usage: used_mb, max_mb, percentage |
608
+ | `GET` | `/audit?limit=&offset=` | `limit` default 100 | Audit log (filtered to current user) |
609
+
610
+ ### Chunk Upload (S3 multipart, files > 10MB)
611
+
612
+ | Method | Path | Body | Description |
613
+ |--------|------|------|-------------|
614
+ | `POST` | `/chunk/init` | `{disk, path}` | Initiate → `{upload_id, key, chunk_size}` |
615
+ | `POST` | `/chunk/presign` | `{disk, key, upload_id, part_number}` | Presign URL for part |
616
+ | `POST` | `/chunk/complete` | `{disk, key, upload_id, parts}` | Complete upload |
617
+ | `POST` | `/chunk/abort` | `{disk, key, upload_id}` | Abort upload |
618
+
619
+ ### Upload Response Example
620
+
621
+ ```json
622
+ {
623
+ "data": {
624
+ "key": "users/42/photo.jpg",
625
+ "url": "https://bucket.r2.cloudflarestorage.com/photo.jpg",
626
+ "name": "photo.jpg",
627
+ "size": 245760,
628
+ "variants": {
629
+ "thumb": { "url": "...", "key": "..._thumb.webp", "width": 150, "height": 100 },
630
+ "medium": { "url": "...", "key": "..._medium.webp", "width": 768, "height": 512 },
631
+ "large": { "url": "...", "key": "..._large.webp", "width": 1920, "height": 1280 }
632
+ },
633
+ "ai_tags": {
634
+ "tags": ["landscape", "mountain", "sunset"],
635
+ "title": "Mountain sunset landscape",
636
+ "alt_text": "A mountain range silhouetted against an orange sunset sky",
637
+ "caption": "Beautiful sunset over mountain peaks with warm orange and purple tones."
638
+ }
639
+ },
640
+ "error": null
641
+ }
642
+ ```
643
+
644
+ ### Duplicate Detection
645
+
646
+ If a file with the same SHA-256 hash exists, upload returns:
647
+
648
+ ```json
649
+ {
650
+ "data": {
651
+ "key": "existing/path/photo.jpg",
652
+ "url": "...",
653
+ "duplicate": true,
654
+ "message": "File already exists. Use force_upload to override."
655
+ }
656
+ }
657
+ ```
658
+
659
+ Send `force_upload=true` (in form data) to upload anyway.
660
+
661
+ ---
662
+
663
+ ## Framework Adapters
664
+
665
+ ### Laravel
666
+
667
+ ```bash
668
+ composer require fluxfiles/laravel
669
+ php artisan vendor:publish --tag=fluxfiles-config
670
+ ```
671
+
672
+ Add to `.env`:
673
+
674
+ ```env
675
+ FLUXFILES_ENDPOINT=https://fm.yourdomain.com
676
+ FLUXFILES_SECRET=your-secret-min-32-chars
677
+ ```
678
+
679
+ **Blade component:**
680
+
681
+ ```blade
682
+ {{-- Embedded file browser --}}
683
+ <x-fluxfiles disk="local" mode="browser" height="600px" />
684
+
685
+ {{-- Modal file picker --}}
686
+ <x-fluxfiles disk="r2" mode="picker" @select="handleFileSelect" />
687
+ ```
688
+
689
+ **Generate token in controller:**
690
+
691
+ ```php
692
+ use FluxFiles\Laravel\FluxFilesFacade as FluxFiles;
693
+
694
+ $token = FluxFiles::token(
695
+ userId: (string) auth()->id(),
696
+ perms: ['read', 'write'],
697
+ disks: ['local', 's3'],
698
+ prefix: 'users/' . auth()->id() . '/'
699
+ );
700
+ ```
701
+
702
+ **Config** (`config/fluxfiles.php`):
703
+
704
+ ```php
705
+ return [
706
+ 'endpoint' => env('FLUXFILES_ENDPOINT'),
707
+ 'secret' => env('FLUXFILES_SECRET'),
708
+ 'disk' => 'local',
709
+ 'disks' => ['local', 'r2'],
710
+ 'prefix' => 'users/{user_id}',
711
+ 'max_upload' => 50,
712
+ 'max_storage' => 500,
713
+ 'allowed_ext' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf'],
714
+ ];
715
+ ```
716
+
717
+ ### WordPress
718
+
719
+ **Install:**
720
+
721
+ ```bash
722
+ # Option 1: Copy plugin folder
723
+ cp -r adapters/wordpress/ /path/to/wp-content/plugins/fluxfiles/
724
+
725
+ # Option 2: If using Composer in your WP project
726
+ composer require fluxfiles/fluxfiles
727
+ cp -r vendor/fluxfiles/fluxfiles/adapters/wordpress wp-content/plugins/fluxfiles
728
+ ```
729
+
730
+ **Requires:** Composer dependencies installed in the FluxFiles root (`composer install`).
731
+
732
+ **Activate & Configure:**
733
+
734
+ 1. **Plugins > Installed Plugins** → Activate **FluxFiles**
735
+ 2. **Settings > FluxFiles** → fill in:
736
+ - **Endpoint:** `https://fm.yourdomain.com`
737
+ - **JWT Secret:** must match `FLUXFILES_SECRET` in `.env`
738
+ - **Default Disk:** `local`, `s3`, or `r2`
739
+ - **Path Prefix:** `wp/{user_id}` (isolates files per WP user)
740
+
741
+ **Shortcode:**
742
+
743
+ ```
744
+ [fluxfiles disk="r2" path="uploads" mode="picker" height="500px"]
745
+ ```
746
+
747
+ **Media Button:** A "FluxFiles" button appears in the Classic Editor toolbar — opens a modal file picker.
748
+
749
+ **REST API:** Available at `/wp-json/fluxfiles/v1/`:
750
+
751
+ ```
752
+ GET /wp-json/fluxfiles/v1/files?disk=local&path=
753
+ POST /wp-json/fluxfiles/v1/upload
754
+ ```
755
+
756
+ **PHP API:**
757
+
758
+ ```php
759
+ $token = FluxFilesPlugin::instance()->generateToken($user_id);
760
+ ```
761
+
762
+ ### React
763
+
764
+ ```bash
765
+ npm install @fluxfiles/react
766
+ ```
767
+
768
+ **Components:**
769
+
770
+ ```tsx
771
+ import { FluxFiles, FluxFilesModal, useFluxFiles } from '@fluxfiles/react';
772
+
773
+ // Embedded file browser
774
+ function FileBrowser() {
775
+ return (
776
+ <FluxFiles
777
+ endpoint="https://fm.yourdomain.com"
778
+ token={token}
779
+ disk="r2"
780
+ disks={['local', 'r2']}
781
+ locale="en"
782
+ theme="auto"
783
+ onSelect={(file) => console.log(file)}
784
+ style={{ height: '600px' }}
785
+ />
786
+ );
787
+ }
788
+
789
+ // Modal file picker
790
+ function FilePicker() {
791
+ const [open, setOpen] = useState(false);
792
+
793
+ return (
794
+ <>
795
+ <button onClick={() => setOpen(true)}>Choose File</button>
796
+ <FluxFilesModal
797
+ open={open}
798
+ onClose={() => setOpen(false)}
799
+ endpoint="https://fm.yourdomain.com"
800
+ token={token}
801
+ onSelect={(file) => {
802
+ console.log(file.url);
803
+ setOpen(false);
804
+ }}
805
+ />
806
+ </>
807
+ );
808
+ }
809
+
810
+ // Hook for programmatic control
811
+ function AdvancedUsage() {
812
+ const { ref, navigate, refresh, setDisk, search, aiTag } = useFluxFiles({
813
+ endpoint: 'https://fm.yourdomain.com',
814
+ token,
815
+ onSelect: (file) => console.log(file),
816
+ });
817
+
818
+ return (
819
+ <div>
820
+ <FluxFiles ref={ref} endpoint="..." token="..." />
821
+ <button onClick={() => navigate('/photos')}>Go to Photos</button>
822
+ <button onClick={() => setDisk('r2')}>Switch to R2</button>
823
+ </div>
824
+ );
825
+ }
826
+ ```
827
+
828
+ **Build from source:**
829
+
830
+ ```bash
831
+ cd adapters/react
832
+ npm install
833
+ npm run build # → dist/index.js, dist/index.mjs, dist/index.d.ts
834
+ npm run typecheck # TypeScript validation
835
+ ```
836
+
837
+ ### Vue 3 / Nuxt 3
838
+
839
+ ```bash
840
+ npm install @fluxfiles/vue
841
+ ```
842
+
843
+ ```vue
844
+ <script setup>
845
+ import { ref } from 'vue';
846
+ import { FluxFiles, FluxFilesModal } from '@fluxfiles/vue';
847
+
848
+ const open = ref(false);
849
+ const handleSelect = (file) => console.log(file.url);
850
+ </script>
851
+
852
+ <template>
853
+ <!-- Embedded -->
854
+ <FluxFiles
855
+ endpoint="https://fm.yourdomain.com"
856
+ :token="token"
857
+ disk="local"
858
+ @select="handleSelect"
859
+ style="height: 600px"
860
+ />
861
+
862
+ <!-- Modal -->
863
+ <button @click="open = true">Choose File</button>
864
+ <FluxFilesModal
865
+ v-model:open="open"
866
+ endpoint="https://fm.yourdomain.com"
867
+ :token="token"
868
+ @select="handleSelect"
869
+ @close="open = false"
870
+ />
871
+ </template>
872
+ ```
873
+
874
+ **Nuxt 3 auto-import:**
875
+
876
+ ```ts
877
+ // nuxt.config.ts
878
+ export default defineNuxtConfig({
879
+ plugins: ['@fluxfiles/vue/nuxt'],
880
+ });
881
+ ```
882
+
883
+ ### CKEditor 4
884
+
885
+ 1. Copy `adapters/ckeditor4/` to your CKEditor plugins directory
886
+ 2. Load `fluxfiles.js` SDK on the page
887
+
888
+ ```js
889
+ CKEDITOR.replace('editor', {
890
+ extraPlugins: 'fluxfiles',
891
+ fluxfiles: {
892
+ endpoint: 'https://fm.yourdomain.com',
893
+ token: 'JWT_TOKEN',
894
+ disk: 'local',
895
+ locale: 'en',
896
+ multiple: false
897
+ }
898
+ });
899
+ ```
900
+
901
+ Click the **FluxFiles** toolbar button — images insert as `<img>`, other files as `<a>`.
902
+
903
+ ### TinyMCE (4.x / 5.x)
904
+
905
+ 1. Copy `adapters/tinymce/` to your TinyMCE plugins directory
906
+ 2. Load `fluxfiles.js` SDK on the page
907
+
908
+ ```js
909
+ tinymce.init({
910
+ selector: '#editor',
911
+ plugins: 'fluxfiles',
912
+ toolbar: 'undo redo | bold italic | fluxfiles',
913
+ fluxfiles_endpoint: 'https://fm.yourdomain.com',
914
+ fluxfiles_token: 'JWT_TOKEN',
915
+ fluxfiles_disk: 'local',
916
+ fluxfiles_locale: 'en',
917
+ fluxfiles_multiple: false
918
+ });
919
+ ```
920
+
921
+ Auto-detects TinyMCE 4 vs 5 API.
922
+
923
+ ---
924
+
925
+ ## Internationalization
926
+
927
+ 16 languages built in. Translation files in `lang/*.json`.
928
+
929
+ | Code | Language | Dir | | Code | Language | Dir |
930
+ |------|----------|-----|-|------|----------|-----|
931
+ | `en` | English | LTR | | `pt` | Portugues | LTR |
932
+ | `vi` | Tieng Viet | LTR | | `it` | Italiano | LTR |
933
+ | `zh` | Chinese | LTR | | `ru` | Русский | LTR |
934
+ | `ja` | Japanese | LTR | | `th` | ไทย | LTR |
935
+ | `ko` | Korean | LTR | | `hi` | हिन्दी | LTR |
936
+ | `fr` | Francais | LTR | | `tr` | Turkce | LTR |
937
+ | `de` | Deutsch | LTR | | `nl` | Nederlands | LTR |
938
+ | `es` | Espanol | LTR | | `ar` | Arabic | RTL |
939
+
940
+ **Locale priority:** SDK `locale` option > URL param (`?locale=` or `?lang=`) > `FLUXFILES_LOCALE` env > `en`
941
+
942
+ **Default is English.** No auto-detection from browser. To use a different language, set it explicitly.
943
+
944
+ **Set locale via URL (standalone mode):**
945
+
946
+ ```
947
+ /public/index.html?token=...&locale=vi
948
+ /public/index.html?token=...&lang=ja
949
+ ```
950
+
951
+ **Set locale via SDK:**
952
+
953
+ ```js
954
+ FluxFiles.open({ locale: 'vi', ... });
955
+ // or change at runtime:
956
+ FluxFiles.setLocale('ja');
957
+ ```
958
+
959
+ **Set locale server-wide (env):**
960
+
961
+ ```env
962
+ FLUXFILES_LOCALE=vi
963
+ ```
964
+
965
+ **Add a new language:** See [`lang/CONTRIBUTING.md`](lang/CONTRIBUTING.md) — copy `lang/en.json`, translate, submit PR.
966
+
967
+ ---
968
+
969
+ ## Security
970
+
971
+ ### Built-in Protections
972
+
973
+ | Protection | How |
974
+ |-----------|-----|
975
+ | **JWT HS256** | Algorithm pinned — prevents algorithm confusion attacks |
976
+ | **CORS whitelist** | Only configured origins receive `Access-Control-Allow-Origin` |
977
+ | **Origin validation** | POST/PUT/DELETE requests are rejected if Origin header doesn't match whitelist |
978
+ | **postMessage origin** | SDK and iframe validate `e.origin` to prevent cross-origin message injection |
979
+ | **Path traversal** | `..` and `.` segments stripped, null bytes removed, paths normalized before use |
980
+ | **Extension blocking** | Dangerous extensions (php, exe, sh, bat, etc.) blocked even in double-extension filenames (e.g. `shell.php.jpg`) |
981
+ | **Path scoping** | Users confined to their `prefix` directory — cannot access files outside scope |
982
+ | **Owner-only mode** | `owner_only` JWT claim restricts delete/rename/move to files the user uploaded |
983
+ | **System path protection** | `_fluxfiles/` and `_variants/` directories blocked from list/delete/rename/move — hidden in file listing |
984
+ | **Disk whitelist** | Per-token disk access — users can only access disks listed in JWT |
985
+ | **Permission model** | Granular `read`, `write`, `delete` checked on every operation |
986
+ | **BYOB encryption** | AES-256-GCM with HKDF-derived key (separate from signing key) |
987
+ | **BYOB local blocked** | BYOB tokens cannot use `local` driver — only S3-compatible storage |
988
+ | **Rate limiting** | Token bucket per user, file-locked, configurable (default: 60 read, 10 write/min) |
989
+ | **Quota enforcement** | Per-user storage limits checked before upload and cross-disk copy |
990
+ | **Duplicate detection** | SHA-256 hash prevents redundant uploads |
991
+ | **Audit trail** | All write actions logged with user ID, action, IP, user agent. Rotation at 5MB. |
992
+ | **Presign validation** | Method restricted to GET/PUT only, TTL capped at 86400 seconds |
993
+ | **Error handling** | Generic errors to client, detailed errors to server log only |
994
+ | **Search XSS** | HTML entities escaped before highlight `<mark>` tags applied |
995
+
996
+ ### Production Checklist
997
+
998
+ - [ ] Set `FLUXFILES_SECRET` to a cryptographically random string (min 32 chars)
999
+ - [ ] Set `FLUXFILES_ALLOWED_ORIGINS` to your production domain(s)
1000
+ - [ ] Use HTTPS everywhere
1001
+ - [ ] Block public access to `.env`, `vendor/`, `storage/rate_limit.json`
1002
+ - [ ] Set `storage/` directory permissions to 755, `.env` to 600
1003
+ - [ ] Never commit `.env` with real credentials to git
1004
+ - [ ] Review and rotate API keys periodically
1005
+
1006
+ ---
1007
+
1008
+ ## Testing
1009
+
1010
+ ```bash
1011
+ # Start dev server
1012
+ php -S localhost:8080 router.php
1013
+
1014
+ # API integration tests
1015
+ bash tests/test-api.sh # Local disk — list, upload, rename, move, copy, delete, metadata, search
1016
+ bash tests/test-r2.sh # R2/S3 cloud storage tests
1017
+
1018
+ # Unit tests
1019
+ php tests/test-claims.php # JWT claims parsing + path scoping
1020
+ php tests/test-diskmanager.php # DiskManager factory
1021
+ php tests/test-ratelimiter.php # Rate limiter
1022
+ php tests/test-metadata.php # Metadata handler
1023
+ php tests/test-byob.php # BYOB encryption + token validation
1024
+ php tests/test-i18n.php # i18n — validates all 16 language files
1025
+ php tests/test-i18n.php --api # i18n API endpoint tests
1026
+
1027
+ # Generate tokens for manual testing
1028
+ php tests/generate-token.php
1029
+
1030
+ # Browser-based tests
1031
+ open tests/test-sdk.html # SDK integration
1032
+ open tests/test-ckeditor4.html # CKEditor 4
1033
+ open tests/test-tinymce.html # TinyMCE
1034
+ ```
1035
+
1036
+ ---
1037
+
1038
+ ## Environment Variables
1039
+
1040
+ | Variable | Required | Default | Description |
1041
+ |----------|----------|---------|-------------|
1042
+ | `FLUXFILES_SECRET` | **Yes** | — | JWT signing secret (min 32 chars) |
1043
+ | `FLUXFILES_ALLOWED_ORIGINS` | **Yes** | — | Comma-separated CORS origins |
1044
+ | `FLUXFILES_LOCALE` | No | `en` | UI language (`en`, `vi`, `zh`, `ja`, etc.) |
1045
+ | `FLUXFILES_RATE_LIMIT_READ` | No | `60` | Max read requests per minute per user |
1046
+ | `FLUXFILES_RATE_LIMIT_WRITE` | No | `10` | Max write requests per minute per user |
1047
+ | `AWS_ACCESS_KEY_ID` | No | — | AWS S3 access key |
1048
+ | `AWS_SECRET_ACCESS_KEY` | No | — | AWS S3 secret key |
1049
+ | `AWS_DEFAULT_REGION` | No | `ap-southeast-1` | AWS region |
1050
+ | `AWS_BUCKET` | No | — | S3 bucket name |
1051
+ | `R2_ACCESS_KEY_ID` | No | — | Cloudflare R2 access key |
1052
+ | `R2_SECRET_ACCESS_KEY` | No | — | Cloudflare R2 secret key |
1053
+ | `R2_ACCOUNT_ID` | No | — | Cloudflare account ID |
1054
+ | `R2_BUCKET` | No | — | R2 bucket name |
1055
+ | `FLUXFILES_AI_PROVIDER` | No | — | `claude` or `openai` (empty = disabled) |
1056
+ | `FLUXFILES_AI_API_KEY` | No | — | AI provider API key |
1057
+ | `FLUXFILES_AI_MODEL` | No | auto | Override AI model (default: `claude-sonnet-4-20250514` / `gpt-4o`) |
1058
+ | `FLUXFILES_AI_AUTO_TAG` | No | `false` | Auto-tag images on upload |
1059
+
1060
+ ---
1061
+
1062
+ ## Project Structure
1063
+
1064
+ ```
1065
+ FluxFiles/
1066
+ ├── api/ # PHP backend
1067
+ │ ├── index.php # Router, CORS, Origin validation, JWT auth
1068
+ │ ├── FileManager.php # Core file operations (list/upload/delete/move/copy/rename/mkdir/crop/presign)
1069
+ │ ├── StorageMetadataHandler.php # Metadata + search index + audit in user storage (S3 or sidecar JSON)
1070
+ │ ├── MetadataRepositoryInterface.php
1071
+ │ ├── DiskManager.php # Flysystem factory (local/s3/r2/byob)
1072
+ │ ├── Claims.php # JWT claims value object (perms, disks, prefix, limits)
1073
+ │ ├── JwtMiddleware.php # JWT extraction + HS256 verification
1074
+ │ ├── ImageOptimizer.php # Resize + WebP variants (thumb/medium/large)
1075
+ │ ├── AiTagger.php # Claude / OpenAI vision API integration
1076
+ │ ├── ChunkUploader.php # S3 multipart upload (files > 10MB)
1077
+ │ ├── CredentialEncryptor.php # AES-256-GCM encryption for BYOB credentials
1078
+ │ ├── RateLimiterFileStorage.php # Token bucket rate limiter (file-based with flock)
1079
+ │ ├── AuditLogStorage.php # Audit log stored in user's disk (_fluxfiles/audit.jsonl)
1080
+ │ ├── QuotaManager.php # Storage quota calculation + enforcement
1081
+ │ ├── I18n.php # Locale detection, JSON translation loading, t() / tp()
1082
+ │ └── ApiException.php # HTTP error exception class
1083
+ ├── assets/
1084
+ │ ├── fm.js # Alpine.js UI component (file browser, detail panel, crop, bulk ops)
1085
+ │ └── fm.css # Styles — light/dark mode, RTL support, CSS custom properties
1086
+ ├── config/
1087
+ │ └── disks.php # Storage disk definitions (local/s3/r2)
1088
+ ├── lang/ # 16 translation files (en.json, vi.json, etc.)
1089
+ │ ├── en.json
1090
+ │ ├── vi.json
1091
+ │ ├── ...
1092
+ │ └── CONTRIBUTING.md # How to add a new language
1093
+ ├── public/
1094
+ │ └── index.html # Iframe entry point (Alpine.js + htmx)
1095
+ ├── storage/
1096
+ │ ├── uploads/ # Local disk root (gitignored)
1097
+ │ └── rate_limit.json # Rate limiter data (gitignored)
1098
+ ├── tests/ # Test suite (bash scripts + PHP unit tests)
1099
+ ├── adapters/
1100
+ │ ├── laravel/ # Composer package: fluxfiles/laravel
1101
+ │ │ ├── src/ # ServiceProvider, Facade, Controller, Middleware, Blade component
1102
+ │ │ ├── config/fluxfiles.php # Publishable config
1103
+ │ │ ├── routes/fluxfiles.php # Route definitions
1104
+ │ │ └── composer.json
1105
+ │ ├── wordpress/ # WP plugin
1106
+ │ │ ├── fluxfiles.php # Plugin header + boot
1107
+ │ │ ├── includes/ # Plugin, Admin, Api, Shortcode, MediaButton classes
1108
+ │ │ ├── templates/settings.php # Admin settings page
1109
+ │ │ └── assets/admin.css
1110
+ │ ├── react/ # npm: @fluxfiles/react (TypeScript)
1111
+ │ │ ├── src/ # FluxFiles.tsx, FluxFilesModal.tsx, useFluxFiles.ts, types.ts
1112
+ │ │ └── package.json
1113
+ │ ├── vue/ # npm: @fluxfiles/vue (TypeScript)
1114
+ │ │ ├── src/ # FluxFiles.vue, FluxFilesModal.vue, useFluxFiles.ts
1115
+ │ │ └── package.json
1116
+ │ ├── ckeditor4/ # CKEditor 4 plugin
1117
+ │ └── tinymce/ # TinyMCE 4/5 plugin
1118
+ ├── fluxfiles.js # Host app SDK (IIFE + UMD, zero dependencies)
1119
+ ├── fluxfiles.d.ts # TypeScript declarations for SDK
1120
+ ├── embed.php # PHP helpers: fluxfiles_token(), fluxfiles_embed(), fluxfiles_byob_token()
1121
+ ├── router.php # PHP built-in server router (dev mode)
1122
+ ├── composer.json # PHP dependencies
1123
+ ├── package.json # npm metadata (for SDK publishing)
1124
+ ├── .env.example # Environment template
1125
+ ├── CHANGELOG.md
1126
+ └── LICENSE # MIT
1127
+ ```
1128
+
1129
+ ---
1130
+
1131
+ ## Customization
1132
+
1133
+ | What | Where | Notes |
1134
+ |------|-------|-------|
1135
+ | **Secrets & CORS** | `.env` | `FLUXFILES_SECRET`, `FLUXFILES_ALLOWED_ORIGINS` |
1136
+ | **Storage disks** | `config/disks.php` | Add/remove disk definitions |
1137
+ | **Cloud credentials** | `.env` | `AWS_*`, `R2_*` variables |
1138
+ | **AI tagging** | `.env` | Provider, API key, model, auto-tag on upload |
1139
+ | **Branding / colors** | `assets/fm.css` | CSS custom properties (`--ff-primary`, `--ff-bg`, etc.) |
1140
+ | **UI behavior** | `assets/fm.js` | Alpine.js component — modify any behavior |
1141
+ | **SDK protocol** | `fluxfiles.js` | Event names, iframe communication |
1142
+ | **Token defaults** | `embed.php` | Default TTL, claims, signing |
1143
+ | **Image variants** | `api/ImageOptimizer.php` | Change sizes (thumb/medium/large) and quality |
1144
+ | **Rate limits** | `.env` | `FLUXFILES_RATE_LIMIT_READ`, `FLUXFILES_RATE_LIMIT_WRITE` |
1145
+ | **Translations** | `lang/*.json` | Edit existing or add new locale |
1146
+ | **Dangerous extensions** | `api/FileManager.php` | `DANGEROUS_EXTENSIONS` constant |
1147
+ | **Adapters** | `adapters/*/` | Package name, config, routes, views |
1148
+
1149
+ ---
1150
+
1151
+ ## Attribution
1152
+
1153
+ Created and maintained by **thai-pc**. If you fork or redistribute, please retain the copyright notice:
1154
+
1155
+ ```
1156
+ Based on FluxFiles by thai-pc — https://github.com/thai-pc/fluxfiles
1157
+ ```
1158
+
1159
+ ---
1160
+
1161
+ ## License
1162
+
1163
+ [MIT](LICENSE)