PHPackages                             jackardios/laravel-file-stash - 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. jackardios/laravel-file-stash

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

jackardios/laravel-file-stash
=============================

Fetch and cache files from local filesystem, cloud storage or public webservers in Laravel

015PHPCI passing

Since Mar 16Pushed 3mo agoCompare

[ Source](https://github.com/Jackardios/laravel-file-stash)[ Packagist](https://packagist.org/packages/jackardios/laravel-file-stash)[ RSS](/packages/jackardios-laravel-file-stash/feed)WikiDiscussions master Synced 3w ago

READMEChangelogDependenciesVersions (1)Used By (0)

Laravel File Stash
==================

[](#laravel-file-stash)

[![Tests](https://github.com/jackardios/laravel-file-stash/actions/workflows/tests.yml/badge.svg)](https://github.com/jackardios/laravel-file-stash/actions/workflows/tests.yml)

**Fetch and cache files from HTTP, cloud storage, or any Laravel disk — safely, even under heavy concurrency.**

---

The Problem
-----------

[](#the-problem)

You have queue workers processing jobs that need the same files — images, documents, exports. Each worker fetches its own copy. Downloads duplicate. Disk fills up. Workers corrupt each other's writes. Pruning deletes a file another worker is reading.

```
Worker A ──fetch──► image.jpg ──write──► /cache/abc123   ✗ corrupt
Worker B ──fetch──► image.jpg ──write──► /cache/abc123   ✗ overwrite
Worker C ──────────read───────────────► /cache/abc123   ✗ partial data

Pruner   ──────────delete─────────────► /cache/abc123   ✗ gone mid-read

```

The Solution
------------

[](#the-solution)

File Stash gives every worker a safe, shared file cache with proper locking:

```
Worker A ──fetch──► image.jpg ──LOCK_EX──► /cache/abc123 ──unlock──► done
Worker B ────────── (waits) ──────────────LOCK_SH──► read ──► done
Worker C ────────── (waits) ──────────────LOCK_SH──► read ──► done

Pruner   ────────── (skips locked files) ────────────────────► prune

```

**One download. Shared reads. No corruption. No race conditions.**

---

When Is This Useful?
--------------------

[](#when-is-this-useful)

File Stash helps when your application needs to **download a file and process it locally** — not just move it between storages, but actually do something with the bytes:

- **Image processing pipelines** — resize, watermark, or convert images from S3/CDN across multiple queue workers
- **PDF processing** — extract text, merge, or convert documents from cloud storage
- **ML/AI workers** — load model weights or input data from cloud storage, process locally
- **Video/audio processing** — extract thumbnails, transcode, analyze media files
- **Data import jobs** — parse CSV/Excel files from external sources in parallel workers
- **Email attachments** — fetch, attach, and discard temporary files

In all these cases, `Storage::get()` returns file contents as a string (loaded into memory). File Stash gives you a **local file path** you can pass to any tool — FFmpeg, Imagick, Python scripts, shell commands — without holding the entire file in PHP memory.

---

How It Works
------------

[](#how-it-works)

```
                          ┌───────────────────────────────┐
                          │        File Stash Cache       │
                          │   /storage/cache/files/       │
                          │                               │
  ┌─────────────┐  get()  │  ┌─────────┐   File exists?   │
  │  Worker 1   │────────►│  │ SHA-256 │──── yes ──► LOCK_SH ──► read ──► callback
  └─────────────┘         │  │  hash   │                  │
  ┌─────────────┐  get()  │  │         │──── no ───► fetch ──► LOCK_EX ──► write
  │  Worker 2   │────────►│  └─────────┘             │    │
  └─────────────┘         │                          │    │
  ┌─────────────┐  get()  │     Sources:             │    │
  │  Worker 3   │────────►│     • https://...   ◄────┘    │
  └─────────────┘         │     • s3://...                │
                          │     • local://...             │
  ┌─────────────┐ prune() │                               │
  │  Scheduler  │────────►│  File-level locks prevent     │
  └─────────────┘         │  deletion of files in use     │
                          └───────────────────────────────┘

```

Each file is identified by a SHA-256 hash of its URL. The first worker to request a file fetches and caches it; subsequent workers read the cached copy through a shared lock. File-level locks prevent pruning of files that are currently in use, and lifecycle locks coordinate destructive operations like `clear()`.

---

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

[](#installation)

```
composer require jackardios/laravel-file-stash
```

The service provider and `FileStash` facade are auto-discovered.

Publish the config (optional):

```
php artisan vendor:publish --provider="Jackardios\FileStash\FileStashServiceProvider" --tag="config"
```

**Requirements:** PHP ^8.1, Laravel ^10 / ^11 / ^12

---

Quick Start
-----------

[](#quick-start)

### Process a remote file locally

[](#process-a-remote-file-locally)

```
use FileStash;
use Jackardios\FileStash\GenericFile;

$file = new GenericFile('https://cdn.example.com/uploads/photo.jpg');

FileStash::get($file, function ($file, $cachedPath) {
    // $cachedPath is a real local path — pass it to any tool
    $image = Image::read($cachedPath);
    $image->resize(300, 200)->save(storage_path('thumbs/photo.jpg'));
});
```

### Process a file from a Laravel storage disk

[](#process-a-file-from-a-laravel-storage-disk)

Any configured disk works — S3, GCS, SFTP, local. The prefix before `://` is the disk name from `config/filesystems.php`:

```
// "reports" is the disk name from config/filesystems.php
$file = new GenericFile('reports://exports/sales-2024.xlsx');

FileStash::get($file, function ($file, $path) {
    // $path is a local file — pass it to any library that needs a file path
    $spreadsheet = IOFactory::load($path);
    $data = $spreadsheet->getActiveSheet()->toArray();
    // Process $data...
});
```

> **Note:** `reports://` refers to a Laravel disk named `reports`, not a URL protocol. Configure your disks in `config/filesystems.php`.

### Batch processing

[](#batch-processing)

Process multiple files — each is held with a shared lock so nothing gets pruned or corrupted during the callback:

```
$attachments = Attachment::where('report_id', $reportId)->get();

$files = $attachments->map(
    fn ($a) => new GenericFile($a->storage_url)
)->all();

FileStash::batch($files, function ($files, $paths) {
    // All files are locked for reading — safe from pruning and deletion
    $zip = new ZipArchive();
    $zip->open(storage_path('app/export.zip'), ZipArchive::CREATE);
    foreach ($paths as $i => $path) {
        $zip->addFile($path, basename($files[$i]->getUrl()));
    }
    $zip->close();
});
```

### One-time files (auto-cleanup)

[](#one-time-files-auto-cleanup)

Use `getOnce()` when you only need the file temporarily — it's deleted from cache after the callback:

```
$invoice = new GenericFile('https://billing.example.com/invoices/INV-2024-001.pdf');

FileStash::getOnce($invoice, function ($file, $path) {
    // Send email with attachment, then discard the cached file
    Mail::to('user@example.com')->send(new InvoiceMail($path));
});
// Cached file is automatically removed from disk
```

---

Custom File Implementations
---------------------------

[](#custom-file-implementations)

`GenericFile` works for simple cases. For domain models, implement the `File` interface directly on your Eloquent model:

```
use Jackardios\FileStash\Contracts\File;

class Document extends Model implements File
{
    public function getUrl(): string
    {
        // Remote URL
        return $this->cdn_url;

        // Or a Laravel disk path
        // return "documents://{$this->path}";
    }
}
```

Then pass models directly:

```
$document = Document::find(1);

FileStash::get($document, function ($document, $path) {
    // $document is your Eloquent model — access any attribute
    $text = (new PdfParser())->parseFile($path)->getText();
    $document->update(['extracted_text' => $text]);
});
```

---

File Sources
------------

[](#file-sources)

The URL prefix determines where the file is fetched from:

URL formatSourceExample`https://...` or `http://...`Remote HTTP via Guzzle`https://cdn.example.com/photo.jpg``diskname://path`Laravel filesystem disk`photos://uploads/photo.jpg`The `diskname` must match a key in your `config/filesystems.php` `disks` array.

> Local file paths like `/var/files/image.jpg` are not supported directly. Configure a [local disk](https://laravel.com/docs/filesystem#the-local-driver) and use `mydisk://image.jpg`.

---

Concurrency Model
-----------------

[](#concurrency-model)

File Stash is designed for environments with multiple parallel queue workers processing the same files. Here's what happens under load:

### Concurrent reads — safe

[](#concurrent-reads--safe)

```
Worker A ─── get("img.jpg") ──► LOCK_SH ──► read ──► unlock
Worker B ─── get("img.jpg") ──► LOCK_SH ──► read ──► unlock   (parallel, no waiting)
Worker C ─── get("img.jpg") ──► LOCK_SH ──► read ──► unlock

```

Multiple workers can read the same cached file simultaneously. Shared locks (`LOCK_SH`) do not block each other.

### Write during reads — safe

[](#write-during-reads--safe)

```
Worker A ─── get("new.jpg") ──► cache miss ──► LOCK_EX ──► download + write ──► unlock
Worker B ─── get("new.jpg") ──► cache miss ──► waits... ──────────────────────► LOCK_SH ──► read

```

If two workers request the same uncached file, one acquires the exclusive lock and fetches; the other waits, then reads the cached result.

### Batch + prune — safe

[](#batch--prune--safe)

```
Worker  ─── batch([a, b, c]) ──► LOCK_SH on each file ──► callback ──► unlock
Pruner  ─── prune()          ──► tries LOCK_EX on file ──► skipped (file is busy)

```

While your callback runs, each cached file is held with a shared lock (`LOCK_SH`). The pruner tries to acquire an exclusive lock (`LOCK_EX`) before deleting — if it can't, it skips the file. Your files are safe for the entire duration of the callback.

`clear()` goes further — it acquires an exclusive lifecycle lock, so it waits until all `batch()`/`get()` operations finish before deleting anything.

### Lock configuration

[](#lock-configuration)

```
// config/file-stash.php
'lock_max_attempts'      => 3,    // retries before giving up
'lock_wait_timeout'      => -1,   // seconds to wait (-1 = forever)
'lifecycle_lock_timeout' => 30,   // seconds for batch/clear coordination
```

For strict latency requirements, throw instead of waiting:

```
use Jackardios\FileStash\Exceptions\FileLockedException;

try {
    FileStash::get($file, $callback, throwOnLock: true);
} catch (FileLockedException) {
    // File is busy, handle gracefully
}
```

---

API Reference
-------------

[](#api-reference)

### Core Methods

[](#core-methods)

```
// Cache and use a file
FileStash::get(File $file, ?callable $callback, bool $throwOnLock = false): mixed

// Cache, use, then delete
FileStash::getOnce(File $file, ?callable $callback, bool $throwOnLock = false): mixed

// Cache and use multiple files (prune-safe)
FileStash::batch(array $files, ?callable $callback, bool $throwOnLock = false): mixed

// Cache, use, then delete multiple files
FileStash::batchOnce(array $files, ?callable $callback, bool $throwOnLock = false): mixed
```

### Cache Management

[](#cache-management)

```
FileStash::exists(File $file): bool          // Check if a file's source exists
FileStash::forget(File $file): bool          // Remove a specific cached file
FileStash::prune(): array                     // Remove expired/oversized files
FileStash::clear(): void                      // Delete all unused cached files
FileStash::metrics(): CacheMetrics            // Get hit/miss/eviction counters
```

---

Pruning
-------

[](#pruning)

Pruning removes cached files that are older than `max_age` or exceed the `max_size` limit.

### Automatic pruning

[](#automatic-pruning)

The service provider registers a scheduled command that runs automatically. By default, it prunes every 5 minutes:

```
// config/file-stash.php
'prune_interval' => '*/5 * * * *',
```

Make sure Laravel's scheduler is running:

```
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
```

### Manual pruning

[](#manual-pruning)

Run the artisan command:

```
php artisan prune-file-stash
```

Or call it programmatically:

```
$stats = FileStash::prune();
// ['deleted' => 12, 'remaining' => 48, 'total_size' => 524288000, 'completed' => true]
```

### Clearing all cache

[](#clearing-all-cache)

```
FileStash::clear();           // Delete all unused cached files
FileStash::forget($file);     // Remove a specific file
```

The cache is also cleared automatically when you run `php artisan cache:clear`.

---

Events
------

[](#events)

Enable event dispatching for observability:

```
// config/file-stash.php
'events_enabled' => true,
```

EventWhenKey properties`CacheHit`File served from cache`$file`, `$cachedPath``CacheMiss`File not cached, fetching`$file`, `$url``CacheFileRetrieved`File successfully cached`$file`, `$cachedPath`, `$bytes`, `$source``CacheFileEvicted`File deleted from cache`$path`, `$reason``CachePruneCompleted`Prune finished`$deleted`, `$remaining`, `$totalSize`, `$completed`All events are in the `Jackardios\FileStash\Events` namespace.

When `events_enabled` is `false` (default), zero overhead — no objects allocated, no dispatching.

```
use Jackardios\FileStash\Events\CacheHit;

Event::listen(CacheHit::class, function (CacheHit $event) {
    Log::info("Cache hit: {$event->file->getUrl()}");
});
```

---

Metrics
-------

[](#metrics)

Track cache effectiveness per-process:

```
$metrics = FileStash::metrics();

$metrics->hits;        // cache hits
$metrics->misses;      // cache misses
$metrics->retrievals;  // files fetched from source
$metrics->evictions;   // files removed
$metrics->errors;      // failed retrievals

$metrics->hitRate();   // float|null — percentage
$metrics->toArray();   // all counters as array
$metrics->reset();     // zero out counters
```

Push to your monitoring stack:

```
app()->terminating(function () {
    $m = FileStash::metrics()->toArray();
    // Send to Prometheus, StatsD, Datadog, etc.
});
```

---

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

[](#configuration)

All settings support environment variables. Publish the config to customize:

```
php artisan vendor:publish --provider="Jackardios\FileStash\FileStashServiceProvider" --tag="config"
```

### Cache limits

[](#cache-limits)

KeyEnvDefaultDescription`path`—`storage/framework/cache/files`Cache directory`max_file_size``FILE_STASH_MAX_FILE_SIZE``-1` (unlimited)Max file size in bytes`max_age``FILE_STASH_MAX_AGE``60`TTL in minutes before pruning`max_size``FILE_STASH_MAX_SIZE``1E+9` (1 GB)Soft limit for total cache size### HTTP

[](#http)

KeyEnvDefaultDescription`timeout``FILE_STASH_TIMEOUT``-1`Total request timeout (seconds)`connect_timeout``FILE_STASH_CONNECT_TIMEOUT``30`Connection timeout (seconds)`read_timeout``FILE_STASH_READ_TIMEOUT``30`Stream read timeout (seconds)`http_retries``FILE_STASH_HTTP_RETRIES``0`Retry attempts (4xx except 429 not retried)`http_retry_delay``FILE_STASH_HTTP_RETRY_DELAY``100`Base delay in ms (exponential backoff)`user_agent``FILE_STASH_USER_AGENT``Laravel-FileStash/4.x`User-Agent header`max_redirects``FILE_STASH_MAX_REDIRECTS``5`Max redirects to follow### Security

[](#security)

KeyEnvDefaultDescription`allowed_hosts``FILE_STASH_ALLOWED_HOSTS``null` (all)Host whitelist for SSRF protection`mime_types`—`[]` (all)Allowed MIME types```
// Wildcards supported
'allowed_hosts' => ['example.com', '*.cdn.example.com'],
```

```
# Comma-separated in .env
FILE_STASH_ALLOWED_HOSTS=example.com,*.cdn.example.com
```

### Concurrency

[](#concurrency)

KeyEnvDefaultDescription`lock_max_attempts``FILE_STASH_LOCK_MAX_ATTEMPTS``3`Lock acquisition retries`lock_wait_timeout``FILE_STASH_LOCK_WAIT_TIMEOUT``-1` (forever)Lock wait timeout (seconds)`lifecycle_lock_timeout``FILE_STASH_LIFECYCLE_LOCK_TIMEOUT``30`Batch/clear coordination timeout`batch_chunk_size``FILE_STASH_BATCH_CHUNK_SIZE``100`Files per chunk (prevents fd exhaustion)### Pruning

[](#pruning-1)

KeyEnvDefaultDescription`prune_interval``FILE_STASH_PRUNE_INTERVAL``*/5 * * * *`Cron schedule for auto-pruning`prune_timeout``FILE_STASH_PRUNE_TIMEOUT``300`Prune timeout (seconds)### Performance

[](#performance)

KeyEnvDefaultDescription`touch_interval``FILE_STASH_TOUCH_INTERVAL``60`Min seconds between `touch()` on hot files`events_enabled``FILE_STASH_EVENTS_ENABLED``false`Enable event dispatching---

Exceptions
----------

[](#exceptions)

All exceptions are in `Jackardios\FileStash\Exceptions` with `public readonly` properties for structured handling:

ExceptionWhenProperties`FileIsTooLargeException`File exceeds `max_file_size``int $maxBytes``FileLockedException`File locked, `throwOnLock` is `true`—`HostNotAllowedException`Host not in `allowed_hosts``string $host``MimeTypeIsNotAllowedException`MIME type not allowed`string $mimeType``InvalidConfigurationException`Invalid config value`string $key`, `string $reason``SourceResourceIsInvalidException`Invalid stream resource—`SourceResourceTimedOutException`Stream read timed out—`FailedToRetrieveFileException`All retries exhausted—```
use Jackardios\FileStash\Exceptions\HostNotAllowedException;

try {
    FileStash::get($file, $callback);
} catch (HostNotAllowedException $e) {
    Log::warning("Blocked: {$e->host}");
}
```

---

Testing
-------

[](#testing)

The facade ships with a fake that skips real HTTP/disk operations:

```
use FileStash;
use Jackardios\FileStash\GenericFile;

public function test_it_processes_file(): void
{
    FileStash::fake();

    $file = new GenericFile('https://example.com/image.jpg');

    $result = FileStash::get($file, fn ($file, $path) => 'processed');

    $this->assertEquals('processed', $result);
}
```

The fake supports all contract methods: `get`, `getOnce`, `batch`, `batchOnce`, `forget`, `exists`, `prune`, `clear`, `metrics`.

---

Acknowledgements
----------------

[](#acknowledgements)

This package is based on [biigle/laravel-file-cache](https://github.com/biigle/laravel-file-cache) by Martin Zurowietz. It extends the original with lifecycle locks, batch chunking, events, metrics, SSRF protection, HTTP retries, and other improvements.

---

License
-------

[](#license)

MIT

###  Health Score

20

—

LowBetter than 13% of packages

Maintenance54

Moderate activity, may be stable

Popularity7

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity12

Early-stage or recently created project

 Bus Factor1

Top contributor holds 52.5% 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.

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/24757335?v=4)[Salavat Salakhutdinov](/maintainers/jackardios)[@Jackardios](https://github.com/Jackardios)

---

Top Contributors

[![mzur](https://avatars.githubusercontent.com/u/2457311?v=4)](https://github.com/mzur "mzur (64 commits)")[![Jackardios](https://avatars.githubusercontent.com/u/24757335?v=4)](https://github.com/Jackardios "Jackardios (58 commits)")

### Embed Badge

![Health badge](/badges/jackardios-laravel-file-stash/health.svg)

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

PHPackages © 2026

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