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
Oilab Laravel Drive
oi-lab/oi-laravel-drive
claude-code
file-drive
folders
attachments
soft-deletes
morph-relations
managed-items
folder-sharing

oi-lab/oi-laravel-drive gives any entity of the app (a Team, an Organization…) a drive: a tree of folders and files. It reuses the Folder/File/Attachment models of oi-laravel-attachments and adds the entity, an owner, a date of creation, a trash, a JSON API, MCP tools and events.

Install
php artisan boost:add-skill oi-lab/oi-laravel-drive

Save this content to: .claude/skills/oilab-laravel-drive/SKILL.md

# OI Laravel Drive — AI Context

`oi-lab/oi-laravel-drive` gives any entity of the app (a Team, an Organization…) a drive: a tree of folders
and files. It reuses the `Folder`/`File`/`Attachment` models of oi-laravel-attachments and adds the entity,
an owner, a date of creation, a trash, a JSON API, MCP tools and events.

## Core Concepts

| Model | Table | Notes |
|-------|-------|-------|
| `OiLab\OiLaravelDrive\Models\Drive` | `drives` | `driveable` morph (the entity), `name`, `user_id` (owner), UUID route key |
| `OiLab\OiLaravelDrive\Models\DriveEntry` | `drive_entries` | One per folder/file: `drive_id`, `entry` morph, `kind`, `user_id` (owner), `created_at`, `trashed_with_folder_id`; managed items: `key` (unique per drive), `role` (`notice`), `source` morph (linked file), `locked`, `sealed` |
| attachments `Folder` | `folders` | Tree via `parent_id`; soft deletes = trash |
| attachments `File` | `files` | Stored on `files.disk` under `drive/{drive uuid}/`; soft deletes = trash |
| attachments `Attachment` | `attachments` | Collection `drive`: files a file in a folder (`attachable` = folder) |
| `OiLab\OiLaravelDrive\Models\DriveUpload` | `drive_uploads` | One-time upload link opened by an assistant |
| `OiLab\OiLaravelDrive\Models\DriveFolderShare` | `drive_folder_shares` | Folder shared read-only with another entity (`shareable` morph) |
| `OiLab\OiLaravelDrive\Models\DriveFolderLink` | `drive_folder_links` | Public link of a folder: `token`, hashed `password`, `expires_at`, `views_count` |

Runtime relations added to the attachments models (no change to their classes):
`$folder->driveEntry`, `$file->driveEntry`, `$file->driveFolders` (active folders only), `$folder->driveFiles`,
`$folder->driveShares`, `$folder->driveLink`.

Rules:
- A file filed in no active folder is at the **root**. One file may be in several folders.
- Deleting a file removes it from every folder. Taking it out of one folder is `detach`.
- Deleting a folder takes its subfolders; `withFiles` also takes the files filed **only** in that tree;
  shared files are only taken out. Without `withFiles`, the files stay (at the root if filed nowhere else).
- With the trash on, deletions are soft; what is trashed with a folder (`trashed_with_folder_id`) comes back
  with it. `force` or `trash.enabled = false` deletes for good (storage included).
- Public identifiers (API, MCP, Data `id`) are **UUIDs**.
- **Managed items** (generated by the host): a `locked` item cannot be renamed, moved or deleted, nor a
  folder holding one; a `sealed` folder takes no folder or file and lets none out. Users get a 403
  `DriveException` (`locked`, `locked_content`, `sealed`); the host changes its items inside
  `DriveLocks::bypass(fn () => …)`.
- A **linked file** (`link()`) shows the stored object of another model: no copy, and deleting the link
  (trash purge included) never deletes the object. A **notice** is a locked `Notice.md` explaining its
  folder, given with its text in `DriveListingData::notice`.

## Public API

