# Host.File and Host.Directory — files and folders on the computer

> Read, write and list real files on the computer a local app runs on, with C#'s `System.IO` names under `Host`: `Host.File.ReadAllText(path)`, `Host.File.WriteAllText(path, text)`, `Host.Directory.GetFiles(folder, "*.png")`, `Host.Path.Combine(Host.HomeDirectory, "Projects")`. Every path must lie inside a folder the app declared (`folder "~/Projects";`); a path that leads anywhere else — by `..` or through a link — is refused.

<!-- id: stdlib-host-files · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/host-files/ -->

## Summary        {#summary}
**`Host.File`** and **`Host.Directory`** are C#'s `File` and `Directory` over the real disk of the computer a local
app runs on. Plain `File` is something else — the app's own file store, which works everywhere ([Files (addressing something the app stores)](https://osysharp.com/reference/storage/index/)).

```osy title="keep notes as real files in one folder" test app=stdlib-host-files
app Notebook {
  model "**/*.osy";
  use Osysharp.Host {
    folder "~/Notebook";
  }
}

string Folder() => Host.Path.Combine(Host.HomeDirectory, "Notebook");

string PathOf(string name) => Host.Path.Combine(Folder(), name + ".txt");

void Save(string name, string text) {
  Host.Directory.CreateDirectory(Folder());
  Host.File.WriteAllText(PathOf(name), text);
}

string Read(string name) => Host.File.Exists(PathOf(name)) ? Host.File.ReadAllText(PathOf(name)) : "";

List<string> Notes() => Host.Directory.Exists(Folder()) ? Host.Directory.GetFiles(Folder(), "*.txt") : [];
```

## Signature      {#signature}
```osy syntax
// files
bool         Host.File.Exists(string path)
string       Host.File.ReadAllText(string path)
List<string> Host.File.ReadAllLines(string path)
byte[]       Host.File.ReadAllBytes(string path)
void         Host.File.WriteAllText(string path, string contents)
void         Host.File.WriteAllBytes(string path, byte[] bytes)
void         Host.File.AppendAllText(string path, string contents)
bool         Host.File.Delete(string path)                       // false when there was no file
void         Host.File.Copy(string source, string dest [, bool overwrite])
void         Host.File.Move(string source, string dest [, bool overwrite])
long         Host.File.GetLength(string path)                    // in bytes
DateTime     Host.File.GetLastWriteTimeUtc(string path)
DateTime     Host.File.GetLastWriteTime(string path)             // the same instant

// folders
bool         Host.Directory.Exists(string path)
void         Host.Directory.CreateDirectory(string path)         // and any folders above it
bool         Host.Directory.Delete(string path [, bool recursive])
List<string> Host.Directory.GetFiles(string path [, string pattern [, bool recursive]])
List<string> Host.Directory.EnumerateFiles(...)                  // the same list
List<string> Host.Directory.GetDirectories(string path [, string pattern [, bool recursive]])
List<string> Host.Directory.EnumerateDirectories(...)            // the same list
stream<FileChange> Host.Directory.Watch(string path [, string pattern [, bool recursive]])

class FileChange {
  string         Path;    // the full path of the file
  FileChangeKind Kind;    // Created · Changed · Deleted
  DateTime       At;
}

// paths
string       Host.Path.Combine(string path1, string path2 [, path3 [, path4]])
```

