# `Fil.Kino`
[🔗](https://github.com/pehbehbeh/fil/blob/v0.2.0/lib/fil/kino.ex#L2)

Browses disks in [Livebook](https://livebook.dev), uploads files to them, and adds a smart cell that builds a disk.

    Mix.install([{:fil, "~> 0.2"}, {:kino, "~> 0.19"}])

    disk = Fil.disk(adapter: Fil.Adapter.Local, root: "/tmp/fil")
    Fil.Kino.browser(disk)

Needs [Kino](https://hexdocs.pm/kino), an optional dependency of `Fil`.

## Browser

`browser/3` shows one directory of a disk at a time: breadcrumbs back to the root, and a table with the name, size,
modification time and type of each entry. Clicking a directory opens it. Clicking a file shows a preview below the
table, for images (PNG, JPEG, GIF and WebP) and text, and each file has a download button.

The browser only reads by default, so a stray click in a notebook can't change a production disk. With
`writable: true`, each file also gets a delete button, and an upload field below the table writes into the current
directory.

It lists the directory when it's shown and when you open another one, so files that other cells write show up
after a click on Refresh.

It works the same on every disk. A memory disk uses the store of the process that built the browser, which in
Livebook is the evaluator of the cell. Livebook evaluates all regular sections in one process, so after
`Fil.Adapter.Memory.checkout/0` in one cell, a browser in any regular section sees the files. A branching section
has a process of its own, and needs a `checkout/0` of its own.

## Upload

`upload/3` is a file field that writes each file you pick straight to a directory of the disk:

    Fil.Kino.upload(disk, "reports", accept: [".pdf"])

Livebook keeps an upload as a file in the notebook's runtime, and the field streams it from there into
`Fil.write/4`, so large files don't have to fit in memory. The line below the field says what was written, or why
not. An upload never replaces a file unless you pass `if_exists: :overwrite`.

## Smart cell

With `Fil` and Kino installed, Livebook offers a Fil disk smart cell. It's a form for a Local, S3 or Memory disk
that generates the `Fil.disk/1` call, such as:

    disk =
      Fil.disk(
        adapter: Fil.Adapter.S3,
        bucket: "reports",
        region: "eu-central-1",
        access_key_id: System.fetch_env!("LB_AWS_ACCESS_KEY_ID"),
        secret_access_key: System.fetch_env!("LB_AWS_SECRET_ACCESS_KEY")
      )

The S3 credentials are Livebook secrets you pick in the form, so they're never part of the notebook. Without them,
the disk accesses a public bucket without signing. The session token is optional, for temporary credentials such as
those from AWS STS or SSO. Empty fields are left out, so the adapter's defaults apply.

The path-style switch shows what the disk does. `Fil.Adapter.S3` uses path-style URLs when there's an endpoint, so
the switch turns on when you enter one and off when you clear it, until you switch it the other way. The code sets
`path_style:` only when the switch differs from that default.

For a memory disk, the cell calls `Fil.Adapter.Memory.checkout/0` first. The store belongs to the evaluator (see
[Browser](#module-browser) for what that means for sections), and it's gone when the runtime restarts.

# `browser`

```elixir
@spec browser(Fil.Disk.t() | Fil.Ref.t()) :: Kino.JS.Live.t()
```

Returns a file browser for a disk or a ref, starting at the root or at `path`.

    Fil.Kino.browser(disk)
    Fil.Kino.browser(disk, "reports")
    Fil.Kino.browser(disk, "reports", writable: true)
    Fil.Kino.browser(reports)
    Fil.Kino.browser(reports, max_download_size: 1_000_000_000)

It's a `Kino.JS.Live`, and with `writable: true` a `Kino.Layout` of the browser and the upload field. Both render as
the result of a cell, with `Kino.render/1` and inside a `Kino.Layout` or a `Kino.Frame`. Building the browser does
no I/O: the directory is listed when the browser is shown, and a storage error shows up in the browser instead of
raising.

## Options

* `:max_preview_size` (`t:pos_integer/0`) - Files larger than this, in bytes, get no preview, only the download button. 1 MiB by default.
  The size comes from the listing, so nothing is read to decide, and a file that grew since isn't
  caught. The default value is `1048576`.

* `:max_download_size` (`t:pos_integer/0`) - Files larger than this, in bytes, can't be downloaded from the browser. 100 MiB by default. A
  download reads the whole file into memory and sends it to the browser through Livebook, so
  large files are better copied with `Fil.cp/3` or read with `Fil.stream/3`. The size is checked
  against the listing, so a file that grew since isn't caught. The default value is `104857600`.

* `:writable` (`t:boolean/0`) - Adds a delete button to each file, and an upload field for the current directory below the
  browser (see `upload/3`). A delete asks for confirmation first, and only deletes files, never
  directories. The upload never replaces a file. The default value is `false`.

# `browser`

```elixir
@spec browser(Fil.Disk.t(), Path.t()) :: Kino.JS.Live.t()
@spec browser(Fil.Ref.t(), keyword()) :: Kino.JS.Live.t() | Kino.Layout.t()
```

Returns a file browser. See `browser/1`.

# `browser`

```elixir
@spec browser(Fil.Disk.t(), Path.t(), keyword()) :: Kino.JS.Live.t() | Kino.Layout.t()
```

Returns a file browser. See `browser/1`.

# `upload`

```elixir
@spec upload(Fil.Disk.t() | Fil.Ref.t()) :: Kino.Layout.t()
```

Returns a file field that writes each uploaded file to the root of a disk, or to `path`.

    Fil.Kino.upload(disk)
    Fil.Kino.upload(disk, "reports", accept: [".pdf"], on_upload: &IO.inspect/1)
    Fil.Kino.upload(reports)
    Fil.Kino.upload(reports, if_exists: :overwrite)

A file is written under the name it had on your computer. A name that isn't a plain file name, such as `..` or one
with a `/` or a `\`, is refused. It returns a `Kino.Layout` of Kino's file input and a status line, which renders
as the result of a cell. The field stays usable after an error.

## Options

* `:label` (`t:String.t/0`) - The label of the field. `"Upload to memory:reports"` for a memory disk and the path
  `"reports"`, by default.

* `:accept` - The file types the field accepts, as a list of extensions (`".pdf"`) or MIME types
  (`"image/*"`), or `:any`. Passed to `Kino.Input.file/2`, which raises on an empty list. The default value is `:any`.

* `:if_exists` - What to do when the directory already has a file of that name. `:error` keeps the file and
  shows a `Fil.AlreadyExistsError`, `:overwrite` replaces it. See `Fil.write/4`. The default value is `:error`.

* `:on_upload` (function of arity 1) - Called with the `Fil.Ref` of each file after it was written, for example to process it. It
  runs in a process Kino starts for the field.

# `upload`

```elixir
@spec upload(Fil.Disk.t(), Path.t()) :: Kino.Layout.t()
@spec upload(Fil.Ref.t(), keyword()) :: Kino.Layout.t()
```

Returns a file field that writes to a disk. See `upload/1`.

# `upload`

```elixir
@spec upload(Fil.Disk.t(), Path.t(), keyword()) :: Kino.Layout.t()
```

Returns a file field that writes to a disk. See `upload/1`.
