azure-oss/storage-file-share
Laravel filesystem driver for Azure File Share. Integrates with the Storage facade so you can read, write, list, and delete files on Azure file shares through familiar Laravel APIs, with package config for quick setup in your app.
This is the File Share half of a community-maintained Azure Storage SDK for PHP (AzureOss\Storage\File\Share), split out into its own repo. It talks to the Azure Files REST API — the same data that shows up on a VM as an SMB mount — rather than to Blob Storage. Think of it as "cheap POSIX-ish file storage with real directories," which is why it's usually the right answer for legacy line-of-business file shares, CAD/PDF/export dumps, and anything ops already mounts over SMB.
Install the split package (the split is what Composer resolves; the README, tests, and examples live in the parent monorepo, so open that first before reading source):
composer require azure-oss/storage-file-share
Then add a connection string to .env — grab it from the portal under Storage account → Security + networking → Access keys, and confirm it contains a FileEndpoint= entry, not just BlobEndpoint=:
AZURE_STORAGE_FILE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net
AZURE_STORAGE_FILE_SHARE=reports
The first use case to build is almost always a "drop a generated artifact somewhere a human or an SMB client can find it": render a PDF in a queued job, write it to tenant-42/invoices/2026-09.pdf, and let the finance team's mounted drive pick it up. That exercises auth, directory creation, and upload in about twenty lines, and it's the pattern you'll reuse for everything else.
Before writing code, read the client classes in src/ (FileShareClient, ShareClient, DirectoryClient, FileClient) — the object graph is client → share → directory → file, and getting that hierarchy into your head early saves a lot of flailing.
Bind a singleton, don't new clients in controllers. There's no Laravel service provider in this package, so write your own:
// config/services.php
'azure_files' => [
'connection_string' => env('AZURE_STORAGE_FILE_CONNECTION_STRING'),
'share' => env('AZURE_STORAGE_FILE_SHARE', 'reports'),
],
// AppServiceProvider::register()
$this->app->singleton(FileShareClient::class, fn () => FileShareClient::fromConnectionString(
config('services.azure_files.connection_string')
));
$this->app->singleton(ShareClient::class, fn ($app) => $app->make(FileShareClient::class)
->getShareClient(config('services.azure_files.share')));
Directory-per-tenant / per-domain. Because these are real directories, you can mirror your domain model into the path ({team}/{module}/{yyyy}/{mm}/{uuid}.pdf) and list a subtree cheaply. This is the big advantage over Blob Storage — a listFilesAndDirectories() on one directory is a natural, cheap "folder view," and an ops person can browse it over SMB with zero extra tooling.
Wrap it behind your own interface. Given the project's maturity signals (single-digit dependents, split repo, no Laravel integration), keep the SDK on one side of a boundary:
interface ReportArchive
{
public function put(string $path, string $contents, array $metadata = []): void;
public function stream(string $path);
public function delete(string $path): void;
}
Then AzureFileShareArchive implements ReportArchive is the only file that imports AzureOss\.... Your tests get a trivial in-memory fake, and swapping to Blob/S3 later is a one-class change.
Large files: upload in ranges, don't buffer. Anything over a few megabytes should be streamed in chunks with explicit offsets. The File Share API expects you to declare the file's size before writing ranges, so the shape is: create the file at final size, then write range chunks. Do this inside a queued job with a bounded chunk size (4–8 MB is a sane default) so a 2 GB export doesn't blow memory_limit.
Download via a streamed response, not Storage::download:
return response()->streamDownload(function () use ($fileClient) {
echo $fileClient->download()->getContent()->getContents();
}, 'invoice.pdf', ['Content-Type' => 'application/pdf']);
Lifecycle jobs. Pair the upload path with a scheduled prune command (files:prune --older-than=90d) that lists a directory and deletes by lastModified. Azure Files has no lifecycle-management rules like Blob does — retention is your job.
Metadata for app state. Store your own keys (tenant_id, job_id, checksum) on the file rather than a side table where reasonable; but see the caveats below on naming.
php-oss-for-azure/azure-storage-*-php) for usage, and pin an exact version in composer.json — the split can drift in packaging even when the component API is stable.Storage::disk('azure-files'). Don't try to teach filesystems.php about it; treat it as a separate, deliberately-scoped service.mkdir -p; write an idempotent ensureDirectory($path) helper that walks the segments, and call it at the top of every write path.createFile needs the final content length up front. Get this wrong and you'll see InvalidHeaderValue/OutOfRangeInput errors that read nothing like "you forgot the size."tmp/ then copy to the final path.If-Match / ETags for concurrent writers. Two jobs uploading the same logical file will silently last-writer-wins otherwise. If you need optimistic locking, read the ETag, pass it on write, and handle the 412.JobId missing" bugs.404 (missing directory), 409 (ShareAlreadyExists, ParentNotFound), 412 (ETag mismatch), 403 (key/permission or IP firewall). A 403 that appears only in production is almost always the storage account firewall, not your code.How can I help you explore Laravel packages today?