## Description    {#description}
**Every path is a full path inside a folder the app declared.** The `use Osysharp.Host { folder "…"; }` lines are the
whole of what the app may open ([Host — the computer a local app runs on](https://osysharp.com/reference/stdlib/host/)). A path is judged by where it really leads — `..` is removed and
every symbolic link on the way is followed first — so each of these is refused under `folder "~/Notebook";`:

| path | why it is refused |
|---|---|
| `~/.ssh/id_ed25519` | outside the declared folder |
| `~/Notebook/../.ssh/id_ed25519` | climbs out of it |
| `~/Notebook/link/id_ed25519`, where `link` points at `~/.ssh` | the link leads out of it |
| `~/Notebook-old/a.txt` | a different folder that only starts with the same letters |
| `notes/a.txt` | a relative path — it would mean a different file wherever the platform runs |

The refusal names the path, every folder the app declares, and the `folder "…";` line that would allow it.

**Build paths with `Host.Path.Combine`**, starting from `Host.HomeDirectory`. It joins with the computer's own
separator, so the same line is right on Windows and on a Mac. Plain `Path.Combine` is pure text and always joins with
`/` ([Path — the text of a file path](https://osysharp.com/reference/stdlib/path/)); `~`, `documents:` and `temp:` are understood in a `folder` line only, not in a path a call is
given — in code they are `Host.HomeDirectory`, `Host.DocumentsDirectory` and `Host.TempDirectory`.

**A write replaces the whole file in one step.** `WriteAllText` and `WriteAllBytes` write the new content beside the
file and then rename it into place, so the file on disk is always either what it was or all of the new content. A
crash, or a write that is stopped half-way, leaves the last complete file — which is what makes a folder of files safe
to treat as the record of a user's work. (`AppendAllText` adds to the file where it is.)

```osy title="a project kept as a folder: a JSON file and a picture, written whole" test app=stdlib-host-projects
app Workshop {
  model "**/*.osy";
  use Osysharp.Host {
    folder "documents:/Workshop";
  }
}

class ProjectFile {
  public string Name;
  public string Notes;
}

string Root() => Host.Path.Combine(Host.DocumentsDirectory, "Workshop");

string FolderOf(string project) => Host.Path.Combine(Root(), project);

void Save(ProjectFile project, byte[] picture) {
  Host.Directory.CreateDirectory(FolderOf(project.Name));
  Host.File.WriteAllText(Host.Path.Combine(FolderOf(project.Name), "project.json"), JsonSerializer.Serialize(project));
  Host.File.WriteAllBytes(Host.Path.Combine(FolderOf(project.Name), "cover.png"), picture);
}

ProjectFile Open(string project) =>
  JsonSerializer.Deserialize<ProjectFile>(Host.File.ReadAllText(Host.Path.Combine(FolderOf(project), "project.json")));

/// Every project is a sub-folder; the folders ARE the list.
List<string> Projects() => Host.Directory.Exists(Root()) ? Host.Directory.GetDirectories(Root()) : [];
```

**Text is UTF-8.** `WriteAllText` writes UTF-8 with no byte-order mark; `ReadAllText` reads UTF-8 unless the file
starts with a mark that says otherwise, as in C#.

**A listing answers full paths, sorted,** and does not follow or list symbolic links, so a listing stays inside the
folder it was asked about. Each path starts with the folder **as you named it**, as in C# — so a path a listing
answers compares equal to one you build yourself from the same root (`Host.Path.Combine(root, name)`), also where the
root's name leads through a link (the temp folder on a Mac). A watched folder's `FileChange.Path` follows the same
rule. `GetFiles` and `EnumerateFiles` answer the same list — an Osy# list is a value held in
full — and the third argument is `recursive`, a bool, where C# takes a `SearchOption`.

**What differs from C#, and why:**

- `Host.File.Delete` and `Host.Directory.Delete` answer whether there was something to delete, as the app's own
  `File.Delete` does; deleting what is not there is not an error.
- A read answers at most 64 MB. A whole file becomes one value in memory.
- `GetLastWriteTime` and `GetLastWriteTimeUtc` answer the same instant: a `DateTime` here is a moment, not a wall
  clock in some zone.

**Reading a file that is not there** throws, naming the path; ask `Host.File.Exists` first when that is likely.

### Watching a folder for what appears in it    {#watch}
`Host.Directory.Watch` is a stream of the folder's changes from the moment it is asked: one `FileChange` for each
matching file that is created, changed or deleted. Bind it to a `live var` through a [yield — a function that produces results over time](https://osysharp.com/reference/function/yield/) function and
a page shows files as another program makes them:

```osy title="pictures appear on the page as they are written to the folder" test app=stdlib-host-files
using Osysharp.Host;

stream<FileChange> NewPictures() {
  Host.Directory.CreateDirectory(Folder());
  foreach (var change in Host.Directory.Watch(Folder(), "*.png")) {
    if (change.Kind == FileChangeKind.Created) { yield return change; }
  }
}
```

`FileChange` and `FileChangeKind` are named in a file that says `using Osysharp.Host;`. A watch reports what happens
after it starts, so list the folder first (`Host.Directory.GetFiles`) for what is
already there. A file written with `Host.File.WriteAllText` or `WriteAllBytes` arrives as one `Created` — the whole
file is in place when the change is reported. A file another program writes in pieces is reported as `Created` and
then `Changed` as it grows. The watch ends when the page that asked leaves.

## Examples       {#examples}
```osy title="a path outside the declared folder writes nothing" run app=stdlib-host-files
[Test]
void outside_the_declared_folder_is_refused_however_the_path_is_written() {
  var outside = Host.Path.Combine(Host.HomeDirectory, "not-in-the-notebook.txt");
  var refused = Assert.Throws<Exception>(() => Host.File.WriteAllText(outside, "x"));
  Assert.Contains("outside every folder this app declares", refused.Message);
  Assert.Contains("`folder \"~/Notebook\";`", refused.Message);

  var climbing = Host.Path.Combine(Folder(), "..", "not-in-the-notebook.txt");
  Assert.Throws<Exception>(() => Host.File.WriteAllText(climbing, "x"));

  var relative = Assert.Throws<Exception>(() => Host.File.ReadAllText("notes.txt"));
  Assert.Contains("relative path", relative.Message);
}
```

## See also       {#see-also}
- [Host — the computer a local app runs on](https://osysharp.com/reference/stdlib/host/) — the capability, what an app declares, and where it runs
- [Host.Process — run a program on the computer](https://osysharp.com/reference/stdlib/host-process/) — run a program over the files
- [yield — a function that produces results over time](https://osysharp.com/reference/function/yield/) — a function whose answer arrives over time
- [Files (addressing something the app stores)](https://osysharp.com/reference/storage/index/) — the app's own file store, which a deployed app has too
- [Path — the text of a file path](https://osysharp.com/reference/stdlib/path/) — the text of a path
