fluxfiles/laravel

Laravel adapter for FluxFiles file manager

Maintainers

Package info

github.com/thai-pc/fluxfiles-laravel

Homepage

Issues

pkg:composer/fluxfiles/laravel

Statistics

Installs: 31

Dependents: 0

Suggesters: 0

Stars: 0

v0.2.21 2026-06-24 16:52 UTC

README

Laravel adapter for FluxFiles — a standalone, embeddable file manager with multi-storage support (Local, AWS S3, Cloudflare R2).

Requirements

  • PHP >= 8.1 (matches fluxfiles/fluxfiles)
  • Laravel 10, 11, or 12

Installation

composer require fluxfiles/laravel

Publish the config file:

php artisan vendor:publish --tag=fluxfiles-config

Add to your .env:

FLUXFILES_SECRET=your-random-32-char-secret

For the default local disk, expose Laravel public storage once:

php artisan storage:link

The default local disk writes to storage/app/public/fluxfiles/uploads and returns URLs under /storage/fluxfiles/uploads.

Cloud storage (optional)

The s3 and r2 disks read these from .env (see config/fluxfiles.php):

# AWS S3 — or any S3-compatible (set AWS_ENDPOINT for MinIO / DO Spaces)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=ap-southeast-1
AWS_BUCKET=
AWS_ENDPOINT=                 # empty = native AWS S3
AWS_VISIBILITY=private        # 'public' = direct object URLs
AWS_PUBLIC_URL=               # CDN / custom domain for a public disk

# Cloudflare R2
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_ACCOUNT_ID=
R2_BUCKET=
R2_VISIBILITY=private
R2_PUBLIC_URL=                # r2.dev or custom domain for a public bucket

Modes

Mode Description
proxy (default) FluxFiles API runs through Laravel routes — no separate server needed
standalone FluxFiles runs on its own server; Laravel only generates tokens and embeds the iframe

Proxy mode: exclude the route prefix from CSRF or uploads/deletes return 419 — see CSRF exclusion.

Set mode in .env:

FLUXFILES_MODE=proxy
# or
FLUXFILES_MODE=standalone
FLUXFILES_ENDPOINT=https://your-fluxfiles-server.com

Usage

Blade Component

<x-fluxfiles
    disk="local"
    mode="picker"
    width="100%"
    height="600px"
    @select="handleFileSelect"
/>

Generate Token

use FluxFiles\Laravel\FluxFilesFacade as FluxFiles;

// For the current authenticated user
$token = FluxFiles::tokenForUser();

// With custom overrides
$token = FluxFiles::token(auth()->id(), [
    'perms'      => ['read', 'write'],
    'disks'      => ['local', 's3'],
    'prefix'     => 'user-123/',
    'max_upload'  => 20,    // MB — per uploaded file
    'max_storage' => 1000,  // MB — total quota per prefix (0 = unlimited)
    'max_files'   => 0,     // max files per prefix (0 = unlimited)
    'allowed_ext' => ['jpg', 'png', 'pdf'], // lowercase, no dot; null = all safe
    'ttl'         => 7200,  // seconds — token lifetime (7200 = 2 hours)
]);

Enable Import from URL

Import-from-URL is off by default. There's nothing to install or configure server-side per tenant — enabling it is a token claim. Add the import claims to the override array:

$token = FluxFiles::token(auth()->id(), [
    'perms'                => ['read', 'write'],
    'allow_url_import'     => true,                 // required — turns the feature on
    'max_import_mb'        => 20,                   // optional — cap per import (MB)
    'import_url_allowlist' => ['*.unsplash.com'],   // optional — restrict source hosts
    // 'import_path' => 'imports', 'import_rate_limit' => 10, 'import_concurrency' => 3
]);

The core then accepts POST /api/fm/import-url ({ "url": "…", "path": "…" }) for that token — SSRF-guarded and sharing the quota/dedup/variants pipeline. Server-wide defaults (when a claim is omitted) come from FLUXFILES_IMPORT_* env vars on the core service.

Per-tenant configuration