```php
use OiLab\OiLaravelDrive\Services\DriveManager;
use OiLab\OiLaravelDrive\Services\DriveFolders;
use OiLab\OiLaravelDrive\Services\DriveFiles;
use OiLab\OiLaravelDrive\Services\DrivePresenter;

$drives = app(DriveManager::class);
$drive = $drives->driveFor($team);              // default drive, created on first use
$drive = $drives->resolveDrive($user, $uuid);   // by UUID, or the user's current entity's default drive

$folder = app(DriveFolders::class)->create($drive, 'Clients', parent: null, owner: $user, createdAt: null);
app(DriveFolders::class)->update($folder, name: 'Clients 2026');
app(DriveFolders::class)->move($folder, $parentOrNull);
app(DriveFolders::class)->delete($folder, withFiles: false, force: false);
app(DriveFolders::class)->restore($folder);

$files = app(DriveFiles::class);
$file = $files->store($drive, $uploadedFile, folders: [$folder], owner: null, createdAt: null, title: null, description: null);
$file = $files->storeContent($drive, $content, 'notes.md', folders: []);
$files->update($file, ['name' => 'x.md', 'title' => '…', 'description' => '…']);
$files->attach($file, [$a, $b]); $files->detach($file, [$a]); $files->sync($file, []);
$files->delete($file); $files->restore($file);
$files->downloadUrl($file);                     // ['url' => …, 'expires_at' => …]
$files->text($file);                            // ['text' => …, 'truncated' => bool] or null (binary)

$drives->contents($drive, $folderOrNull);       // DriveListingData
$drives->search($drive, 'term');                // DriveItemsData
$drives->trash($drive); $drives->emptyTrash($drive); $drives->purgeTrash($days);
$drives->setOwner($folderOrFile, $userOrNull); $drives->setCreatedAt($folderOrFile, $date);
$drives->findFolder($uuid, withTrashed: false); $drives->findFile($uuid); $drives->driveOf($item);

app(DrivePresenter::class)->file($file);        // DriveFileData (folder_ids, owner, created_at…)

// Managed items — idempotent, locks lifted for these calls
app(DriveFolders::class)->ensure($drive, 'fy:2026/sales', 'Ventes', parent: $year, locked: true, sealed: true);
$drives->folderByKey($drive, 'fy:2026/sales'); $drives->fileByKey($drive, $key);
$files->link($drive, $invoice, $path, '20260115_acme_F-1.pdf', folders: [$month], disk: null, mimetype: 'application/pdf');
$drives->linksOf($invoice, $driveOrNull); $files->unlink($invoice, $driveOrNull); // the object stays
$files->notice($drive, $folderOrNull, $markdown, 'Notice.md');                    // rewritten when changed
DriveLocks::bypass(fn () => app(DriveFolders::class)->delete($folder));
```

Sharing (read-only, subfolders and files included, nothing while the folder is trashed):

```php
$shares = app(DriveShares::class);
$shares->share($folder, $team, $by); $shares->unshare($folder, $team); $shares->sync($folder, [$a, $b]);
$shares->targets($user, $folder);              // who the user may share with: bound ShareTargets (`sharing.targets`)
$shares->sharedWith($user);                     // topmost folders shared with the user's current entity
$shares->reaches($user, $folderOrFile);         // the policy does NOT know about shares: check this for non-members
$shares->contents($root, $folder);              // breadcrumbs cut at the shared root
$links = app(DriveLinks::class);
$link = $links->open($folder); $links->setPassword($link, $pw); $links->setExpiry($link, $date);
$links->regenerate($link); $links->revoke($folder); $links->find($token); $links->checkPassword($link, $pw);
$links->recordView($link); $link->unlockKey();  // keep unlockKey() in the visitor's session
app(DriveArchives::class)->download($folder);   // ZIP, max links.archive_max_bytes
```

The host serves the public page (`links.route` names its route, `token` parameter).

Failures throw `OiLab\OiLaravelDrive\Exceptions\DriveException` (`rule`, `status`, French message), rendered
as `{error, message}` JSON on API requests.

Authorization: every folder/file is authorized through its drive — `Gate::authorize('view'|'update', $drive)`,
`create` (class) and `delete` only in multiple mode. Default `DrivePolicy`: member of the entity, from that
entity when `driveable.current_only`.

Access contract (`OiLab\OiLaravelDrive\Contracts\DriveableResolver`): `current(User): ?Model` and
`isMember(User, Model): bool`. The default `ConfigurableDriveableResolver` reads `driveable.current` on the
user and `driveable.members` on the entity.

