# [Attachment] — a file readable wherever its row is

> `[Attachment]` on a `FileAsset` reference makes the file readable by exactly the people who may read the row holding it — through that row's own security rules, denies and all. A plain `FileAsset` reference grants nothing. Setting an `[Attachment]` shares the file, so only its owner (or someone it was shared with to share further) may.

<!-- id: storage-attachment · area: storage · stability: preview · html: https://osysharp.com/reference/storage/attachment/ -->

## Summary        {#summary}

A file a person attaches to a message should be seen by exactly the people who may see the message. `[Attachment]`
says that, once, on the reference:

```osy syntax
entity MessageFile {
  [Required] Message Message;
  [Required, Attachment] FileAsset File;   // readable wherever this row is
}
```

Whoever may read a `MessageFile` row may read its file — and every version of it. Nobody else gains anything: a
customer reads the screenshot on their own conversation's reply, and not the one on another customer's, and not the
one on an internal note their own conversation carries, because they cannot read those rows.

## Signature      {#signature}

```osy syntax
[Attachment] FileAsset File;            // one file per row; [Required] or nullable, as the row needs
```

Only on an **entity**, and only on a **single** `FileAsset` reference. For several files, give each its own row — an
entity holding one `[Attachment] FileAsset` and a reference to what it belongs to.

## Description    {#description}

### Why is a plain reference not enough?   {#plain-reference}

A file's access is normally its OWN: its owner, a `FileGrant`, or `IsPublic`. A business record that points at a
file says nothing about who may see the bytes — an invoice may be linked from an order and still be private to
finance. That is the right default, and it stays the default.

An attachment is the other case: the file has no audience of its own, it simply belongs to the row. `[Attachment]`
is how you say so, and the platform then answers "may this person read the file?" by asking the row's own rules.

### Who may read an attached file?   {#readers}

The file is readable when **any** of these holds:

- its own rules admit the reader (they own it, it was shared with them, or it is public), or
- it is held by an `[Attachment]` on a row the reader's **read rules on that row's entity** admit.

The second half is the row's whole composed read filter — every `allow`, every `deny`, every `when` — so a `deny`
that hides a note from a customer hides the note's file too. The file's history follows it: each version is readable
exactly when the file is.

This holds on every path that reads a file: a query, `Image(fileAsset:)`, `File.ReadAllBytes`, the signed address
the renderer asks for, and a download link. A reader who may not read it gets a `404`, the same answer as for a file
that does not exist.

### Who may attach a file?   {#writers}

Setting an `[Attachment]` reference **shares** the file with everyone who may read the row, so it takes what sharing
takes: the file's **owner**, or someone holding a `FileGrant` at `Share` or above. Being able to *read* a file —
through another attachment, or a `Read` grant — is not enough.

Without that rule, anyone could create a row they can read, point its attachment at a file id they learned, and read
the file through their own row. So the commit refuses it, with a sentence saying why:

> This file cannot be attached here: attaching it would show it to everyone who can read this MessageFile, and only
> its owner (or someone it was shared with to share further) may do that. Upload your own copy instead.

Uploading a file and attaching it in the same unit of work works, because the person who uploads a file owns it.
Workflows and other system work are not judged, so a mail receiver can store an arriving file and attach it.

Clearing an attachment shares nothing and is never refused here. A set-based statement (`UpdateByFilterAsync`, a bulk
insert) may not name an `[Attachment]` column at all under a caller's security — each file is judged one at a time.

### A signed-out visitor's own files   {#visitor-files}

A customer in a support widget is usually not signed in, and the screenshot they paste still has to be theirs. A file
made with nobody signed in is its **visit's** — the visit the server issued that browser (`Visitor.Id`). The platform
stamps it on `FileAsset.CreatedByVisit` when the file is made, overwriting anything the code set, and from then on
that browser reads it, attaches it and replaces it exactly as a signed-in owner would; another browser, a signed-in
person and a request with no visit do not. A file a signed-in person or the system makes has no visit.

