puter.fs.share()

Websites Puter Apps Node.js Workers

This method gives another Puter user access to a file or directory you own, or one you have been given manage access to.

What an app can share. An app never gets more reach than it was given. It can share its own AppData, and files the user specifically granted it, at up to the level of access it holds itself — so an app with read access can grant read, and nothing more. Files its user owns but never handed to the app stay out of reach, and listShared() shows an app only the shares it can reach. Shares an app creates are attributed to the user and carry issuedByApp, so the owner can tell them apart in getShares() — and those are the only ones an app can list or withdraw on an item. Opening an item to anyone with the link is the owner's own call, never an app's.

Syntax

puter.fs.share(path, recipient)
puter.fs.share(path, recipient, mode)
puter.fs.share(options)

Parameters

path (String) (required)

The path to the file or directory to share. If path is not absolute, it will be resolved relative to the app's root directory.

recipient (String | Object | Array) (required)

Who to share with. A string containing @ is treated as an email address, and any other string as a username. You can also pass { email } or { username }, or an array to share with several people at once.

Where the deployment has Teams, pass { team: uid } to share with every member of a team the caller belongs to — including anyone added to it later. There is no string form for a team: a bare string is always read as an email or username.

A team share is never refused for one member's sake, so it does not produce recipient_not_accepting_shares, and recipient blocks do not apply to it: the grant is the team's, not one colleague's to withhold from another. A member who has blocked you still reaches anything you share with a team you both belong to — they are simply not notified about it. Leaving the team is what ends that access.

Pass { anyone: true } to share with anyone with the link — see below. Only the object form is read as that; the word anyone typed as a string is a username like any other.

mode (String) (optional)

How much access to grant. Defaults to 'read'.

  • 'read' - Read the item.
  • 'write' - Read and change the item. Does not allow re-sharing it, or publishing a directory as a website.
  • 'manage' - Everything 'write' allows, plus re-sharing the item with other people and publishing a shared directory as a website.
  • 'list', 'see' - Weaker than read; useful for making an item discoverable without exposing its contents.

options (Object) (optional)

An object with the following properties:

  • path (String) - Item to share. Required when passing options as the only argument.
  • uid (String) - Item to share, by UID. Can be used instead of path.
  • paths (Array) - Several items to share in one call.
  • recipient (String | Object | Array) - Who to share with.
  • mode (String) - Access to grant. Defaults to 'read'.

Return value

A Promise that resolves to an array of share objects, one per recipient/item pair that succeeded. Each has:

  • uid (String) - Identifier for this share.
  • mode (String) - Access the recipient now has.
  • path (String) - Path of the shared item, masked when you do not own it (see listShared()).
  • name (String) - Name of the shared item. The masked path hides the folder it sits in, so this is what to label it with.
  • entryUid (String) - UID of the shared item.
  • isDir (Boolean) - Whether the shared item is a directory.
  • issuer (String) - Username of whoever granted the share.
  • holder (String) - Username of whoever received it. null for a team share and for a link share.
  • anyone (Boolean) - Present and true on the item's "anyone with the link" share. See below.
  • inheritedFrom (String) - Path of the shared ancestor this access comes from, or null when the share is on the item itself.
  • pending (Boolean) - Present and true when the recipient's email has no confirmed Puter account. See below.
  • recipientEmail (String) - Address a pending share was sent to. Only set when pending.
  • modified (Number) - Last-modified time of the item, in unix seconds.
  • size (Number) - Size of the item in bytes; null for a directory.
  • isNew (Boolean) - Whether this call created access that did not exist before. false means the recipient already had it, possibly at a different mode — sharing again is not an error, so this is how you tell the two apart. Only share() reports it; a listing leaves it undefined.

Sharing the same item with the same person again replaces their access rather than adding a second share, so raising someone from read to write is just another call.

If some recipients succeed and others fail, the promise resolves with the ones that worked. It rejects only when every pair failed.

Errors

A rejection carries { message, code }. Because each recipient/item pair succeeds or fails on its own, these are the codes of the pairs that failed — you only see one as a rejection when every pair failed.

One refusal applies to the whole call instead: handing out access requires a verified phone number or a verified card on the account, on deployments that can verify either. An account on a paid plan is never asked — its card is already on file. The rejection is phone_verification_required (or card_verification_required where only a card can be verified) and carries factors, the verifications the deployment accepts, in the order to offer them. Inside the Puter desktop the user is walked through it and the call is retried on its own. Withdrawing and listing shares never ask for this.

code Meaning
subject_does_not_exist No such item, or you cannot see it. Also what a caller without permission to share gets, so the response never reveals which.
forbidden You can see the item but may not share it at the level you asked for — or you asked to open it to anyone with the link, which only its owner may do (or undo), in person rather than through an app.
user_does_not_exist The username has no account. (An unknown email is invited instead — see below.)
recipient_not_accepting_shares The recipient is not accepting this share — they have blocked you, or turned off new shares from everyone. Nothing is granted and they are not notified. Which of the two it is is not reported.
email_not_allowed The address can't receive an invite — malformed, or refused by the deployment's policy.
cannot_share_with_self You are the recipient.
cannot_share_with_owner The recipient already owns the item.
invalid_mode mode is not one of see, list, read, write, manage — or, for anyone with the link, not read or write.
subscription_required Sharing with anyone with the link is part of the paid plans, and the caller's account is on a free one.
share_daily_limit_reached You have handed out as many new shares as one account may per day (see rate limits).
too_many_recipients, too_many_items One call's fan-out cap; split the request.