FluxFiles is stateless — the token is the per-tenant config. There's no config file or table per customer; you mint a different token, and FluxFiles enforces its claims server-side. Drive the values from the tenant's plan:

use FluxFiles\Laravel\FluxFilesFacade as FluxFiles;

$tenant = auth()->user()->tenant;        // your own model

$claims = match ($tenant->plan) {
    // Free — 5 MB/file, images only, 500 MB quota, 200 files
    'free' => [
        'disks'       => ['local'],
        'max_upload'  => 5,
        'allowed_ext' => ['jpg', 'jpeg', 'png', 'webp'],
        'max_storage' => 500,
        'max_files'   => 200,
    ],
    // Pro — 100 MB/file, any safe type, 50 GB quota, unlimited files
    'pro' => [
        'disks'       => ['s3'],
        'max_upload'  => 100,
        'allowed_ext' => null,
        'max_storage' => 51200,
        'max_files'   => 0,
        'ai_auto_tag' => true,             // AI captions/tags on upload
        'rate_read'   => 240,              // higher API rate limit
        'variants'    => ['large' => 2560], // bigger preview variant
    ],
};

$token = FluxFiles::token(auth()->id(), [
    'prefix' => "tenant_{$tenant->id}/",   // isolates each tenant's files
    'perms'  => ['read', 'write', 'delete'],
    ...$claims,
]);

Always derive prefix from the authenticated tenant server-side — never from client input. For full isolation, give each tenant their own bucket with BYOB (byob_disks). See the root README's Multi-tenant section for the full claim list, and Permissions for the storage each tenant's _fluxfiles/ index needs.

The ai_auto_tag, rate_read / rate_write and variants overrides are enforced by core ≥ 0.2.8 — on an older core they're simply ignored (the adapter still issues a valid token, it just won't crash). Run composer update fluxfiles/fluxfiles to pick them up.

Blade Directives

<script>
FluxFiles.open({
    endpoint: '@fluxfilesEndpoint',
    token: '@fluxfilesToken',
    disk: 'local',
    mode: 'picker'
});
</script>

Facade Methods

use FluxFiles\Laravel\FluxFilesFacade as FluxFiles;

FluxFiles::token($user, $overrides);   // Generate JWT token
FluxFiles::tokenForUser($overrides);   // Token for auth user
FluxFiles::endpoint();                  // Get FluxFiles URL
FluxFiles::iframeSrc();                 // Get iframe source URL
FluxFiles::sdkUrl();                    // Get SDK script URL

Configuration

After publishing, edit config/fluxfiles.php:

return [
    'secret'     => env('FLUXFILES_SECRET'),
    'mode'       => env('FLUXFILES_MODE', 'proxy'),
    'endpoint'   => env('FLUXFILES_ENDPOINT'),

    'route_prefix' => 'api/fm',
    'middleware'    => ['web', 'auth'],

    'disks' => [
        'local' => [...],
        's3'    => [...],
        'r2'    => [...],
    ],

    'defaults' => [
        'perms'       => ['read', 'write', 'delete'],
        'disks'       => ['local'],
        'prefix'      => '',
        'max_upload'  => 10,    // MB
        'allowed_ext' => null,  // null = allow all
        'max_storage' => 0,     // 0 = unlimited
        'ttl'         => 3600,  // seconds
    ],

    'locale'      => env('FLUXFILES_LOCALE', ''),
    'ai_provider' => env('FLUXFILES_AI_PROVIDER', ''),
    'ai_api_key'  => env('FLUXFILES_AI_API_KEY', ''),
    'ai_model'    => env('FLUXFILES_AI_MODEL', ''),
    'ai_auto_tag' => env('FLUXFILES_AI_AUTO_TAG', false),
];

CSRF exclusion (proxy mode only)

In proxy mode the routes run under the web middleware group (needed for the session-based auth bridge), which includes Laravel's CSRF protection. The FluxFiles SDK authenticates every call with an Authorization: Bearer <jwt> header — not a Laravel CSRF token — so the mutating routes (upload, delete, move, rename, …) return 419 Page Expired until you exclude the FluxFiles route prefix from CSRF. Use the value of config('fluxfiles.route_prefix') (default api/fm) with a /* wildcard.

