Search by

heyjorgedev / sqlite-flysystem-driver

heyjorgedev

A Flysystem storage driver backed by SQLite.

Package info

github.com/heyjorgedev/sqlite-flysystem-driver

pkg:composer/heyjorgedev/sqlite-flysystem-driver

Statistics

Installs: 126

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-09-16 22:49 UTC

This package is auto-updated.

Last update: 2026-10-12 13:08:21 UTC


README

Tests

A Flysystem 3 storage adapter that keeps your files inside a single SQLite database, with an optional Laravel disk driver that can serve public files over HTTP.

It works well when you want uploads to live next to the rest of your app's data: one file to back up, copy or ship, with no object storage or shared volume to set up.

  • Passes Flysystem's official adapter conformance suite.
  • Supports checksums (ChecksumProvider) and public URLs (PublicUrlGenerator).
  • Metadata reads, fileExists() and listings never load file contents, so they cost the same for a 1 KB file and a 1 GB file.
  • Streams byte ranges without loading the whole file into memory.
  • Laravel driver with an optional static-file route that sends ETag/Last-Modified and handles 304s and Range requests.

Requirements

  • PHP 8.2+ with pdo_sqlite
  • league/flysystem ^3.7
  • Optional: ext-sqlite3, which enables incremental blob streaming (see Streaming)
  • Optional: Laravel 12 or 13 for the disk driver

Installation

composer require heyjorgedev/sqlite-flysystem-driver

Usage with Flysystem

use HeyJorgeDev\SqliteFlysystem\SqliteAdapter;
use League\Flysystem\Filesystem;

$filesystem = new Filesystem(new SqliteAdapter(__DIR__ . '/storage/files.sqlite'));

$filesystem->write('avatars/jorge.png', $contents);
$filesystem->read('avatars/jorge.png');
$filesystem->listContents('avatars', deep: true);

The schema is created on first use. The constructor takes:

Argument Default Description
$connection required A database file path, ':memory:', or an existing PDO instance.
$table 'files' Table to store files in, so several adapters can share one database.
$mimeTypeDetector FinfoMimeTypeDetector Any League\MimeTypeDetection\MimeTypeDetector.
$urlPrefix null Enables publicUrl() (e.g. https://example.com/files).

Passing a file path vs a PDO

When you pass a path, the adapter owns the connection and tunes it for file storage. It always sets busy_timeout, and for file databases it also enables WAL with synchronous=NORMAL, memory-maps up to 256 MB, and uses 16 KB pages when it creates the file.

When you pass a PDO, the adapter leaves its pragmas alone. Use this to share a connection your app already manages, or for an in-memory database in tests:

$adapter = new SqliteAdapter(new PDO('sqlite::memory:'));

An in-memory database exists only for the lifetime of its connection.

Streaming

readStream() and streamContents() read files in chunks instead of loading them whole:

// Yields up to 64 KB at a time, starting at byte 1024, for 1 MB total.
foreach ($adapter->streamContents('videos/intro.mp4', 65536, 1024, 1048576) as $chunk) {
    echo $chunk;
}

On file databases with ext-sqlite3 installed, streaming uses SQLite's incremental blob API through a separate read-only connection, so memory use and time stay proportional to the bytes you read. Without it (or for in-memory databases), the adapter falls back to reading 4 MB windows, which is slower for very large files.

Usage with Laravel

The service provider is auto-discovered. Add a disk to config/filesystems.php:

'disks' => [
    'sqlite' => [
        'driver' => 'sqlite-flysystem',
        'database' => storage_path('files.sqlite'), // default: storage_path('flysystem.sqlite')
        'table' => 'files',                         // default: 'files'
    ],
],
Storage::disk('sqlite')->put('reports/q3.pdf', $pdf);

Serving public files

Set serve to have the package register a GET route that streams public files straight from the database:

'sqlite' => [
    'driver' => 'sqlite-flysystem',
    'database' => storage_path('files.sqlite'),
    'serve' => 'files',                           // GET /files/{path}
    'serve_cache_control' => 'public, max-age=86400', // default: 'public, max-age=3600'
    'serve_middleware' => [],                      // default: none
],

Storage::disk('sqlite')->url('avatars/jorge.png') then returns https://your-app.test/files/avatars/jorge.png.

The route behaves like a static file server:

  • Files with private visibility return 403; missing paths and directories return 404. Files written without a visibility are public.
  • Responses carry an ETag and Last-Modified, and conditional requests (If-None-Match, If-Modified-Since) get a 304.
  • Single byte ranges (Range: bytes=0-1023, bytes=1024-, bytes=-500) return 206, so video seeking and download resumption work. If-Range is respected.
  • No middleware runs by default, so serving files never starts a session.

Public URLs without the route

If something else serves the files (a CDN, a reverse proxy, your own controller), set url_prefix to make Storage::url() work without registering a route:

'sqlite' => [
    'driver' => 'sqlite-flysystem',
    'url_prefix' => 'https://cdn.example.com/files',
],

The prefix comes from url_prefix, then url, then the serve route, in that order. The result is also written into the disk's url config key, so packages that check it (Statamic asset containers, for example) see the disk as public.

Storage model

Each file and each directory is one row in the table. Writing a file creates its parent directory rows automatically. File contents are stored as a BLOB in the last column, so reading metadata never touches the file data, even for large files.

To inspect or back up the store, open the database with any SQLite tool:

sqlite3 storage/files.sqlite "SELECT path, file_size, mime_type FROM files WHERE is_dir = 0"

Testing

composer test

CI runs the suite on PHP 8.2–8.5 against Laravel 12 and 13, with both the lowest and the latest allowed dependencies, plus one job without ext-sqlite3 to cover the fallback streaming path.

License

MIT