PHPackages                             jsdevart/laravel-file-vault - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. jsdevart/laravel-file-vault

ActiveLibrary[File &amp; Storage](/categories/file-storage)

jsdevart/laravel-file-vault
===========================

Private file storage for Laravel with signed URLs and multi-context.

v2.1.0(2mo ago)0218MITPHPPHP ^8.2

Since Apr 9Pushed 2mo agoCompare

[ Source](https://github.com/JSDevArt/laravel-file-vault)[ Packagist](https://packagist.org/packages/jsdevart/laravel-file-vault)[ Docs](https://github.com/JSDevArt/laravel-file-vault)[ RSS](/packages/jsdevart-laravel-file-vault/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (4)Versions (4)Used By (0)

Laravel File Vault
==================

[](#laravel-file-vault)

Private file storage for Laravel with signed URLs, multi-context organization, and local/S3 support.

Requirements
------------

[](#requirements)

- PHP 8.2+ for Laravel 11–12; **PHP 8.4+** when you use Laravel 13 (Symfony 8 components require it)
- Laravel 11, 12, or 13

Installation
------------

[](#installation)

```
composer require jsdevart/laravel-file-vault
```

The package registers itself automatically via Laravel's package discovery.

Publish the config file:

```
php artisan vendor:publish --tag=file-vault-config
```

Configuration
-------------

[](#configuration)

`config/file-vault.php`:

KeyDefaultDescription`disk``null` (uses `filesystems.default`)Storage disk to use (`local`, `s3`, etc.)`expiry_minutes``60`Signed URL expiry time in minutes`force_serve_route``false`If `true`, `getUrl()` always uses the app signed route (see below) instead of S3 presigned URLs`serve_route``files.serve`Route name that serves private files when using the signed routeEnvironment variables:

```
FILE_VAULT_DISK=local
FILE_VAULT_EXPIRY_MINUTES=60
FILE_VAULT_SERVE_ROUTE=files.serve
FILE_VAULT_FORCE_SERVE_ROUTE=false
```

Set `FILE_VAULT_FORCE_SERVE_ROUTE=true` when your storage disk uses an **internal** endpoint (for example MinIO or S3 at a Docker-only hostname). Presigned URLs would point at that host and fail in the browser; forcing the signed route makes `getUrl()` return an URL on your app, which can stream the file via `Storage` in your serve controller.

Usage
-----

[](#usage)

### 1. Create a storage service for your context

[](#1-create-a-storage-service-for-your-context)

Copy `stubs/ExampleStorageService.php` into your application and adjust it:

```
// app/Services/Storage/UserStorageService.php

namespace App\Services\Storage;

use JSDevArt\LaravelFileVault\Services\BaseFileStorageService;

class UserStorageService extends BaseFileStorageService
{
    public function __construct()
    {
        parent::__construct('user'); // root folder in storage
    }

    protected function buildBasePath(string $element): string
    {
        return match ($element) {
            'picture'  => 'pictures',
            'document' => 'documents',
            default    => $element,
        };
    }
}
```

Files will be stored at: `{context}/{element_folder}/XX/XX/{uuid}.{ext}`

Example: `user/pictures/aB/cD/3f2a...uuid.jpg`

### 2. Register your services

[](#2-register-your-services)

In your `AppServiceProvider`:

```
use JSDevArt\LaravelFileVault\FileStorageRegistry;
use App\Services\Storage\UserStorageService;

public function boot(): void
{
    app(FileStorageRegistry::class)->register(
        'user',
        new UserStorageService(),
    );
}
```

You can also pass a custom disk per registration, without creating a subclass:

```
// Uses the named-argument form of the parent constructor
app(FileStorageRegistry::class)->register(
    'imports',
    new ImportStorageService(disk: 's3', context: 'imports'),
);
```

### 3. Use in your controllers

[](#3-use-in-your-controllers)

```
use JSDevArt\LaravelFileVault\FileStorageRegistry;

// Store a file
$service = app(FileStorageRegistry::class)->get('user');
$path = $service->store('picture', $request->file('photo')->get(), 'jpg');
$user->update(['photo_path' => $path]);

// Generate a signed URL (expires per config)
$url = app(FileStorageRegistry::class)->get('user')->getUrl($user->photo_path);

// Delete a file
app(FileStorageRegistry::class)->get('user')->delete($user->photo_path);

// Check existence
app(FileStorageRegistry::class)->get('user')->exists($user->photo_path);
```

### 4. Display the URL in a view or API response

[](#4-display-the-url-in-a-view-or-api-response)

```
// In a model accessor (recommended)
public function getPhotoUrlAttribute(): ?string
{
    return $this->photo_path
        ? app(FileStorageRegistry::class)->get('user')->getUrl($this->photo_path)
        : null;
}
```

### 5. Get an absolute on-disk path (`getPath`)

[](#5-get-an-absolute-on-disk-path-getpath)

Some libraries (e.g. `spatie/simple-excel`, PhpSpreadsheet) require a real file-system path rather than file contents. Use `getPath()` to get one regardless of where the file lives:

```
use JSDevArt\LaravelFileVault\FileStorageRegistry;

$storage = app(FileStorageRegistry::class)->get('imports');

// Returns FilePathResult|null  (null when the file does not exist)
$result = $storage->getPath($filePath);

if ($result === null) {
    abort(404);
}

// Keep $result in scope for as long as you need $result->path.
// If the file lives on a remote disk the package created a local temp file;
// it is deleted automatically when $result goes out of scope.
$rows = SimpleExcelReader::create($result->path)
    ->headersToSnakeCase()
    ->getRows()
    ->collect();
```

**How it works:**

Disk driverBehaviour`local`Returns the absolute path directly via `Storage::path()` — no copy is made.Remote (S3, GCS, …)Downloads the file to a temp file in `sys_get_temp_dir()`. The original file extension is preserved so format-detection (`.xlsx`, `.csv`, …) works correctly.**Why `FilePathResult` instead of a plain string?**

The temp file's lifetime is tied to the `FilePathResult` object. If you extracted just the path string — `$path = $storage->getPath($file)->path` — the result object would be immediately garbage-collected, deleting the temp file before you use it. Keeping the full object in a named variable prevents that.

```
// Correct — $result stays alive while $result->path is used
$result = $storage->getPath($file);
process($result->path);

// Incorrect — temp file deleted before process() runs
process($storage->getPath($file)->path);
```

Serving private files (local disk)
----------------------------------

[](#serving-private-files-local-disk)

The package does **not** provide a controller. You are responsible for implementing the route that streams private files. This gives you full control over middleware, authorization, and response headers.

The example below uses the same disk as File Vault (`file-vault.disk` or your default filesystem disk). It assumes a **local** (or otherwise path-based) disk, because `Storage::disk(...)->path()` is not meaningful for pure S3-style drivers.

Reference implementation:

```
// routes/web.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;

Route::get('/files/{path}', function (Request $request, string $path) {
    if (! $request->hasValidSignature()) {
        abort(403);
    }

    $disk    = config('file-vault.disk') ?? config('filesystems.default');
    $decoded = base64_decode($path);

    if (! $decoded || ! Storage::disk($disk)->exists($decoded)) {
        abort(404);
    }

    $filePath = Storage::disk($disk)->path($decoded);
    $mimeType = mime_content_type($filePath) ?: 'application/octet-stream';

    return response()->stream(function () use ($filePath) {
        $stream = fopen($filePath, 'rb');
        fpassthru($stream);
        fclose($stream);
    }, 200, [
        'Content-Type'        => $mimeType,
        'Content-Length'      => Storage::disk($disk)->size($decoded),
        'Cache-Control'       => 'private, max-age=3600',
        'Content-Disposition' => 'inline; filename="'.basename($decoded).'"',
    ]);
})->name('files.serve')->where('path', '.*');
```

Make sure the route name matches `file-vault.serve_route` in your config (default: `files.serve`).

For **S3** (and other cloud disks), `getUrl()` normally delegates to the driver's native `temporaryUrl()`. With `force_serve_route` enabled, it uses the same signed app route as the local driver so you can proxy the object through Laravel.

###  Health Score

42

—

FairBetter than 89% of packages

Maintenance86

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 75% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

3

Last Release

72d ago

Major Versions

v1.0.0 → v2.0.02026-04-09

### Community

Maintainers

![](https://www.gravatar.com/avatar/9ea9ba821d35e93223522b1a6cd9441fc8dfdd234df7850c552c5d8f6c6f2b4d?d=identicon)[jackpump](/maintainers/jackpump)

---

Top Contributors

[![layer7jack](https://avatars.githubusercontent.com/u/265355626?v=4)](https://github.com/layer7jack "layer7jack (6 commits)")[![jsarturo](https://avatars.githubusercontent.com/u/109831522?v=4)](https://github.com/jsarturo "jsarturo (2 commits)")

---

Tags

laravels3file-uploadsigned-urlfile storageprivate-files

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/jsdevart-laravel-file-vault/health.svg)

```
[![Health](https://phpackages.com/badges/jsdevart-laravel-file-vault/health.svg)](https://phpackages.com/packages/jsdevart-laravel-file-vault)
```

###  Alternatives

[slimani/filament-media-manager

A media manager plugin for Filament.

115.1k](/packages/slimani-filament-media-manager)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