The upload half is [upload](https://osysharp.com/reference/ui/upload/): a signed-out page may upload **privately** (a path the platform chooses), and
only on a visit its cookie proves.

### Downloading a file   {#download}

`Link(fileAsset: file.Id) { Text("Download"); }` is a link that SAVES the file under its own name: the page asks for
an address signed for this reader (the same one `Image(fileAsset:)` uses) and adds `download=1`, so the server answers
`Content-Disposition: attachment`. A file whose type a browser would open as a document — HTML, SVG, XML,
JavaScript — is sent as an attachment whether or not anybody asked, always with `nosniff`, so an `.html` a customer
mailed in is saved and never rendered on your origin.

### Showing a file's name, type and size on a page   {#on-a-page}

An included `FileAsset` is a row like any other: `.Include(r => r.File)` brings its `Name` and `MimeType`, and a page
reads them directly. A file's **size lives on its content**, one hop further, so include that hop when you show it —
`.Include(r => r.File.CurrentVersion)` brings the file and its current content together.

The same holds after an action attaches a file: the page's `live var` asks again, and the new file arrives with the
row that points at it, so `r.File.Name` reads the new name on the next frame.

## Examples       {#examples}

### A document list that names each file   {#example-names}

```osy title="each row shows its file's name, type and size" test app=storage-attachment-names
app Filing { use Osysharp.Storage; use Osysharp.Ui; }

using Osysharp.Storage;
using Osysharp.Ui;

[Principal] entity User { [Required, MaxLength(200)] string Email; }

entity Filing {
  [MaxLength(200)] string Title = "";
  [Attachment] FileAsset? Scan;
  security { allow read, create, update when IsAuthenticated; }
}

[Authorize(IsAuthenticated)]
public List<Filing> Filings() {
  return Filing.Where(f => f.Scan != null).Include(f => f.Scan.CurrentVersion).OrderBy(f => f.Title).Take(50).ToList();
}

[Route("/filings")]
component FilingsPage() {
  live var rows = Filings();
  render {
    Stack(gap: 2) {
      foreach (var f in rows) {
        if (f.Scan != null) {
          Text(f.Title + " — " + f.Scan.Name + " · " + f.Scan.MimeType + " · " + Text.ByteSize(f.Scan.CurrentVersion.Size ?? 0));
        }
      }
    }
  }
}
```

### A conversation where a note's file stays internal   {#example}

```osy title="a reply's screenshot reaches the customer; the note's file never does" test app=storage-attachment
using Osysharp.Storage;

[Principal] entity User { [Required, MaxLength(200)] string Email; }

entity Agent { [Required] User Member; security { allow read when IsAuthenticated; } }
policy IsAgent => Agent.Any(a => a.Member == user);

entity Ticket {
  [Required] User Customer;
  security { allow read where Customer == user; allow read, create when IsAgent; }
}

entity Reply {
  [Required] Ticket Ticket;
  bool Internal;
  security {
    allow read where Ticket.Customer == user && !Internal;
    allow read, create when IsAgent;
  }
}

entity ReplyFile {
  [Required] Reply Reply;
  [Required, Attachment] FileAsset File;
  security {
    allow read where Reply.Ticket.Customer == user && !Reply.Internal;
    allow read, create when IsAgent;
  }
}

/// An agent answers with a screenshot: the file is theirs, and attaching it shares it with whoever reads the reply.
void AnswerWithAScreenshot(Ticket ticket, byte[] png) {
  var reply = new Reply { Ticket = ticket, Internal = false };
  var shot = File.Create("annotated.png", "image/png", png);
  new ReplyFile { Reply = reply, File = shot };
}
```

## See also       {#see-also}
- [Files (addressing something the app stores)](https://osysharp.com/reference/storage/index/) — the two ways the platform stores a file, and which one you hold
- [A file's history — supersede, and what each version keeps](https://osysharp.com/reference/storage/file-versions/) — a file's history, which an attachment's readers see too
- [Showing a picture on a page](https://osysharp.com/reference/ui/image/) — showing an attached picture on a page
- [upload](https://osysharp.com/reference/ui/upload/) — the picker, a pasted or dropped file, and a visitor's private upload
- [The Conversations kit — one timeline per conversation, across every channel](https://osysharp.com/reference/ui/conversations-kit/) — attachments on a conversation's messages, built on this