The file/location differs by Laravel version:

Laravel 11 / 12 — there is no VerifyCsrfToken.php; configure it in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'api/fm/*',   // = config('fluxfiles.route_prefix') . '/*'
    ]);
})

Laravel 9 / 10 — add it to the $except array in app/Http/Middleware/VerifyCsrfToken.php:

protected $except = [
    'api/fm/*',   // = config('fluxfiles.route_prefix') . '/*'
];

Standalone mode is unaffected — the core runs as its own server with its own Origin-based CSRF check, and Laravel only mints tokens / embeds the iframe.

Permissions

FluxFiles keeps all of its state on disk (no database). Two locations must be writable by the user PHP-FPM runs as (usually www-data):

Path Holds Created when
<local disk root>/_fluxfiles/ search index, folder index, file locks, audit log, trash manifest, metadata sidecars first write to that disk (upload / mkdir / …)
config('fluxfiles.storage_path') (default storage/fluxfiles/) proxy-mode rate-limiter counter (rate_limit.json) first request — see the dedicated section below

The _fluxfiles/ directory lives inside the disk root you configure (e.g. public_path('uploads')public/uploads/_fluxfiles/). PHP creates it on the first write, so the safest rule is: let PHP create it — don't pre-create it as root or your deploy user.

# Make the uploads tree writable by the web server user (adjust www-data to your
# PHP-FPM user: `ps aux | grep php-fpm | grep -v root`)
sudo chown -R www-data:www-data /path/to/public/uploads
sudo chmod -R u+rwX,g+rwX /path/to/public/uploads

Symptom → fix. A 500 with fopen(.../_fluxfiles/index.lock): Permission denied (or, on core ≥ 0.2.7, a clean storage_not_writable error naming the path) means the web server user can't write _fluxfiles/. Almost always the directory was pre-created by a different user — chown it back to the PHP-FPM user (or delete it and let PHP recreate it):

sudo chown -R www-data:www-data /path/to/public/uploads/_fluxfiles

If the disk root is inside public/, make sure _fluxfiles/ is not served: its contents are internal (the index can reveal file names). For nginx:

location ~ /_fluxfiles/ { deny all; }

On S3 / R2 disks there is nothing to chmod — _fluxfiles/ lives in the bucket as regular objects, governed by your IAM policy (s3:PutObject etc.). Run the Bucket Doctor to verify those grants.

Deployment & permissions (rate_limit.json)

In proxy mode the rate limiter keeps its counter in a JSON file at config('fluxfiles.storage_path') — by default storage/fluxfiles/rate_limit.json (override with FLUXFILES_STORAGE_PATH). PHP creates the directory 0755 and the file 0600 automatically on the first request.

What you need on the server:

  • The directory must be writable by the user PHP-FPM runs as (usually www-data). Laravel's storage/ already requires this, so the standard deploy perms cover it:

    sudo chown -R www-data:www-data storage bootstrap/cache
    sudo chmod -R 775 storage bootstrap/cache
  • Let PHP create rate_limit.json itself. It's chmod-ed to 0600 (owner only), so it must be owned by the PHP-FPM user. If a deploy script or root pre-creates it as another user, PHP-FPM can't read it and every request fails with 500 "Rate limiter unavailable". Fix:

    sudo chown www-data:www-data storage/fluxfiles/rate_limit.json   # or just delete it; PHP recreates it
  • No web-server rule needed. Unlike the standalone core, this file lives under Laravel's storage/ (outside the public/ web root), so it is never served. Keep the 0600 mode — don't loosen it.

  • Read-only / immutable deploys (e.g. containers): point the path at a writable volume —

    FLUXFILES_STORAGE_PATH=/var/lib/fluxfiles
    sudo install -d -o www-data -g www-data -m 775 /var/lib/fluxfiles

Standalone mode (running the core server directly) puts the file at packages/core/storage/rate_limit.json instead — there it is under the web root, so block it at the web server (location /storage/rate_limit.json { deny all; }).

Using an existing upload directory

