potherca/flysystem-github
Flysystem adapter for GitHub. Use a GitHub repository as a filesystem backend, reading, writing, listing, and deleting files through Flysystem's simple API while the package handles the underlying GitHub API calls. Useful for storing content, configs, or docs directly in a repo.
potherca/flysystem-github is a Flysystem 1.x adapter that maps Flysystem's read/write/listContents/delete calls onto the GitHub Contents API. In day-to-day Laravel terms: it lets you treat a GitHub repository (or a subdirectory in it) as a storage disk.
Before you install anything, check the constraints. Last tagged release was April 2016, which means:
league/flysystem must be ^1.0. Flysystem 2/3 renamed and re-typed the adapter interfaces (FilesystemAdapter, StorageAttributes, etc.) and this package will not load against them.league/flysystem ^1.0 and the framework's own constraint. Realistically this package is usable on Laravel 5.x/6.x-era apps, or in a standalone script/service, not on Laravel 10/11 without forking.Install and configure:
composer require potherca/flysystem-github
You need a GitHub personal access token with repo scope (or public_repo if you only ever touch public repos). Keep it in .env, never in config/filesystems.php.
Where to look first, in this order:
README.md and composer.json — confirm the PHP and Flysystem constraints before writing any code.src/…/GithubAdapter.php) — the constructor signature and the exact config keys (username, token, repository, branch, and any path/prefix option) are defined there, not in documentation. Verify them; don't trust blog posts.api.github.com happen and where you'd add logging or a Guzzle handler for debugging.First use case to try: a read-mostly content disk. Point the adapter at a docs/content repo and read markdown or JSON out of it:
$adapter = new GithubAdapter($client, [
'repository' => 'acme/content',
'branch' => 'main',
'prefix' => 'articles', // verify key name against source
]);
$filesystem = new League\Flysystem\Filesystem($adapter);
$markdown = $filesystem->read('2024/hello.md');
If that works, you understand the surface area. If read() returns false, jump to the Gotchas section — path prefix, branch name, and token scope are the three usual culprits.
Don't instantiate the adapter inline. Register a singleton and expose it as a Flysystem disk so the rest of the app uses Storage::disk('github') and never sees GitHub-specific code.
// config/filesystems.php
'disks' => [
'github' => [
'driver' => 'github',
'username' => env('GITHUB_USERNAME'),
'token' => env('GITHUB_TOKEN'),
'repository' => env('GITHUB_REPOSITORY'), // owner/name
'branch' => env('GITHUB_BRANCH', 'main'),
],
],
Then in a service provider, extend the Storage manager with a custom driver that builds the adapter and returns a Filesystem. That keeps the dependency at the container boundary — the day you replace this package with direct API calls, only the provider changes.
The most durable use is reading. Load files on demand, and cache hard:
League\Flysystem\Cached\CachedAdapter (backed by Laravel's cache store) so listContents() and read() don't burn API quota on every request.This is the single most important behavioural fact. The Contents API creates a commit per file mutation, so:
sha, which the adapter fetches for you. That makes writes a read-then-write cycle and introduces race conditions (see Gotchas).git revert.// Scheduled publish job — one commit, one file, retried on conflict
Storage::disk('github')->put("generated/{$date}.json", $payload);
content/, data/) so nobody accidentally writes to the repo root or the .github/ folder.Storage::fake() won't know this driver. Test your domain code against League\Flysystem\Memory\InMemoryFilesystemAdapter (or a local disk) and keep the GitHub adapter behind an interface; only smoke-test the real adapter in one integration test.Compatibility is the headline risk. Flysystem 1.x only; the project is unmaintained since 2016. Before committing to it, confirm it actually runs on your PHP version — and budget for forking. A 60-line class using Illuminate\Support\Facades\Http against the Contents API is a legitimate alternative and probably easier to maintain than a patched abandoned dependency.
Branch names. Older GitHub repos default to master, newer ones to main. A wrong branch gives 404s that look like missing files. Make the branch explicit in config and validate it at boot.
Real directory limits. Git has no directories. The adapter synthesizes them from file paths, so empty directories cannot exist, and createDir() may be a no-op or a fake success. Never rely on directory existence as state.
listContents() is expensive. It maps to one API call per directory level, paginated at 100 entries. Listing a large repo recursively can take dozens of requests. Always list a narrow prefix, cache the result, and be aware that pagination beyond the first page may need explicit handling depending on how the 2016 code was written.
Rate limits. Authenticated calls are 5,000/hour. A single page render that lists a directory and reads ten files is eleven requests. Without caching you will hit the ceiling in production, and GitHub returns 403 with a X-RateLimit-Remaining: 0 header — which the adapter may surface as a generic failure.
Size and encoding. The Contents API returns base64-encoded content and only inlines files below roughly 1 MB; larger files need the blob API. Anything binary or large is a bad fit. Keep the disk to text: markdown, JSON, YAML, generated HTML.
Write conflicts. Read-then-write means two concurrent writers can race and produce a 409 Conflict. Wrap writes in a retry loop with jitter, and keep the set of writers as small as possible. Never write from a web request path that users can trigger concurrently.
Errors become false. Flysystem 1.x conventions mean many failures return false rather than throwing. Check return values, or use the League\Flysystem\Filesystem methods that throw (write, read throw on failure in most cases — but verify per method). Enable Guzzle debug output or your logging handler to see the underlying 4xx.
Commit attribution and CI loops. Commits are authored by the token owner and will trigger repo webhooks and Actions. If you push generated files from a workflow that is itself triggered by pushes, you can create a loop. Guard with [skip ci] in the commit message if the adapter lets you set one, or filter by actor in the workflow.
Extension points. The client wrapper is the seam to subclass: override the HTTP call method to add retries, timeouts, a different base_uri (GitHub Enterprise), or logging. Subclass the adapter if you want custom getUrl() behaviour — verify whether it returns html_url, raw.githubusercontent.com, or nothing at all, since public URL generation matters for anything user-facing.
Migration note for new projects. If you're starting fresh, skip the package. Use knplabs/github-api or Laravel's HTTP client behind a small repository class, or simply git clone/git push in a queued job for bulk syncs — it's faster, atomic, and avoids the 1 MB / rate-limit / one-commit-per-file trade-offs entirely. Reserve this adapter for small, low-frequency, read-mostly integrations where the Flysystem API itself is the value.
How can I help you explore Laravel packages today?