Osy#betaRun it on Osyrin: osyrin.com

Reference / Stdlib

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

Host.File.ReadAllText(path) · WriteAllText(path, text) · Exists · Delete · Copy · Move · Host.Directory.GetFiles(path, pattern, recursive) · CreateDirectory · Exists · Delete · Watch(path, pattern) · Host.Path.Combine(a, b) // under `use Osysharp.Host { folder "…"; }`

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.

stable4 examples compiled by CIhostlocalfiles

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)).

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#

// 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#

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). 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";:

pathwhy it is refused
~/.ssh/id_ed25519outside the declared folder
~/Notebook/../.ssh/id_ed25519climbs out of it
~/Notebook/link/id_ed25519, where link points at ~/.sshthe link leads out of it
~/Notebook-old/a.txta different folder that only starts with the same letters
notes/a.txta 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); ~, 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.)

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#

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 function and a page shows files as another program makes them:

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#

[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#

Related

Host — the computer a local app runs on

`use Osysharp.Host` gives an app the computer it runs on: run a program (`Host.Process`), read and write files in…

Host.Process — run a program on the computer

Run a program on the computer a local app runs on. `Host.Process.Run("git", ["--version"])` waits for it and answers a…

yield — a function that produces results over time

A `stream<T>` function produces its results one at a time instead of all at once, and a `live var` bound to one renders…

Files (addressing something the app stores)

The platform stores a file two ways, and which one you hold decides how you address it. A PATH-keyed file lives at an…

Path — the text of a file path

C#'s `System.IO.Path`: build and take apart file paths as text — `Path.Combine`, `GetFileName`…