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";:
| 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); ~, 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.DeleteandHost.Directory.Deleteanswer whether there was something to delete, as the app's ownFile.Deletedoes; deleting what is not there is not an error.- A read answers at most 64 MB. A whole file becomes one value in memory.
GetLastWriteTimeandGetLastWriteTimeUtcanswer the same instant: aDateTimehere 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#
- Host — the computer a local app runs on — the capability, what an app declares, and where it runs
- Host.Process — run a program on the computer — run a program over the files
- yield — a function that produces results over time — a function whose answer arrives over time
- Files (addressing something the app stores) — the app's own file store, which a deployed app has too
- Path — the text of a file path — the text of a path