File management
A Field document is a folder that you can copy, move into a project, or send to somebody else.
If your code reads a file, you'll probably want that file to travel with it. Put it in the document
and give Field its name; Field looks there first, then in the document's workspace projects and
any other directories you have registered. A machine-specific path such as
File("/Users/you/Desktop/scan.ply") would need changing when the document moves.
Reading a file by name
Put the file in the document's files/ folder and read it by name:
val rows = "data.csv".file.readLines()
"data.csv".file is a java.io.File. Two more extensions run the same resolution and hand back a
different type:
| Spelling | You get | Reach for it when |
|---|---|---|
"data.csv".file |
java.io.File |
Ordinary reads and writes. |
"data.csv".path |
java.nio.file.Path |
Calling Files or another NIO API. |
"data.csv".absolutePath |
String, absolute and normalised |
Passing a filename to a subprocess. |
There is a spelled-out form too, for when the name is in a variable rather than a literal:
val a = files.file("data.csv") // `files` is the current session's file manager
val b = fileManager.file(name) // `fileManager` is the same object under a longer name
val c = files.path("data.csv")
Where a plain name is looked for
Field searches these locations in order and uses the first file it finds:
files/inside the document folder;files/inside each workspace project the document belongs to, which lives at_projects/<project>/files/in the workspace;- directories registered with
FileManager.add(name, directory, search = true), in registration order; - an exact filename registered with
FileManager.addFile(name, target).
If none has it, you still get a path back: the one the name would have inside the document's
files/. That is what makes writing a new file work.
// Resolves to <document>/files/out/frame-0001.png whether or not anything is there yet.
// Intermediate directories are yours to create.
val out = "out/frame-0001.png".file
out.parentFile.mkdirs()
out.writeBytes(png)
A resolution is cached only once the file exists, so resolving a name before you create it does not pin the wrong answer for the rest of the session.
Naming the root yourself
Two roots have built-in names, and a leading % asks for one of them by hand rather than searching:
val mine = "%document/data.csv".file // this document's files/ only, skipping the projects
val shared = "%project/palette.json".file // the project files/ only, skipping the document
val dir = "%document".path // the files/ directory itself
%project needs the document to be in exactly one workspace project, or in one whose files/
already holds the name. In a document that is in no project it throws; in a document that is in two
and where the file is in neither, it throws as ambiguous.
In a writable document, asking for %document/... or reading files.documentFilesDirectory
creates the document's files/ directory if it is missing. In a read-only document, these lookups
return the path without creating the directory. Existing files can still be read.
Field also registers %docs when the loaded installation contains public/docs/. This points
to the bundled documentation, not to the current document or working directory. For example,
the Loris study uses its included recording directly:
val source = "%docs/assets/subharmonic_sound/notld-barbara-source.wav".path
No files are copied into the document. Like other named directories, %docs can be replaced
with FileManager.add("docs", anotherDirectory); an installation without bundled docs does not
register it.
Roots of your own
Give a directory outside the document a name, then ask for files in it with %name/:
FileManager["textures"] = "~/Pictures/field-textures"
val brick = "%textures/brick.png".file
Registration is process-global rather than per-document, so every open document sees %textures,
and registering the same name again replaces it without saying so. The directory has to exist
already unless you ask for it:
FileManager.add("renders", "~/Desktop/field-renders", create = true)
FileManager["renders"] // the absolute Path stored, or null
FileManager.remove("renders") // unregisters; nothing on disk is touched
A name has to look like an identifier: letters, digits, _ and -, not starting with a digit. A
leading % or $ is stripped for you. document and project are reserved, since the resolver
owns them.
Fallback files
Register an individual file when its friendly name differs from its installed location:
FileManager.addFile("my-helper", executablePath) // Path, File, or String
val helper = "my-helper".file
FileManager.removeFile("my-helper") // unregisters without deleting anything
Like named roots, these registrations are shared by every open document. A copy of the file in the document, a project or a search directory still wins. Registering a name again replaces its target.
The name is an exact filename, including any extension, not a path or a glob. The target does not
have to exist yet: until it does, the name resolves as if it were not registered. Globs and
%document/ or %project/ paths ignore these registrations.
Field registers one of these itself, field-slug, pointing into the checkout it was started from.
Build it once with bash native/field-slug/build.sh
(build.sh) and "field-slug".file works from any
document.
Globs
A spec containing *, ?, [ or { is matched by walking each search root recursively and taking
the first match in sorted path order:
val anyScan = "**/*.ply".file // first .ply under the document, then under the projects
val inFolder = "scans/*.ply".file
Sorting makes the choice repeatable, though a glob is a poor way to name one specific file. If a glob matches nothing, resolution throws: Field cannot turn a pattern into a destination for a new file. Use a plain name when you want to create one.
Paths that skip all of this
Absolute paths, and paths starting ~/, are expanded and normalised and otherwise passed straight
through:
val big = "~/Datasets/city.laz".file
Use an explicit path for a file that lives outside the document, such as a dataset too large to copy in. Someone opening the document on another machine will need to provide that file and adjust the path. If you refer to the same external directory more than once, give it a name as a root.
When resolution fails
| Spec | What happens |
|---|---|
| a blank name | Resolution throws because there is no filename to resolve. Supply a relative name or an explicit root path. |
"**/*.ply" matching nothing |
Throws and lists the searched roots. Check the pattern or use a plain filename when creating a new file. |
"%textures/x.png" with nothing registered as textures |
Throws because the root is unknown. Register textures with FileManager.add before resolving the path. |
"%project/x.csv" in a document in no project |
Throws because no project can supply the root. Add the document to a project or choose %document. |
"%project/x.csv" in a document in two projects, file in neither |
Throws because the intended project is ambiguous. Use a registered root to select the destination explicitly. |
"%document/../../elsewhere" |
Throws because the path escapes the named root. Register the external directory as its own root instead. |
| any of the above from a plain unit test or a bare thread | Throws because there is no live session to resolve against. Resolve in an active box and pass the resulting absolute path to the worker. |
The resolver answers "where is data.csv" for a particular open document, so it only works inside
a running box.
Where did that path actually go
Every lookup is appended to the session root's fileManagerAccessHistory property: the request, the
resolved path, which root answered it, whether the file existed, and which box asked. Read it when a
name resolved somewhere you did not expect.
It holds the most recent FileManager.maxAccessHistoryEntries lookups, 1024 by default, and is
emptied when the session's file manager is first created, so it is a recent-activity log rather than
a complete one.
Full signatures and the rest of the API are in FileManager.kt in the Data reference.