Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Flysystem Onedrive Laravel Package

justus/flysystem-onedrive

A Flysystem adapter for Microsoft OneDrive, letting you read, write, and manage files through the Microsoft Graph API using the familiar Flysystem interface in your Laravel or PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the driver

composer require justus/flysystem-onedrive

Pin it: "justus/flysystem-onedrive": "^2.0.8". Check composer show justus/flysystem-onedrive and confirm it pulled league/flysystem:^3.0; if your app is still on Flysystem 1.x/2.x you will get a confusing class-not-found at runtime.

Nothing about the v2.0.8 release changes the wiring below. It is a patch bump: a contributor edit to OneDriveAdapter.php, a CI refresh (PHP versions, checkout action), and a laravel-shift "Laravel 13.x Compatibility" PR. That last one almost certainly only relaxes/adjusts the illuminate/* / testbench constraints in composer.json — it does not mean a service provider appeared. Confirm in vendor/justus/flysystem-onedrive/ before you assume anything: no service provider, no config file, no facade. You still wire it up yourself.

2. Register an Azure AD application

Before writing any PHP, get credentials from the Entra ID (Azure AD) portal:

  • App registration → note the Application (client) ID and Directory (tenant) ID
  • Certificates & secrets → create a client secret
  • API permissions → Microsoft Graph → add Files.ReadWrite.All (application permission for daemon-style access, or delegated if acting on behalf of a signed-in user), then Grant admin consent

Skipping admin consent is the single most common cause of "works in Graph Explorer, 403 in Laravel."

3. Identify the drive

You need either a driveId (from https://graph.microsoft.com/v1.0/me/drive or /users/{id}/drive) or a userId. Store these in .env, not in code.

4. Wire it into Laravel

Since the package ships no Laravel integration, add the disk manually in AppServiceProvider::boot():

use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem;
use Justus\FlysystemOneDrive\OneDriveAdapter; // verify exact FQCN in the README / src

Storage::extend('onedrive', function ($app, array $config) {
    $adapter = new OneDriveAdapter(
        clientId:     $config['client_id'],
        clientSecret: $config['client_secret'],
        tenantId:     $config['tenant_id'],
        driveId:      $config['drive_id'],
        // root/path prefix, if supported by your version
    );

    return new Filesystem($adapter);
});

Then a normal disk entry in config/filesystems.php:

'onedrive' => [
    'driver' => 'onedrive',
    'client_id'     => env('ONEDRIVE_CLIENT_ID'),
    'client_secret' => env('ONEDRIVE_CLIENT_SECRET'),
    'tenant_id'     => env('ONEDRIVE_TENANT_ID'),
    'drive_id'      => env('ONEDRIVE_DRIVE_ID'),
],

First use case: prove the auth loop end-to-end before building anything.

Storage::disk('onedrive')->put('healthcheck.txt', 'hello from Laravel');
dump(Storage::disk('onedrive')->get('healthcheck.txt'));
dump(Storage::disk('onedrive')->files());

If put() succeeds, you're done with setup. Everything after that is ordinary Flysystem.

Where to look first: the adapter's constructor signature (the only thing that actually matters for wiring), then the README for supported OneDriveAdapter options, then vendor/justus/flysystem-onedrive/src to see which League\Flysystem\FilesystemAdapter methods are implemented versus stubbed. The v2.0.8 changelog names PR #24 ("Update OneDriveAdapter.php") with no detail — if you're upgrading from 2.0.7, read that diff, because the adapter internals changed in a patch release. With zero dependents on Packagist, you are effectively reading the source anyway.

Implementation Patterns

Treat it as a normal disk, not a special case. The value of this package is that Storage::disk('onedrive') behaves like Storage::disk('s3'). Lean on that:

// Uploads straight from a request
$path = $request->file('contract')->store('contracts/2026', 'onedrive');

// Deterministic naming
Storage::disk('onedrive')->putFileAs('invoices', $request->file('pdf'), "{$invoice->id}.pdf");

// Existence checks and cleanup
if (Storage::disk('onedrive')->exists($path)) {
    Storage::disk('onedrive')->delete($path);
}

Use it as a secondary/archival disk, not your primary store. The realistic production shape for OneDrive is "the customer's document library" or "the finance team's shared folder" — a destination that other humans open in the OneDrive UI. Keep your own DB as the source of truth for paths and metadata, and push blobs out:

class ArchiveInvoiceToSharePoint implements ShouldQueue
{
    public function handle(): void
    {
        Storage::disk('onedrive')->writeStream(
            "invoices/{$this->invoice->number}.pdf",
            Storage::disk('local')->readStream($this->invoice->local_path)
        );
    }
}

Queue these. Graph latency from a web request is measured in hundreds of milliseconds per call, and a page that writes three files will feel broken.

Prefer streams over strings for anything binary. readStream() / writeStream() avoid loading a PDF into memory and, depending on the adapter version, may route through a Graph upload session rather than a single JSON body. Re-verify this after the 2.0.8 adapter edit rather than assuming it still holds.

Wrap the disk behind your own service. Because this is a small package with a small API surface, don't sprinkle Storage::disk('onedrive') across 40 call sites. A thin OneDriveStorage wrapper (or a DocumentStore interface with an S3/in-memory implementation for local dev) means a driver swap or a package abandonment costs you one file, not a refactor:

interface DocumentStore {
    public function put(string $path, $contents): void;
    public function temporaryUrl(string $path): string;
}

Directory structure: mirror your domain, not your users. Shared drives have naming rules and humans who rename folders. Write to a path you control (/app/{tenant}/{year}/{month}/) and never let a user-supplied string become a path segment without Str::slug() + a UUID suffix.

Listing: expect files() / allFiles() on a large folder to be paginated internally by the adapter. If you need this on a hot path, cache the result — one allFiles() on a 10k-file drive is dozens of Graph round-trips.

Testing: never let the test suite touch real OneDrive. Either bind a fake Filesystem backed by League\Flysystem\InMemory\InMemoryFilesystemAdapter under the same disk name, or Storage::fake('onedrive'). If any of your code calls a method the fake doesn't support (temporary URLs, sharing links), that's a signal the method isn't really Flysystem-standard and deserves an interface of its own.

Gotchas and Tips

Auth is the whole ballgame. The adapter typically uses client-credentials flow, meaning every file operation triggers (or reuses) a token request. Two consequences: (1) a misconfigured secret produces a 401 that surfaces as an opaque Flysystem UnableToWriteFile exception — catch and log the underlying Graph error body, not just the Flysystem message; (2) if you make many small calls, token acquisition dominates latency. Do one put() per logical unit of work, not one per byte.

Files.ReadWrite.All application permission grants access to every drive in the tenant. For anything customer-facing, scope to a specific site/drive or use delegated auth. Reviewers ask about this.

Simple uploads are size-capped. Graph's plain PUT .../content handles small files; large files require a resumable upload session with chunked ranges. Check whether your adapter version implements writeStream via upload sessions — and re-check it after 2.0.8, since OneDriveAdapter.php was modified without a changelog entry describing behaviour. If it doesn't, cap your file sizes in validation (e.g. max:4096) and handle big files through a separate path, or you'll get a 413 from Graph that looks like a random failure.

Paths are case-insensitive but case-preserving. Invoices/2026.pdf and invoices/2026.pdf are the same file in OneDrive but distinct keys in your database. If you've ever run this on Linux/S3, you will silently overwrite files. Normalize path casing on write.

Forbidden characters. OneDrive rejects " * : < > ? / \ | and #, % in some contexts, plus names ending in . or a space, plus reserved names like CON, PRN, AUX. User-supplied filenames will hit this. Sanitize before you call the disk and you'll avoid a class of 400s that only appear in production with real customer data.

url() and temporaryUrl() are not really Flysystem concepts here. OneDrive has no public object URLs. A working implementation requires calling Graph's createLink / createSharingLink, which changes the sharing state of the file and may require additional permissions. If the adapter stubs these, serve files through your own authenticated Laravel route that streams via readStream():

Route::get('/documents/{document}', function (Document $document) {
    abort_unless(auth()->user()->can('view', $document), 403);
    return Storage::disk('onedrive')->download($document->path);
});

This also gives you authorization, which is the real reason not to hand out sharing links.

Throttling is real. Graph returns 429 with a Retry-After header under load. Laravel's Http client retry won't help if the adapter uses its own client. For batch operations (a nightly sync of 5,000 files), add your own backoff and usleep() between items; a tight loop will get you throttled and then fail the whole job.

exists() costs a request. Code like if (!$disk->exists($p)) $disk->write($p, ...) doubles your API calls and races. In queued jobs, just write.

Root path / prefix support is version-dependent. If the adapter doesn't take a root option, prefix all your paths yourself through the wrapper — otherwise every call site repeats "clients/{$tenant}/".

Debug with Graph Explorer first. When something 403s or 404s, reproduce the exact call at developer.microsoft.com/graph/graph-explorer with the same identity. It isolates "bad credentials/permissions" from "bad adapter usage" in about 30 seconds, and it's far faster than dumping Flysystem exceptions.

Laravel 13 compatibility is now declared — but read it precisely. The 2.0.8 changelog includes a laravel-shift "Laravel 13.x Compatibility" PR, which in practice means the framework constraint in composer.json was widened/adjusted so the package installs alongside Laravel 13 tooling. It is not a service provider, config publish, or facade. If you're on Laravel 13 and were previously blocked at composer update, this release unblocks you and nothing else changes in your code. Check the php constraint and the CI matrix (both touched in 2.0.8) against your runtime before bumping — Laravel 13 implies a newer PHP floor than Laravel 11.

Upgrade path for 2.0.8: it's a patch, so composer update justus/flysystem-onedrive is safe in principle, but the adapter file changed. Re-run your healthcheck snippet and one integration test against a real dev drive immediately after bumping, and check temporaryUrl() / large-file behaviour specifically, since those are the areas a silent adapter edit most likely affects.

Treat maintenance as a risk factor, not a footnote. Still a tiny package with zero dependents and no Laravel service provider. The good news from 2.0.8 is that it's alive: new contributor, CI refresh, and Laravel 13 compatibility work are all signs of a maintained repo rather than an abandoned one. The bad news is that compatibility is asserted by a bot PR, not by a published support matrix or a Laravel integration layer. Pin the version in composer.json ("justus/flysystem-onedrive": "^2.0.8"), keep the adapter behind your own interface, and write one integration test that hits a real dev drive in CI-nightly so a silent Graph API change (or a silent adapter edit) doesn't take you by surprise. You are the maintainer of this integration in practice — budget accordingly.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels