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/LICENSE +21 -0
- package/README.md +1163 -0
- package/fluxfiles.d.ts +77 -0
- package/fluxfiles.js +231 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
# FluxFiles
|
|
2
|
+
|
|
3
|
+
[](https://packagist.org/packages/fluxfiles/fluxfiles)
|
|
4
|
+
[](https://packagist.org/packages/fluxfiles/laravel)
|
|
5
|
+
[](https://www.npmjs.com/package/fluxfiles)
|
|
6
|
+
[](https://www.npmjs.com/package/@fluxfiles/react)
|
|
7
|
+
[](https://www.npmjs.com/package/@fluxfiles/vue)
|
|
8
|
+
[](https://packagist.org/packages/fluxfiles/fluxfiles)
|
|
9
|
+
[](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[]|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)
|