{ anyone: true } opens the item to every signed-in Puter account that can name it — by the link the desktop offers, by one built with getShareLink(), or by its path. It takes read or write, never manage: a link hands out access, not the authority to share onward. An item inside a folder opened this way is reachable the same way.

// Anyone signed in who has the link can read it.
const [share] = await puter.fs.share('report.txt', { anyone: true }, 'read');
share.anyone;   // true
share.holder;   // null — there is no one person

// Close it again.
await puter.fs.unshare('report.txt', { anyone: true });

Three things set it apart from sharing with a person:

  • It is the owner's call, in person. Someone holding manage on the item can share it with people, but not open it to everyone; they get forbidden. So does an app acting for the owner, at any level of reach it was given: it can hand the item to a named recipient, but opening it to every account is not access to pass on. Closing the link is bounded the same way.
  • It is a paid-plan feature. A free account is refused with subscription_required. The plan is checked again every time the link is used, so while the owner has no plan the link is silent — nobody has to find it and take it back — and it is not listed as a share by getShares() or listSharedByMe() either, since nobody can use it. The share itself is kept: once the owner is on a plan again the link works, and is listed, exactly as it was.
  • Nobody is told. No notification goes out, and the item does not appear in anyone's listShared(); whoever has the link opens it from the link.

The share shows in getShares() with anyone: true and a null holder. Sharing again with a different mode replaces it, as it does for a person.

Sharing with someone who has no account

A well-formed email address with no confirmed Puter account is invited rather than refused. The share is recorded and the recipient is emailed, but it grants nothing yet — the returned share carries pending: true and a null holder. An address that could never receive that invite is rejected with email_not_allowed instead of becoming an invite nobody can claim.

Access is written when they create an account with that address and confirm it. Signing up alone is not enough: until the address is confirmed it is a claim rather than an identity, and honouring it would hand the share to whoever registered it first.

An invite shows up in getShares() with pending: true, and unshare() cancels it.

const [share] = await puter.fs.share('report.txt', 'newcomer@example.com');

if ( share.pending ) {
    puter.print(`Invited ${share.recipientEmail} — access starts when they join`);
} else {
    puter.print(`Shared with ${share.holder}`);
}

Examples

Share a file with another user

<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        (async () => {
            // (1) create a file
            await puter.fs.write('report.txt', 'Quarterly numbers');

            // (2) share it, read-only
            const shares = await puter.fs.share('report.txt', 'friend@example.com');
            puter.print(`Shared with ${shares[0].holder} as ${shares[0].mode}<br>`);
        })()
    </script>
</body>
</html>

Let someone edit, and let someone else re-share

// An editor can change the file but cannot pass it on.
await puter.fs.share('report.txt', 'editor@example.com', 'write');

// A manager can edit it AND share it with other people.
await puter.fs.share('report.txt', 'manager@example.com', 'manage');

Share one item with several people

await puter.fs.share({
    path: 'report.txt',
    recipient: ['a@example.com', 'b@example.com'],
    mode: 'read',
});

Live updates

Changes inside a shared item are not pushed to recipients in real time — filesystem socket events go to the item's owner only. A client that shows shared content and needs it current should re-read it (readdir/stat) when freshness matters, for example on focus or an explicit refresh.

What sharing does not promise

A few things are worth knowing before you share something sensitive.

A signed URL outlives the share. Anyone who can read a shared item can mint a signed URL for it, and that URL is a bearer token: it works for whoever holds it, signed in or not. Signatures over an item you do not own expire after an hour, but withdrawing access does not invalidate one that has already been issued. Treat an hour as the floor on how long a recipient can keep, or pass on, what you gave them.

An app you have authorized can share on your behalf. Sharing is done in your name, so an app acting for you can share the items it can already reach — its own AppData, and whatever you handed it — with anyone, and at any level it holds itself. It cannot reach past that into the rest of your files. Shares an app issued are marked with issuedByApp in getShares(), so you can tell them apart from your own — and they bound what it can undo: the shares you made yourself, the ones another app made, and the ones a manage delegate made are not an app's to list or take back, and none of them is an app's to open to anyone with the link.

A link is a bearer credential, with a sign-in. An item open to anyone with the link is open to any account that can name it, including a temporary one created on the spot to look at it. Closing the share is the only way to close the door; the link itself cannot be rotated.

Moving an item into someone else's folder hands it over. The folder's owner becomes the item's owner, its bytes start counting against their storage rather than yours, and any shares you had on it are withdrawn — they were yours to give, and it is no longer yours. The same applies in reverse: files a recipient creates inside a folder you shared belong to you and count against your storage.

Deleting a shared item withdraws its shares. Deleting moves the item to Trash, and every share on it and on everything inside it — people, teams, links and unclaimed invites alike — is withdrawn at that moment. Restoring it from Trash does not bring them back; share it again.