## MCP Tools

```php
use OiLab\OiLaravelDrive\Mcp\DriveTools;

$this->tools = [...DriveTools::all(), ...];     // in the host's laravel/mcp Server::boot()
// add DriveTools::INSTRUCTIONS to the server instructions
```

Tools (base class `Mcp\Tools\DriveTool`, caller = token user, `drive_id` optional): `list-drive-folder`,
`search-drive`, `get-drive-file`, `list-drive-trash`, `get-drive-upload-status`, `create-drive-folder`,
`update-drive-folder`, `delete-drive-folder`, `request-drive-upload`, `upload-drive-file`, `update-drive-file`,
`delete-drive-file`, `restore-drive-item`, `get-drive-folder-sharing`, `share-drive-folder`,
`update-drive-folder-link` (+ `list-drives` in multiple mode). `mcp.naming = snake` gives
`list_drive_folder`. A host with its own tool base class can wrap them: each tool's `name()`, `description()`,
`schema()` and `handle()` are public.

Uploads: `request-drive-upload` opens a `DriveUpload`; the file is PUT raw to the signed `drive.uploads.put`
route (or dropped on the signed `drive.uploads.page` page) and becomes a drive file at once.

## Events

`OiLab\OiLaravelDrive\Events\*`: `DriveCreated|Updated|Deleted`, `DriveFolderCreated|Updated|Moved|Trashed|
Restored|Deleted`, `DriveFileCreated|Updated|Trashed|Restored|Deleted`, `DriveFileAttached|Detached`,
`DriveEntryOwnerChanged`, `DriveFolderShared|Unshared`, `DriveFolderLinkSaved|Revoked`. `*Deleted` events carry Data snapshots (`DriveFolderData`, `DriveFileData`,
`DriveData`). Use them for Scout / oi-laravel-raggable indexing.

## Commands

```bash
php artisan drive:purge-trash [--days=N]   # daily when trash.purge_after_days is set
php artisan drive:prune-uploads [--days=7] # hourly when uploads.enabled
```

## Configuration

`config/oi-laravel-drive.php` (`php artisan vendor:publish --tag=oi-laravel-drive-config`):

