Share links
Right-click a file and Share gives it a link that works with no account and no session β which is the point: it is how you send a file to someone outside the panel. The dialog shows the link, when it expires, how many times it has been opened, and a button to stop sharing.
// config/filament-file-explorer.php
'share' => [
'enabled' => true,
'table' => 'file_explorer_shares',
'default_ttl_days' => 7,
'max_ttl_days' => 30, // a ceiling, not a default; null allows links that never expire
],
max_ttl_days is a ceiling: a caller asking for longer, or for no expiry, gets the ceiling. Set it to null to allow links that never expire, and default_ttl_days to null to make that the default.
Gated on the share ability, which follows download for any authorizer that does not answer it β so an authorizer written before this existed keeps meaning what its author meant, and one that refuses downloads refuses sharing too.

Seeing what is shared
A file with a live link carries a small link mark in the listing β over its icon in the grid and the column view, beside its name in the details view. It is drawn the way the tag dots are, and for the same reason: that a file is public is worth reading from across the folder rather than by opening a dialog per row. The mark is not gated on the share ability β it says something about the file, and whoever would want a link stopped is the first person who needs to see there is one.
The link button in the toolbar opens the list of every live link of the scope, wherever in the tree its file sits: what is shared, which folder it is in, when it expires, how often it has been opened, a copy button, and a Stop sharing button per row. It is a dialog rather than a mode like the trash β the shares of a scope are not a folderβs contents, and reading them is not a reason to lose the folder you were browsing β and the dialog stays open as you revoke, since revoking several is one visit.
The list leaves out a link that already reaches nothing β its file moved out of the scope, trashed or deleted. Those die on their own (see below), so offering to stop one would be offering to stop nothing.
Both the mark and the list need the share feature on; the list additionally needs the share ability, which is what the button is gated on.
How it holds up without a session
This is the only route in the package with no authenticated user behind it, so the two guards cannot both run at request time. They are split in time:
- The ability is checked when the link is made, and that decision is what the row records. There is nobody to ask afterwards.
- Containment is checked on every request, against the root folder stored on the row β never against anything derived from the media, which would only prove the file sits under its own ancestor.
That split is what makes a link die on its own, with nothing to keep in step:
| The file is | The link |
|---|---|
| moved out of the scope it was shared from | fails the containment walk |
| trashed | leaves the explorerβs collection and fails the same check |
| deleted | has no row |
Nothing keeps a share in step with the tree, because nothing has to.
The route
It takes the token and nothing else β no scope key, no media id, no conversion name β so a link cannot be edited into a link for another file or another rendition.
A bad token, an expired one, a revoked one and a file that has gone all answer 404 alike. Telling them apart would say whether a token was ever real.
Middleware is ['web'], deliberately without auth: a link that needed an account would not be a share.
Revoking
Links are stored rather than signed, because revoking is the half of sharing that matters. A signed link cannot be withdrawn without a revocation list, which is a stored row with extra steps.
Revoking is a database write and takes effect immediately. The row is kept, so a withdrawn share is still there to inspect.
Three ways to revoke, all the same write: the Stop sharing button of the share dialog, the same button on a row of the shared-files list, and Sharing::revoke() from your own code. Both buttons take the share ability β the one that made the link β because someone who can hand a file out can stop handing it out, and a separate ability would let an authorizer allow the first without the second.
Sharing the same file twice hands back the existing link rather than minting a second, so revoking has one thing to revoke β and FileShared therefore fires only for a link that is actually new.
Events
| Β | Β |
|---|---|
FileShared |
media, share β only for a link that did not already exist |
ShareRevoked |
share |
Reading it yourself
use Koassi\FilamentFileExplorer\Support\Sharing;
$sharing = app(Sharing::class);
$share = $sharing->resolve($token); // null for a bad, expired or revoked token
$media = $share ? $sharing->mediaFor($share) : null; // null when containment fails
$sharing->activeForMedia($media->id, $scopeKey, $rootFolderId);
$sharing->activeMediaIds([$id, $id2], $scopeKey, $rootFolderId); // which of these are shared, in one query
$sharing->liveForScope($scopeKey, $rootFolderId); // every live link, newest first
$sharing->url($share);
$sharing->revoke($share);
resolve() is the token half and mediaFor() the containment half β the same two calls the controller makes, so a link you validate in your own code and a link the route serves cannot disagree.