oi-lab / oi-laravel-drive
Owned file spaces with folders for Laravel, built on oi-laravel-attachments, with API, MCP tools and events
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/filesystem: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/queue: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- illuminate/view: ^11.0|^12.0|^13.0
- oi-lab/oi-laravel-attachments: ^1.0
- spatie/laravel-data: ^4.23
Requires (Dev)
- laravel/mcp: ^0.9|^1.0
- laravel/pint: ^1.29
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
Suggests
- laravel/mcp: Required to register the drive tools in an MCP server (^0.9|^1.0).
Provides
None
Conflicts
None
Replaces
None
README
OI Laravel Drive
A file space with folders for any entity of your Laravel application — a Team, an Organization, a Project. Drives reuse the folders and files of oi-laravel-attachments, add the entity they belong to, an owner and a date of creation to every item, a trash, a JSON API, MCP tools for AI assistants and events to build search indexing on.
Features
- Drives per entity — one drive per entity, or several named drives (config)
- Folders — a tree, optional unique names per parent folder
- Files in several folders — a file filed in no folder is at the root
- Owner and date — every folder and file has an owner (a user) and a settable date of creation
- Trash — deletions go to the trash, restorable; optional purge after X days
- Folder deletion — optionally with the files filed only in the deleted tree; shared files are only taken out
- JSON API — ready-made routes, or switch them off and call the services from your own controllers
- Sharing — share a folder read-only with other entities, or open a public link with an optional password and expiry date; download a folder as a ZIP
- MCP tools — 16 tools for
laravel/mcpservers, including one-time upload links for binaries - Events — one per action, to index, notify or audit from other modules
- Pluggable access — a resolver for the current entity and its members, a replaceable policy
How It Works
| Object | Table | Role |
|---|---|---|
Drive |
drives |
The space of an entity (driveable morph), with an owner |
DriveEntry |
drive_entries |
The place of a folder or file in a drive: owner, date, trash marker |
Folder / File |
folders / files |
The models of oi-laravel-attachments, unchanged |
Attachment |
attachments |
Files a file in a folder (collection drive) |
DriveUpload |
drive_uploads |
An upload link opened by an assistant |
DriveFolderShare |
drive_folder_shares |
A folder shared read-only with another entity |
DriveFolderLink |
drive_folder_links |
The public link of a folder: token, hashed password, expiry, visits |
The drive adds driveEntry, driveFolders and driveFiles relations to the attachments models at runtime, so your own Folder/File subclasses keep working. Public identifiers are UUIDs.
Requirements
- PHP 8.2+
- Laravel 11, 12, or 13
oi-lab/oi-laravel-attachments^1.0,spatie/laravel-data^4.23laravel/mcp^0.9 or ^1.0 for the MCP tools (optional)
Installation
composer require oi-lab/oi-laravel-drive
Publish & Migrate
php artisan vendor:publish --tag=oi-laravel-drive-config php artisan migrate
Configuration
return [ 'user_model' => 'App\Models\User', // Which entity the user works in, and who belongs to an entity 'driveable' => [ 'resolver' => ConfigurableDriveableResolver::class, 'current' => 'currentTeam', // relation or attribute of the user 'members' => 'users', // relation of the entity 'current_only' => true, // only the drives of the current entity ], 'drives' => ['multiple' => false, 'default_name' => 'Documents'], 'folders' => ['unique_names' => false], 'files' => ['disk' => env('OI_DRIVE_DISK', ...), 'max_bytes' => 50 * 1024 * 1024, 'allowed_mimes' => null], 'trash' => ['enabled' => true, 'purge_after_days' => env('OI_DRIVE_TRASH_PURGE_DAYS')], 'api' => ['enabled' => true, 'prefix' => 'api/drive', 'middleware' => ['api', 'auth:sanctum']], 'uploads' => ['enabled' => true, 'ttl_minutes' => 10, 'inline_max_bytes' => 1024 * 1024], 'mcp' => ['naming' => 'kebab'], // or 'snake' ];
For other access rules, bind your own DriveableResolver class in driveable.resolver, or extend DrivePolicy and set it in policy.
Usage
Drives
use OiLab\OiLaravelDrive\Services\DriveManager; $drives = app(DriveManager::class); $drive = $drives->driveFor($team); // default drive, created on first use $drive = $drives->resolveDrive($user); // default drive of the user's current entity $archives = $drives->createDrive($team, 'Archives'); // when drives.multiple is on
Folders
use OiLab\OiLaravelDrive\Services\DriveFolders; $folders = app(DriveFolders::class); $clients = $folders->create($drive, 'Clients'); $acme = $folders->create($drive, 'Acme', parent: $clients, owner: $user, createdAt: now()->subYear()); $folders->update($acme, name: 'Acme SAS'); $folders->move($acme, null); // to the root $folders->delete($clients, withFiles: true); // to the trash, with its exclusive files $folders->restore($clients); // with everything trashed with it
Files
use OiLab\OiLaravelDrive\Services\DriveFiles; $files = app(DriveFiles::class); $file = $files->store($drive, $request->file('file'), folders: [$acme, $contracts], title: 'Contract'); $note = $files->storeContent($drive, "# Minutes\n…", 'minutes.md'); $files->attach($file, [$archive]); // file it in one more folder $files->detach($file, [$acme]); // take it out of one $files->sync($file, []); // back to the root $files->delete($file); // from every folder, to the trash ['url' => $url] = $files->downloadUrl($file); // temporary link $text = $files->text($note); // ['text' => …, 'truncated' => false]
Listings, search, trash, owners
$listing = $drives->contents($drive, $acme); // DriveListingData: folders, files, breadcrumbs $found = $drives->search($drive, 'invoice'); // DriveItemsData $trash = $drives->trash($drive); $drives->emptyTrash($drive); $drives->setOwner($file, $colleague); $drives->setCreatedAt($file, Carbon::parse('2020-01-01'));
Sharing
app(DriveShares::class)->share($folder, $partnerTeam); // read-only, subfolders and files included app(DriveShares::class)->sharedWith($user); // folders shared with the user's current entity $link = app(DriveLinks::class)->open($folder); // public link; setPassword(), setExpiry(), regenerate(), revoke() app(DriveArchives::class)->download($folder); // the folder as a ZIP
The host serves the public page (links.route). See docs/usage/sharing.md.
JSON API
With api.enabled, under api.prefix (identifiers are UUIDs, drive is optional and defaults to the current entity's drive):
| Method | URI | Action |
|---|---|---|
| GET | drives |
Drives of the current entity |
| POST/PATCH/DELETE | drives, drives/{drive} |
Manage drives (multiple mode) |
| GET | contents?folder=&drive= |
Content of a folder or the root |
| GET | search?q= |
Search folders and files |
| GET/DELETE | trash |
List or empty the trash |
| POST/GET/PATCH/DELETE | folders, folders/{folder} |
Folders (?with_files=1, ?force=1) |
| POST | folders/{folder}/restore |
Restore a folder |
| POST/GET/PATCH/DELETE | files, files/{file} |
Files (multipart file, folder_ids[]) |
| GET | files/{file}/content, files/{file}/download |
Text + link, or redirect to the file |
| POST/DELETE | files/{file}/folders, files/{file}/folders/{folder} |
File in / out of folders |
Rule violations answer 422 (or 409) with {error, message}. Set api.enabled to false to expose the services through your own controllers.
MCP Tools
use OiLab\OiLaravelDrive\Mcp\DriveTools; class AppServer extends Server { protected function boot(): void { $this->tools = [...DriveTools::all(), /* … */]; } }
Add DriveTools::INSTRUCTIONS to the server instructions. The tools: list-drive-folder, search-drive, get-drive-file (metadata, temporary link, text), list-drive-trash, get-drive-upload-status, create-drive-folder, update-drive-folder, delete-drive-folder, request-drive-upload (one-time signed PUT link or drag-and-drop page), upload-drive-file (inline text or base64), update-drive-file, delete-drive-file, restore-drive-item, get-drive-folder-sharing, share-drive-folder, update-drive-folder-link, plus list-drives in multiple mode.
Events
All in OiLab\OiLaravelDrive\Events, with SerializesModels so listeners may be queued:
DriveCreated, DriveUpdated, DriveDeleted, DriveFolderCreated, DriveFolderUpdated, DriveFolderMoved, DriveFolderTrashed, DriveFolderRestored, DriveFolderDeleted, DriveFileCreated, DriveFileUpdated, DriveFileTrashed, DriveFileRestored, DriveFileDeleted, DriveFileAttached, DriveFileDetached, DriveEntryOwnerChanged, DriveFolderShared, DriveFolderUnshared, DriveFolderLinkSaved, DriveFolderLinkRevoked.
Permanent deletions carry a Data snapshot (DriveFolderData, DriveFileData), the model being gone. Listen to them to feed Laravel Scout or oi-laravel-raggable.
Commands
php artisan drive:purge-trash [--days=30] # scheduled daily when trash.purge_after_days is set php artisan drive:prune-uploads # scheduled hourly when uploads are on
AI Assistant Skills
php artisan oi:skills oilab-laravel-drive --project
Testing
composer test
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
When contributing:
- Write tests for new features
- Ensure all tests pass:
vendor/bin/pest - Follow existing code style
- Update documentation as needed
License
The MIT License (MIT). Please see the License File for more information.
Credits
Olivier Lacombe - Creator and maintainer
Olivier is a Product & Technology Director based in Montpellier, France, with over 20 years of experience innovating in UX/UI and emerging technologies. He specializes in guiding enterprises toward cutting-edge digital solutions, combining user-centered design with continuous optimization and artificial intelligence integration.
Projects & Resources:
- OI Dev Docs - Documentation for all Open Source OI Lab packages
- OnAI - Training courses and masterclasses on generative AI for businesses
- Promptr - Prompt engineering Management Platform
Support
For support, please open an issue on the GitHub repository.