| Key | Default | Env |
|-----|---------|-----|
| `user_model`, `user_name_attribute` | `App\Models\User`, `name` | |
| `driveable.resolver` / `current` / `members` / `current_only` | configurable resolver, `currentTeam`, `users`, `true` | `OI_DRIVE_CURRENT_ATTRIBUTE`, `OI_DRIVE_MEMBERS_RELATION` |
| `policy` | `DrivePolicy` | |
| `drives.multiple` / `default_name` | `false` / `Documents` | `OI_DRIVE_MULTIPLE`, `OI_DRIVE_DEFAULT_NAME` |
| `folders.unique_names` | `false` | `OI_DRIVE_UNIQUE_FOLDER_NAMES` |
| `files.disk` / `directory` / `max_bytes` / `allowed_mimes` | attachments disk, `drive`, 50 MB, `null` | `OI_DRIVE_DISK`, `OI_DRIVE_MAX_BYTES` |
| `files.disk_temporary_urls` / `temporary_url_minutes` | `true` / `30` | |
| `trash.enabled` / `purge_after_days` | `true` / `null` | `OI_DRIVE_TRASH`, `OI_DRIVE_TRASH_PURGE_DAYS` |
| `api.enabled` / `prefix` / `name` / `middleware` | `true`, `api/drive`, `drive.api.`, `['api','auth:sanctum']` | `OI_DRIVE_API` |
| `signed_routes.prefix` / `name` / `middleware` | `drive`, `drive.`, `['throttle:60,1']` | |
| `uploads.enabled` / `ttl_minutes` / `inline_max_bytes` | `true`, `10`, 1 MB | `OI_DRIVE_UPLOADS` |
| `sharing.model` / `name_attribute` / `targets` | `null` (the drive's entity class) / `name` / `AnyShareTarget` | |
| `links.route` / `password_min_length` / `archive_max_bytes` | `null`, `6`, 500 MB | `OI_DRIVE_LINK_ROUTE`, `OI_DRIVE_ARCHIVE_MAX_BYTES` |
| `mcp.naming` | `kebab` | `OI_DRIVE_MCP_NAMING` |

## Host Integration Checklist

1. `composer require oi-lab/oi-laravel-drive` (pulls oi-laravel-attachments), publish config, `php artisan migrate`.
2. Set `driveable.current` / `driveable.members` (or bind a `DriveableResolver`), and the `user_model`.
3. Add morph map entries for `Drive`, the folder/file models and the entity if the app enforces a morph map.
4. Keep the package API (`api.middleware` = the app's API stack) or set `api.enabled = false` and call the services.
5. Register `DriveTools::all()` + `DriveTools::INSTRUCTIONS` in the MCP server.
6. Override the policy (`policy`) when roles restrict writing.
7. Listen to the `Drive*` events for indexing, notifications or audit.

## Updating the AI Skill

After changing the package, update `resources/stubs/ai-skill.md`, run `composer sync-ai-skills`, then in host
apps run `php artisan oi:skills oilab-laravel-drive --project`.

OI Laravel Drive — AI Context

oi-lab/oi-laravel-drive gives any entity of the app (a Team, an Organization…) a drive: a tree of folders and files. It reuses the Folder/File/Attachment models of oi-laravel-attachments and adds the entity, an owner, a date of creation, a trash, a JSON API, MCP tools and events.

Core Concepts

Model Table Notes
OiLab\OiLaravelDrive\Models\Drive drives driveable morph (the entity), name, user_id (owner), UUID route key
OiLab\OiLaravelDrive\Models\DriveEntry drive_entries One per folder/file: drive_id, entry morph, kind, user_id (owner), created_at, trashed_with_folder_id; managed items: key (unique per drive), role (notice), source morph (linked file), locked, sealed
attachments Folder folders Tree via parent_id; soft deletes = trash
attachments File files Stored on files.disk under drive/{drive uuid}/; soft deletes = trash
attachments Attachment attachments Collection drive: files a file in a folder (attachable = folder)
OiLab\OiLaravelDrive\Models\DriveUpload drive_uploads One-time upload link opened by an assistant
OiLab\OiLaravelDrive\Models\DriveFolderShare drive_folder_shares Folder shared read-only with another entity (shareable morph)
OiLab\OiLaravelDrive\Models\DriveFolderLink drive_folder_links Public link of a folder: token, hashed password, expires_at, views_count

Runtime relations added to the attachments models (no change to their classes): $folder->driveEntry, $file->driveEntry, $file->driveFolders (active folders only), $folder->driveFiles, $folder->driveShares, $folder->driveLink.

Rules:

  • A file filed in no active folder is at the root. One file may be in several folders.
  • Deleting a file removes it from every folder. Taking it out of one folder is detach.
  • Deleting a folder takes its subfolders; withFiles also takes the files filed only in that tree; shared files are only taken out. Without withFiles, the files stay (at the root if filed nowhere else).
  • With the trash on, deletions are soft; what is trashed with a folder (trashed_with_folder_id) comes back with it. force or trash.enabled = false deletes for good (storage included).
  • Public identifiers (API, MCP, Data id) are UUIDs.
  • Managed items (generated by the host): a locked item cannot be renamed, moved or deleted, nor a folder holding one; a sealed folder takes no folder or file and lets none out. Users get a 403 DriveException (locked, locked_content, sealed); the host changes its items inside DriveLocks::bypass(fn () => …).
  • A linked file (link()) shows the stored object of another model: no copy, and deleting the link (trash purge included) never deletes the object. A notice is a locked Notice.md explaining its folder, given with its text in DriveListingData::notice.

Public API

use OiLab\OiLaravelDrive\Services\DriveManager;
use OiLab\OiLaravelDrive\Services\DriveFolders;
use OiLab\OiLaravelDrive\Services\DriveFiles;
use OiLab\OiLaravelDrive\Services\DrivePresenter;

$drives = app(DriveManager::class);
$drive = $drives->driveFor($team);              // default drive, created on first use
$drive = $drives->resolveDrive($user, $uuid);   // by UUID, or the user's current entity's default drive

$folder = app(DriveFolders::class)->create($drive, 'Clients', parent: null, owner: $user, createdAt: null);
app(DriveFolders::class)->update($folder, name: 'Clients 2026');
app(DriveFolders::class)->move($folder, $parentOrNull);
app(DriveFolders::class)->delete($folder, withFiles: false, force: false);
app(DriveFolders::class)->restore($folder);

$files = app(DriveFiles::class);
$file = $files->store($drive, $uploadedFile, folders: [$folder], owner: null, createdAt: null, title: null, description: null);
$file = $files->storeContent($drive, $content, 'notes.md', folders: []);
$files->update($file, ['name' => 'x.md', 'title' => '…', 'description' => '…']);
$files->attach($file, [$a, $b]); $files->detach($file, [$a]); $files->sync($file, []);
$files->delete($file); $files->restore($file);
$files->downloadUrl($file);                     // ['url' => …, 'expires_at' => …]
$files->text($file);                            // ['text' => …, 'truncated' => bool] or null (binary)

$drives->contents($drive, $folderOrNull);       // DriveListingData
$drives->search($drive, 'term');                // DriveItemsData
$drives->trash($drive); $drives->emptyTrash($drive); $drives->purgeTrash($days);
$drives->setOwner($folderOrFile, $userOrNull); $drives->setCreatedAt($folderOrFile, $date);
$drives->findFolder($uuid, withTrashed: false); $drives->findFile($uuid); $drives->driveOf($item);

app(DrivePresenter::class)->file($file);        // DriveFileData (folder_ids, owner, created_at…)

// Managed items — idempotent, locks lifted for these calls
app(DriveFolders::class)->ensure($drive, 'fy:2026/sales', 'Ventes', parent: $year, locked: true, sealed: true);
$drives->folderByKey($drive, 'fy:2026/sales'); $drives->fileByKey($drive, $key);
$files->link($drive, $invoice, $path, '20260115_acme_F-1.pdf', folders: [$month], disk: null, mimetype: 'application/pdf');
$drives->linksOf($invoice, $driveOrNull); $files->unlink($invoice, $driveOrNull); // the object stays
$files->notice($drive, $folderOrNull, $markdown, 'Notice.md');                    // rewritten when changed
DriveLocks::bypass(fn () => app(DriveFolders::class)->delete($folder));

Sharing (read-only, subfolders and files included, nothing while the folder is trashed):

$shares = app(DriveShares::class);
$shares->share($folder, $team, $by); $shares->unshare($folder, $team); $shares->sync($folder, [$a, $b]);
$shares->targets($user, $folder);              // who the user may share with: bound ShareTargets (`sharing.targets`)
$shares->sharedWith($user);                     // topmost folders shared with the user's current entity
$shares->reaches($user, $folderOrFile);         // the policy does NOT know about shares: check this for non-members
$shares->contents($root, $folder);              // breadcrumbs cut at the shared root
$links = app(DriveLinks::class);
$link = $links->open($folder); $links->setPassword($link, $pw); $links->setExpiry($link, $date);
$links->regenerate($link); $links->revoke($folder); $links->find($token); $links->checkPassword($link, $pw);
$links->recordView($link); $link->unlockKey();  // keep unlockKey() in the visitor's session
app(DriveArchives::class)->download($folder);   // ZIP, max links.archive_max_bytes

The host serves the public page (links.route names its route, token parameter).

Failures throw OiLab\OiLaravelDrive\Exceptions\DriveException (rule, status, French message), rendered as {error, message} JSON on API requests.

Authorization: every folder/file is authorized through its drive — Gate::authorize('view'|'update', $drive), create (class) and delete only in multiple mode. Default DrivePolicy: member of the entity, from that entity when driveable.current_only.

Access contract (OiLab\OiLaravelDrive\Contracts\DriveableResolver): current(User): ?Model and isMember(User, Model): bool. The default ConfigurableDriveableResolver reads driveable.current on the user and driveable.members on the entity.

MCP Tools

use OiLab\OiLaravelDrive\Mcp\DriveTools;

$this->tools = [...DriveTools::all(), ...];     // in the host's laravel/mcp Server::boot()
// add DriveTools::INSTRUCTIONS to the server instructions

Tools (base class Mcp\Tools\DriveTool, caller = token user, drive_id optional): list-drive-folder, search-drive, get-drive-file, list-drive-trash, get-drive-upload-status, create-drive-folder, update-drive-folder, delete-drive-folder, request-drive-upload, upload-drive-file, update-drive-file, delete-drive-file, restore-drive-item, get-drive-folder-sharing, share-drive-folder, update-drive-folder-link (+ list-drives in multiple mode). mcp.naming = snake gives list_drive_folder. A host with its own tool base class can wrap them: each tool's name(), description(), schema() and handle() are public.

Uploads: request-drive-upload opens a DriveUpload; the file is PUT raw to the signed drive.uploads.put route (or dropped on the signed drive.uploads.page page) and becomes a drive file at once.

Events

OiLab\OiLaravelDrive\Events\*: DriveCreated|Updated|Deleted, DriveFolderCreated|Updated|Moved|Trashed| Restored|Deleted, DriveFileCreated|Updated|Trashed|Restored|Deleted, DriveFileAttached|Detached, DriveEntryOwnerChanged, DriveFolderShared|Unshared, DriveFolderLinkSaved|Revoked. *Deleted events carry Data snapshots (DriveFolderData, DriveFileData, DriveData). Use them for Scout / oi-laravel-raggable indexing.

Commands

php artisan drive:purge-trash [--days=N]   # daily when trash.purge_after_days is set
php artisan drive:prune-uploads [--days=7] # hourly when uploads.enabled

Configuration

config/oi-laravel-drive.php (php artisan vendor:publish --tag=oi-laravel-drive-config):

Key Default Env
user_model, user_name_attribute App\Models\User, name
driveable.resolver / current / members / current_only configurable resolver, currentTeam, users, true OI_DRIVE_CURRENT_ATTRIBUTE, OI_DRIVE_MEMBERS_RELATION
policy DrivePolicy
drives.multiple / default_name false / Documents OI_DRIVE_MULTIPLE, OI_DRIVE_DEFAULT_NAME
folders.unique_names false OI_DRIVE_UNIQUE_FOLDER_NAMES
files.disk / directory / max_bytes / allowed_mimes attachments disk, drive, 50 MB, null OI_DRIVE_DISK, OI_DRIVE_MAX_BYTES
files.disk_temporary_urls / temporary_url_minutes true / 30
trash.enabled / purge_after_days true / null OI_DRIVE_TRASH, OI_DRIVE_TRASH_PURGE_DAYS
api.enabled / prefix / name / middleware true, api/drive, drive.api., ['api','auth:sanctum'] OI_DRIVE_API
signed_routes.prefix / name / middleware drive, drive., ['throttle:60,1']
uploads.enabled / ttl_minutes / inline_max_bytes true, 10, 1 MB OI_DRIVE_UPLOADS
sharing.model / name_attribute / targets null (the drive's entity class) / name / AnyShareTarget
links.route / password_min_length / archive_max_bytes null, 6, 500 MB OI_DRIVE_LINK_ROUTE, OI_DRIVE_ARCHIVE_MAX_BYTES
mcp.naming kebab OI_DRIVE_MCP_NAMING

Host Integration Checklist

  1. composer require oi-lab/oi-laravel-drive (pulls oi-laravel-attachments), publish config, php artisan migrate.
  2. Set driveable.current / driveable.members (or bind a DriveableResolver), and the user_model.
  3. Add morph map entries for Drive, the folder/file models and the entity if the app enforces a morph map.
  4. Keep the package API (api.middleware = the app's API stack) or set api.enabled = false and call the services.
  5. Register DriveTools::all() + DriveTools::INSTRUCTIONS in the MCP server.
  6. Override the policy (policy) when roles restrict writing.
  7. Listen to the Drive* events for indexing, notifications or audit.

Updating the AI Skill

After changing the package, update resources/stubs/ai-skill.md, run composer sync-ai-skills, then in host apps run php artisan oi:skills oilab-laravel-drive --project.

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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route