{"id":21831617,"url":"https://github.com/capevace/cabinet","last_synced_at":"2025-04-14T07:12:16.507Z","repository":{"id":190148097,"uuid":"666767373","full_name":"Capevace/cabinet","owner":"Capevace","description":"Cabinet is a turn-key file management solution for Laravel, that enables uploading and managing files, as well as attaching files to models. It integrates various file sources into a streamlined API and user interface (including disks, spatie/media-library, custom database tables, external APIs and anything you can think of, basically).","archived":false,"fork":false,"pushed_at":"2024-09-30T19:17:22.000Z","size":3879,"stargazers_count":16,"open_issues_count":3,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-14T00:12:27.317Z","etag":null,"topics":["filament","filament-plugin","filamentphp","file","file-management","files","finder","laravel","laravel-package","php"],"latest_commit_sha":null,"homepage":"https://laravel-cabinet.dev","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Capevace.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-15T14:06:19.000Z","updated_at":"2025-04-09T10:26:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"28d7aee6-3f38-402e-bb9e-c8cfb4f15482","html_url":"https://github.com/Capevace/cabinet","commit_stats":null,"previous_names":["capevace/cabinet"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Capevace%2Fcabinet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Capevace%2Fcabinet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Capevace%2Fcabinet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Capevace%2Fcabinet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Capevace","download_url":"https://codeload.github.com/Capevace/cabinet/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248837285,"owners_count":21169374,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["filament","filament-plugin","filamentphp","file","file-management","files","finder","laravel","laravel-package","php"],"created_at":"2024-11-27T19:12:38.097Z","updated_at":"2025-04-14T07:12:16.485Z","avatar_url":"https://github.com/Capevace.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n    \u003cimg src=\"resources/images/finder.png\" alt=\"Tailpipe logo\"\u003e\n\t\u003ch1\u003eLaravel Cabinet\u003c/h1\u003e\n    \t\u003ch3\u003eTurn-key \"Dropbox-like\" file management, supporting multiple sources in the same folders\u003c/h3\u003e\n\u003c!-- \t\u003cdiv\u003e\n\t\t\u003ca \n\t\t\thref=\"https://github.com/Capevace/cabinet/actions/workflows/test.yml\"\n\t\t\u003e\n\t\t\t\u003cimg\n\t\t\t\tsrc=\"https://github.com/Capevace/cabinet/actions/workflows/test.yml/badge.svg\"\n\t\t\t\talt=\"Run tests\"\n\t\t\t/\u003e\n\t\t\u003c/a\u003e\n\t\t\u003ca href=\"https://github.com/Capevace/cabinet/actions/workflows/test.yml\"\u003e\n\t\t\t\u003cimg\n\t\t\t\tsrc=\"https://img.shields.io/badge/coverage-100%25-brightgreen\"\n\t\t\t\talt=\"Code coverage - 100%\"\n\t\t\t/\u003e\n\t\t\u003c/a\u003e\n\t\t\u003cimg\n\t\t\tsrc=\"https://img.shields.io/github/v/release/capevace/cabinet?include_prereleases\"\n\t\t\talt=\"Latest release\"\n\t\t/\u003e\n\t\u003c/div\u003e --\u003e\n\t\u003ch6\u003e\u003cem\u003eMade by \u003ca href=\"https://mateffy.me\"\u003eLukas Mateffy\u003c/a\u003e\u003c/em\u003e\u003c/h6\u003e\u003cbr\u003e\u003cbr\u003e\n\n\u003c/div\u003e\n\u003cbr /\u003e\n\n\n\u003cp\u003e\u003cstrong\u003eCabinet is a turn-key file management solution for Laravel, that enables uploading and managing files, as well as attaching files to models. \nIt integrates various file sources into a streamlined API and user interface (including disks, spatie/media-library, \ncustom database tables, external APIs and anything you can think of, basically).\u003c/strong\u003e\u003c/p\u003e\n\n\u003cbr\u003e\u003cbr\u003e\n\n\u003e [!NOTE]  \n\u003e Cabinet is currently 0.x and considered \"unstable\". While it's API has not changed in months and is already used in production ([domos.de](https://domos.de)), the API may still be \n\u003e subject to change. Planned release for version 1.0 and thus semver API stability is end of May 2024.\n\n\u003cbr\u003e\u003cbr\u003e\n\n\u003cdetails open\u003e\n  \u003csummary\u003e\u003ch2\u003eScreenshots\u003c/h2\u003e\u003c/summary\u003e\n\n  \u003ctable\u003e\n\t  \u003ctr\u003e\n\t\t  \u003ctd width=\"70%\"\u003e\n\t\t\t  \u003cimg src=\"resources/images/finder-with-selection.png\" alt=\"Select files and with optional order support\"\u003e\n\t\t  \u003c/td\u003e\n\t\t  \u003ctd\u003e\n\t\t\t  \u003ch4\u003eFinder popup to mamage and select files\u003c/h4\u003e\n\t\t\t  \u003cp\u003eYour application code just sets some parameters, like what file types are allowed, how many files are selected and what directory to open.\u003c/p\u003e\n\t\t  \u003c/td\u003e\n\t  \u003c/tr\u003e\n\t  \u003ctr\u003e\n\t\t  \u003ctd width=\"70%\"\u003e\n\t\t\t  \u003cimg src=\"resources/images/folder-support.png\" alt=\"Supports creating and nesting folders\"\u003e\n\t\t  \u003c/td\u003e\n\t\t  \u003ctd\u003e\n\t\t\t  \u003ch4\u003eSupports creating and nesting folders and customizing the sidebar\u003c/h4\u003e\n\t\t  \u003c/td\u003e\n\t  \u003c/tr\u003e\n\t  \u003ctr\u003e\n\t\t  \u003ctd width=\"70%\"\u003e\n\t\t\t  \u003cimg src=\"resources/images/file-pickers.png\" alt=\"Pre-built file pickers for Filament Forms\"\u003e\n\t\t  \u003c/td\u003e\n\t\t  \u003ctd\u003e\n\t\t\t  \u003ch4\u003ePre-built file inputs for \u003ca href=\"https://filamentphp.com/\"\u003eFilament Forms\u003c/a\u003e\u003c/h4\u003e\n\t\t  \u003c/td\u003e\n\t  \u003c/tr\u003e\n\t  \u003ctr\u003e\n\t\t  \u003ctd width=\"70%\"\u003e\n\t\t\t  \u003cimg src=\"resources/images/multiple-sources-same-view.png\" alt=\"Multiple file sources in the same view\"\u003e\n\t\t  \u003c/td\u003e\n\t\t  \u003ctd\u003e\n\t\t\t  \u003ch4\u003eSupports showing multiple file types/sources in the same folder/view\u003c/h4\u003e\n\t\t\t  \u003cp\u003eIn this case, we can show an uploaded video (S3) right next to an \"uploaded\" YouTube/Vimeo video (added by URL). They share the combined file type \u003ccode\u003eVideo([mime-type])\u003c/code\u003e which simplifies the code that uses the selected files, and they can still be differentiated using their mime-type. Thumbnails are either generated for local files (in this case using \u003ca href=\"https://spatie.be/docs/laravel-medialibrary/v11/introduction\"\u003espatie/media-library\u003c/a\u003e as a backend) or fetched from the actual YouTube/Vimeo thumbnail URLs.\u003c/p\u003e\n\t\t  \u003c/td\u003e\n\t  \u003c/tr\u003e\n\t  \u003ctr\u003e\n\t\t  \u003ctd width=\"70%\"\u003e\n\t\t\t  \u003cimg src=\"resources/images/non-file-support.png\" alt=\"Supports non-files\"\u003e\n\t\t  \u003c/td\u003e\n\t\t  \u003ctd\u003e\n\t\t\t  \u003ch4\u003eSupports real files and non-files\u003c/h4\u003e\n\t\t\t  \u003cp\u003eHere we use the \u003ca href=\"https://matterport.com/\"\u003eMatterport\u003c/a\u003e API to add virtual scans/tours that only exist as an API resource, not singular files. In our application code however, we can just treat it as if it was a file. It has a custom (user-land) file type \u003ccode\u003eVirtualScan\u003c/code\u003e.\u003c/p\u003e\n\t\t  \u003c/td\u003e\n\t  \u003c/tr\u003e\n\t  \u003ctr\u003e\n\t\t  \u003ctd width=\"70%\"\u003e\n\t\t\t  \u003cimg src=\"resources/images/context-menu.png\" alt=\"Supports a context menu\"\u003e\n\t\t  \u003c/td\u003e\n\t\t  \u003ctd\u003e\n\t\t\t  \u003ch4\u003eSupports a custom context menu\u003c/h4\u003e\n\t\t  \u003c/td\u003e\n\t  \u003c/tr\u003e\n\t  \u003ctr\u003e\n\t\t  \u003ctd width=\"70%\"\u003e\n\t\t\t  \u003cimg src=\"resources/images/embeddable-finder-blur.png\" alt=\"Embed a Finder into your page\"\u003e\n\t\t  \u003c/td\u003e\n\t\t  \u003ctd\u003e\n\t\t\t  \u003ch4\u003eEmbeddable Finder Livewire component\u003c/h4\u003e\n\t\t\t  \u003cp\u003eBy assigning whole directories to your models, you can have encapsulated file management for single models. For example, manage contract documents for a Deal/Project CRM-like, media assets for CMS pages etc...\u003c/p\u003e\n\t\t  \u003c/td\u003e\n\t  \u003c/tr\u003e\n  \u003c/table\u003e\n\u003c/details\u003e\n\n\u003cbr\u003e\u003cbr\u003e\n\n\u003cbr\u003e\n\n## Motivation\n\nWhile building an application, I needed a solution to make uploaded files browsable and selectable inside a \n\"Project\"-like resource. \n\nI needed a unified way to manage both uploadable files and \"virtual files\" that aren't files in the \ntraditional sense, but still make sense to be selectable in a Finder-like view. For example it makes sense to be able to\nselect a 3D-Scan of a building in the same way as a floor plan or its photo, even though the scan is only \naccessible via API and is only referenced via custom table in our app.\n\n**Cabinet** is the solution I came up with that allows to manage files from different sources in a single, unified way.\n\n\u003cbr\u003e\u003cbr\u003e\n\n## How it works\n\nFile management is complicated! So in order to use Cabinet, it helps to have a basic understanding of how it works.\nCabinet is built around the concept of **sources**, **folders**, **files**, **file types** and **file references**.\n\n### Sources\n\nA **source** is a class that is responsible for listing and managing files. Sources will return a list of files for a given folder\nand can resolve file data for a given file identifier. Sources can read/write data from anywhere, from a disk, a \ndatabase table, or an external API.\n\nWhen querying files for a folder, Cabinet will query multiple sources and merge the results. This allows you to display\nmany different types of files in a single view.\n\nTo create a source, Cabinet provides a `Source` interface that needs to be implemented. Sources can then support additional\nfeatures, such as uploading, deleting, etc. by implementing additional interfaces (`AcceptsUploads`, `AcceptsData`, etc.).\n[Learn more about creating sources](#creating-sources).\n\n### Folders\n\nFolders are a way to organize files. Folders can be nested and can contain files and other folders. Folders are\nmanaged by Cabinet itself and are stored in the `cabinet:directories` database table, using the `Directory` model.\n(We use \"Directory\" for the raw model / DB data and \"Folder\" throughout the rest of the code, in order to be able to\ndifferentiate between the two).\n\n### Files\n\nA `File` is a unified data structure that represents a file. It contains the file's metadata, such as its name, size,\nand file type. It also contains everything required to resolve the file's data: its source the file's identifier.\nThese can be used to access a file's URL or its contents by calling the associated source through Cabinet.\n\n### File types\n\nA `FileType` is a class that is responsible for determining the type of a file. Cabinet comes with a few built-in file\ntypes, such as `Image`, `Video`, `Audio`, `Document`, etc. These file types unify many different file extensions and\nmime types into a single type. For example, the `Image` file type will match all image files, such as `.jpg`, `.png`,\n`.gif`, etc. and will also match the `image/jpeg`, `image/png`, `image/gif` mime types.\n\nThis makes it much easier to deal with files, as you don't have to worry about all the different file extensions and\nmime types. You can simply check if a file is an `Image` and Cabinet will take care of the rest. However,\nyou can can still access the file's raw mime type and extension if you want to limit selection / uploads.\n\nYou can also create your own file types for your custom models by implementing the `FileType` interface. \n[Learn more about creating file types](#creating-file-types) or look at some [examples](#built-in-file-types).\n\n### File references\n\nA `FileRef` is a model that is used to reference a file from a model. It uses a polymorphic relationship to reference\nthe model to attach files to and stores the file's ID and source in order to retrieve it. This allows you to attach \nfiles to any model in your application without needing pivot tables or other complex structures. However, `FileRef`'s \nhave a UUID and nullable model columns, so you can also use your foreign key constraints if you want to create stronger\ncouplings.  \n\nIt also stores the name of the relationship that is used to access the file. This allows you to attach multiple different\nfiles to a model, such as a thumbnail and a gallery, without having to worry about naming conflicts.\n\nIn addition, it also contains an order column, which allows saving multiple files in a specific order under a single\nrelationship.\n\n\n\u003cbr\u003e\n\n## Installation \n\nThe following command will install the cabinet core package and the UI package. The pre-built UI is made with [Filament](https://filamentphp.com) and is technically optional, but recommended.\n\n```bash\ncomposer require capevace/cabinet capevace/cabinet-ui\n```\n\nIf you want to completely build your own UI and just use Cabinet headlessly, you can omit the `capevace/cabinet-ui` package.\n\n### Run the install command to publish the config and migrations\n\nCabinet requires two tables to be present in your database. You can publish the migrations and config file using the following command:\n\n```bash\nphp artisan cabinet:install\n```\n\nYou're all done! You can now start using Cabinet.\n\n\u003cbr\u003e\u003cbr\u003e\n\n## Getting started\n\nTo demonstrate how Cabinet works, we'll build an example model that places a thumbnail and image gallery on a `Post` model.\n\n### 1. Add `HasFiles` and `InteractsWithFiles` to your model\n\nIn order for a model to be able to have files attached to it, it needs to implement the `HasFiles` interface, which can be done by using the `InteractsWithFiles` trait.\n\n```php\nuse Capevace\\Cabinet\\Concerns\\InteractsWithFiles;\nuse Capevace\\Cabinet\\Contracts\\HasFiles;\n\nclass Post extends Model implements HasFiles\n{\n    use InteractsWithFiles;\n    \n    // ...\n}\n```\n\n### 2. Define the file relationships\n\nNext, we need to define the file relationships on the model. We'll add a `thumbnail` and `gallery` relationship to our `Post` model.\n\nSee [Defining file relationships](#defining-file-relationships) for more information.\n\n```php\nuse Capevace\\Cabinet\\Concerns\\InteractsWithFiles;\nuse Capevace\\Cabinet\\Contracts\\HasFiles;\n\nclass Post extends Model implements HasFiles\n{\n    use InteractsWithFiles;\n    \n    public function thumbnail()\n    {\n        // Single file\n        return $this-\u003efileRef('thumbnail');\n    }\n    \n    public function gallery_images()\n    {\n        // Multiple files\n        return $this-\u003efileRefs('gallery_images');\n    }\n}\n```\n\n### 3. Uploading files to Cabinet\n\nNow that we have our model set up, we can start uploading files to Cabinet. This can be done in two ways:\n\n1. Using the Cabinet UI `Finder` component\n2. Manually, using the `Cabinet` facade\n\n#### Using the Cabinet UI\n\nThe Cabinet UI comes with a `Finder` component that can be used to upload manage files within Cabinet. This component can be included in any view using the `x-cabinet-finder` Blade component.\n\n```blade\n@props(['folderId' =\u003e null])\n\n{{-- The folderId determines the folder that is opened and the \"root\" of this view --}}\n\u003cx-cabinet::finder\n    :folder-id=\"$folder-\u003eid\"\n/\u003e\n```\n\n#### Manually, using the `Cabinet` facade\n\nYou can also upload files manually by specifying the file source and providing the required data.\nFor normal files (e.g. with spatie/media-library, etc.), this will be a `TemporaryUploadedFile` instance, \nwhich Laravel provides when uploading files. For virtual files, this will be an array of data.\n\n```php\nuse \\Cabinet\\Facades\\Cabinet;\n\n$folder = Cabinet::folder('\u003cmy-folder-uuid\u003e');\n\n$uploadedFile = request()-\u003efile('thumbnail');\n\n// Upload a normal file\n$file = Cabinet::getSource('spatie-media')\n    -\u003eupload($folder, $uploadedFile);\n\n// Upload a virtual file\n$file = Cabinet::getSource('youtube-videos')\n    -\u003eadd($folder, 'Rick Astley - Never Gonna Give You Up (Video)', [\n        'id' =\u003e 'dQw4w9WgXcQ',\n    ]);\n```\n\n### 4. Attach files to the model\n\nYour models are now ready to have files attached to them. You can attach either attach files manually, or use the Cabinet UI to do so (this requires using [Filament forms](https://filamentphp.com)) .\n\n#### Using the Cabinet UI\n\nThe Cabinet UI provides a `FileInput` [Filament form component](https://filamentphp.com) that can be used to attach \nfiles to models. You can use this component by adding it to your form schema like so:\n\n```php\nuse \\Livewire\\Component;\nuse \\Livewire\\Attributes\\Computed;\nuse \\Filament\\Forms\\Contracts\\HasForms;\nuse \\Filament\\Forms\\Form;\n\nuse \\Cabinet\\Filament\\Components\\FileInput;\n\nclass PostForm extends Component implements HasForms\n{\n    public string $postId;\n    \n    #[Computed]\n    public function post(): Post\n    {\n        return Post::findOrFail($this-\u003epostId);\n    }\n    \n    public static function form(Form $form)\n    {\n        return $form\n            -\u003emodel($this-\u003epost)\n            -\u003eschema([\n                // ...\n\n                FileInput::make('thumbnail')\n                    -\u003elabel('Thumbnail')\n                    // The `relationship` method will automatically attach the file\n                    // to the model when saved\n                    -\u003erelationship('thumbnail')\n                    // The amount of files that can be picked\n                    -\u003esingle()\n                    // This specifies the root folder that will be opened in the file picker\n                    -\u003eroot('\u003cmy-folder-uuid\u003e'),\n                    \n                FileInput::make('gallery_images')\n                    -\u003elabel('Gallery Images')\n                    // It is also possible to attach multiple files\n                    -\u003erelationship('gallery_images')\n                    -\u003emax(5)\n                    -\u003eroot('\u003cmy-folder-uuid\u003e'),\n                    \n                // ...\n            ]);\n    }\n    \n    public function save()\n    {\n        $data = $this-\u003eform-\u003egetState();\n        \n        $this-\u003epost-\u003eupdate($data);\n        \n        // This will attach the files to the model\n        $this-\u003eform-\u003esaveRelationships();\n    }\n}\n```\n\n#### Attaching files via code\n\nIf you're not using Filament in your app or you want to attach files in other places, you can also attach files manually using the `Cabinet` facade.\n\n```php\nuse \\Cabinet\\Facades\\Cabinet;\n\n$post = Post::find(1);\n\n$file = Cabinet::file('spatie-media', '5');\n\n// Attach a single file\nCabinet::attach($file, to: $post, as: 'thumbnail');\n\n// or directly via the relationship (we're \"create\"-ing a FileRef)\n$post-\u003ethumbnail()-\u003ecreate($file-\u003egetReferenceData());\n\n// Attach multiple files\nCabinet::attach($files, to: $post, as: 'gallery_images');\n```\n\n\u003cbr\u003e\u003cbr\u003e\n\n## Concepts\n\n## Accessing Folders\n```php\n$directory = Cabinet::directory($id);\n// -\u003e Directory\n\n$folder = Cabinet::folder($id);\n// -\u003e Folder\n// same as $directory-\u003easFolder();\n```\n\n## Referencing Files\n```php\n\n$ref = Cabinet::reference($file);\n\nauth()-\u003euser()-\u003eupdate([\n\t'avatar_file_ref_id' =\u003e $ref-\u003eid,\n]);\n\n\nCabinet::attach($file, to: auth()-\u003euser(), as: 'avatar');\n\n\n```\n\n## Accessing Files\n```php\n$filesAndFolders = $folder-\u003elist();\n// -\u003e Collection\u003cFile|Folder\u003e\n\n$files = $folder-\u003efiles();\n// -\u003e Collection\u003cFile\u003e\n\n$folders = $folder-\u003efolders();\n// -\u003e Collection\u003cFolder\u003e\n\n// Create a File DTO via source model ID\n$file = Cabinet::file('spatie-media', '5');\n// -\u003e Cabinet\\File {#1618\n//     id: \"5\",\n//     source: \"spatie-media\",\n//     type: Cabinet\\Types\\Image {#1629},\n//     name: \"yoda\",\n//     slug: \"yoda.png\",\n//     mimeType: \"image/png\",\n//     size: 6566715,\n//     url: \"https://v3.test/storage/5/yoda.png\",\n// }\n\n// Create a File DTO via source path and disk\n$file = Cabinet::file($source, 'avatars/test.png', disk: 'local');\n```\n\n## Renaming Files\n```php\n// Will rename the file to \"Hello there\", the underlying file will be named 'hello-there-custom.png'.\nCabinet::rename($file, 'Hello there');\n\n// Will rename the file to \"Hello there\", the underlying file will be named 'hello-there-custom.png'.\nCabinet::rename($file, 'Hello there', slug: 'hello-there-custom');\n\n// Will only change the \"virtual\" filename, not the underlying file\nCabinet::rename($file, 'Hello there', slug: false);\n```\n\n## Moving Files\n```php\n// You can pass either the Folder DTO\nCabinet::move($file, $folder);\n\n// Or the Directory model\nCabinet::move($file, $directory);\n```\n\n## Delete Files\n```php\nCabinet::delete($file);\n```\n\n## Uploading Files\n\n```php\nuse Illuminate\\Http\\UploadedFile;\n\n$uploadedFile = new UploadedFile(...);\n\n// Upload to a specific source\n$source = Cabinet::getSource('spatie-media')\n\t-\u003eupload($directory || $folder, $uploadedFile);\n\n// or using shorthand syntax\nCabinet::upload('spatie-media', $folder, $uploadedFile);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcapevace%2Fcabinet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcapevace%2Fcabinet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcapevace%2Fcabinet/lists"}