If your app already has a directory tree like public/uploads/user_1/, public/uploads/user_2/ (populated before FluxFiles was installed), you can point FluxFiles at it — existing files show up immediately, and a one-shot Artisan command makes them searchable.

1. Point the local disk at your existing path

In config/fluxfiles.php:

'disks' => [
    'local' => [
        'driver' => 'local',
        'root'   => public_path('uploads'),     // where your files already live
        'url'    => '/uploads',                 // URL prefix for preview links
    ],
],

2. Scope each user to their own sub-folder via the prefix claim

Always derive the prefix server-side from the authenticated user — never trust client input:

use FluxFiles\Laravel\FluxFilesFacade as FluxFiles;

$token = FluxFiles::tokenForUser([
    'prefix' => 'user_' . auth()->id() . '/',
    'disks'  => ['local'],
    'perms'  => ['read', 'write', 'delete'],
]);

With prefix = 'user_1/', all API paths are transparently scoped to public/uploads/user_1/. User 1 cannot see or touch user_2/.

3. Filesystem permissions

Make public/uploads writable by the PHP process (upload / mkdir / delete):

chown -R www-data:www-data public/uploads
chmod -R u+rwX public/uploads

This also covers the _fluxfiles/ index directory FluxFiles writes inside the root — see Permissions for the common _fluxfiles/index.lock permission-denied symptom and fix.

4. Seed metadata + folder index for pre-existing content

Listing existing files works out of the box. Preview links load only when the disk url matches a path your web server actually serves. Search relies on the FluxFiles metadata index (_fluxfiles/index.json) and the directory index (_fluxfiles/dirs.json), which are only written when content is created through the API. To make pre-existing files and folders searchable, run the included Artisan command once:

# Dry run first — report what would be indexed, no writes
php artisan fluxfiles:seed --disk=local --dry-run

# Apply
php artisan fluxfiles:seed --disk=local

# Generate thumbnails for existing images
php artisan fluxfiles:seed --disk=local --variants

# Add hashes for duplicate detection too
php artisan fluxfiles:seed --disk=local --hash --variants

# Only a sub-tree
php artisan fluxfiles:seed --disk=local --path=user_1

# Force re-index (overwrite any existing metadata)
php artisan fluxfiles:seed --disk=local --overwrite

What it does:

  • Walks the disk recursively (skipping _fluxfiles/, _variants/, and *.meta.json).
  • For each file: creates a metadata record with title derived from the filename so search can find it. Metadata writes are skipped for files that already have metadata unless --overwrite is passed, but --hash and --variants can still fill in missing hashes/thumbnails.
  • For each folder: tracks it in _fluxfiles/dirs.json so folder search (/api/fm/search-folders) can return it.

After seeding, both file and folder search work for the existing tree.

5. Notes & gotchas

  • FluxFiles auto-creates public/uploads/_fluxfiles/ (metadata index and audit log) and public/uploads/_variants/ (image thumbnails). These are hidden from the UI — do not delete them. If you use FTP/rsync/backup tools, add them to your ignore list.
  • url = '/uploads' must match how your web server serves public/. Preview links are built as {url}/{key} — e.g. file key user_1/avatar.jpg/uploads/user_1/avatar.jpg.
  • Files uploaded before seeding won't have an uploaded_by metadata field. If you later enable owner_only, legacy files fall through gracefully (all users can act on them) until the next time someone edits them through the UI.
  • For S3/R2 disks with an existing bucket, the same seed command works — pass --disk=s3 (or --disk=r2). Listing is slower because it pages the bucket remotely.

Features

  • Blade component <x-fluxfiles> with auto token generation
  • Blade directives @fluxfilesToken and @fluxfilesEndpoint
  • Facade FluxFiles::token() for programmatic token generation
  • Proxy mode — serve FluxFiles API through Laravel routes
  • Standalone mode — connect to a separate FluxFiles server
  • Auto-discovery — ServiceProvider and Facade register automatically
  • 16 languages — en, vi, zh, ja, ko, fr, de, es, ar, pt, it, ru, th, hi, tr, nl

License

MIT — see LICENSE for details.

Links