Theme
VFS method reference Preview
Every method here is called on a virtual file system object obtained from GetCurrentVFS() or new VirtualFS(...). Every one returns a response object; check Ok() before using anything.
Paths are always root relative and POSIX, whatever the operating system and whatever the storage underneath.
The examples below assume:
js
var vfs = GetCurrentVFS();
if (vfs === null) { Exit(0); }Inspecting
Stat, Lstat
js
Stat(path: string): respStat;
Lstat(path: string): respStat;Metadata for one file or folder. Stat follows a symbolic link and describes what it points at; Lstat describes the link itself. Info() returns a DirListItem.
js
var resp = vfs.Stat("/forecast/budget2026.xlsx");
if (resp.Ok()) {
var info = resp.Info();
Log.Info(info.Name + " " + info.Size + " bytes, modified " +
FormatDateTime("YYYY-MM-DD HH:mm", info.TimeStamp));
} else {
Log.Error("stat failed: " + resp.ErrorMsg());
}ReadDir
js
ReadDir(path: string, sortBy: string, sortDir: string): respDir;Lists one folder, not recursively. Infos() returns an array of DirListItem.
| Argument | Values |
|---|---|
sortBy | SortByName, SortByNameDirsFirst, SortBySize, SortByTime |
sortDir | SortAsc, SortDesc |
An unrecognized value falls back to SortByNameDirsFirst and SortAsc rather than failing.
js
var resp = vfs.ReadDir("/archive", SortByTime, SortDesc);
if (resp.Ok()) {
var newest = resp.Infos()[0];
Log.Info("most recent: " + newest.Name);
}TreeSize
js
TreeSize(path: string): respSize;Total size in bytes of a file, or of a folder and everything under it.
js
var resp = vfs.TreeSize("/projects/apollo");
if (resp.Ok()) {
Log.Info("apollo occupies " + resp.Size() + " bytes");
}TreeSize walks the whole tree
On object storage that means one request per page of keys, and on a large prefix it can take a long time and cost real money. Never call it from a before handler.
Creating and removing
js
Mkdir(path: string): respBase; // parent must exist
MkdirAll(path: string): respBase; // creates the whole branch
Rmdir(path: string): respBase; // the leaf folder only, and it must be empty
Remove(path: string): respBase; // one file
RemoveAll(path: string): respBase; // recursive, folder and all contents
Rename(source: string, target: string): respBase;
Symlink(path: string, linkFile: string): respBase;
Truncate(path: string, size: number): respBase;js
var resp = vfs.MkdirAll("/processed/2026/08");
if (!resp.Ok()) {
Log.Error("could not create the folder: " + resp.ErrorMsg());
Exit(1);
}| Method | Notes |
|---|---|
Mkdir | Fails when a parent is missing. Use MkdirAll for a branch |
Rmdir | Removes only the last segment, and only when it is empty |
Remove | Files only. A folder needs Rmdir or RemoveAll |
RemoveAll | Irreversible, recursive, and does not ask |
Rename | Within one virtual file system only. Every folder in the target path must already exist. To cross systems, use MoveToVFS |
Symlink | Both paths must be inside the virtual file system. Supported only where the storage supports links, so not on object storage |
Truncate | Sets a file's length. Not supported on every backend |
RemoveAll on a path built from user input deletes whatever it resolves to
Combine that with the fact that the virtual file system object ignores the user's permissions, and a careless script becomes the most destructive thing on the machine. Never pass a user supplied path straight to RemoveAll.
Attributes
js
Chown(path: string, uid: number, gid: number): respBase;
Chmod(path: string, mode: number): respBase;
Chtimes(path: string, atime: number, mtime: number): respBase;Chown takes numeric ids, not names. Chmod takes a numeric POSIX mode, so 0o644 or 420. Chtimes takes UNIX timestamps in seconds.
js
vfs.Chmod("/data/report.csv", 0o640);
vfs.Chtimes("/data/report.csv", 1787411051, 1787411051);These reach through to the underlying storage, so they work on a local disk and on an SFTP target, and are refused by object storage, which has no such concepts. Check Ok(); do not assume.
Reading a file
js
ReadFileAsText(path: string, length?: number, offset?: number, whence?: number): respReadString;
ReadFileAsHex(path: string, length?: number, offset?: number, whence?: number): respReadString;
ReadFileAsBytes(path: string, length?: number, offset?: number, whence?: number): respReadBytes;| Argument | Meaning |
|---|---|
length | At most this many bytes. Omitted or zero means a 32 KiB buffer |
offset | How far to skip |
whence | FromStart, FromCurrent or FromEnd. Defaults to the start |
js
// The first two bytes, as hex. FFD8 means JPEG.
var head = vfs.ReadFileAsHex(CtxRelPath(), 2);
if (head.Ok() && head.Data().toUpperCase() !== "FFD8") {
Log.Warn("not a JPEG despite the name: " + CtxRelPath());
Exit(1);
}js
// A whole small text file.
var resp = vfs.ReadFileAsText("/config/manifest.json", 1048576);
if (resp.Ok()) {
var manifest = JSON.parse(resp.Data());
Log.Info("manifest version " + manifest.version);
}The default read is 32 KiB, not the whole file
Omitting length does not mean "everything". It means one 32 KiB buffer, and a longer file comes back truncated with Ok() still true. Pass an explicit length whenever the file might be larger, and remember that whatever you ask for is held in memory.
For anything genuinely large, do not read it into the script at all. Use ExportFile to put it on local disk and process it there, or hand it to an external tool with RunCapture.
ReadFileAsText on a binary file returns unusable characters rather than an error. Use ReadFileAsBytes or ReadFileAsHex when the content is not text.
Writing a file
js
WriteFileAsText(path: string, data: string, offset: number, whence: number): respBase;
WriteFileAsHex(path: string, data: string, offset: number, whence: number): respBase;
WriteFileAsBytes(path: string, data: number[], offset: number, whence: number): respBase;offset and whence together decide where writing starts:
| Combination | Effect |
|---|---|
0, FromStart | Overwrite from the beginning |
0, FromEnd | Append |
js
vfs.WriteFileAsText("/logs/audit.txt",
FormatDateTime("YYYY-MM-DD HH:mm:ss") + " " + CtxUsername() + " " + CtxRelPath() + "\n",
0, FromEnd);js
vfs.WriteFileAsHex("/data/marker.bin", "FFD8", 0, FromEnd);
vfs.WriteFileAsBytes("/data/marker.bin", [255, 216], 0, FromEnd);Two handlers appending to one file will interleave
Handlers run concurrently across sessions, and an append here is not an atomic operation. An audit file written this way from a busy virtual file system will eventually contain a torn line. For an audit trail you can rely on, use the Connector's own signed log, which is built for exactly this and cannot be turned off.
Import and export
js
ImportFile(localFilePath: string, targetVfsFilePath: string): respBase;
ExportFile(vfsFilePath: string, localFilePath: string): respBase;Move a whole file between the Connector host's own disk and the virtual file system, streaming rather than buffering, so size is not a problem.
- The local path is in the host's own convention:
C:\Data\Budget.xlsxon Windows,/srv/staging/budget.xlsxon Linux. - The VFS path is always POSIX.
- Encryption at rest is handled automatically in both directions:
ImportFileencrypts on the way in,ExportFiledecrypts on the way out.
js
// Take the uploaded file out to local disk for an external scanner to look at.
var local = "/var/scan/" + ShortUID() + ExtractExt(CtxRelPath());
var resp = vfs.ExportFile(CtxRelPath(), local);
if (!resp.Ok()) {
Log.Error("export failed: " + resp.ErrorMsg());
Exit(1);
}
var verdict = RunCapture("/usr/local/bin/scan --quiet " + local);
DelFile(local);
if (verdict.indexOf("INFECTED") >= 0) {
Log.Error("malware in " + CtxRelPath() + " from " + CtxUsername());
vfs.Remove(CtxRelPath());
}ExportFile writes plaintext to local disk
A file that is encrypted at rest inside the virtual file system is written decrypted by ExportFile. It sits there as ordinary bytes, readable by anything on the host with filesystem access, until you delete it. Write to a directory only the Connector service account can read, delete the file as soon as you are finished with it, and consider SecureErase() rather than DelFile() when the content was sensitive.
Moving between virtual file systems
js
CopyToVFS(sourcePath: string, targetVfs: VirtualFileSystem, targetPath: string): respBase;
MoveToVFS(sourcePath: string, targetVfs: VirtualFileSystem, targetPath: string): respBase;Copy or move a file from the virtual file system you call the method on into a different one. The target must be a different object; MoveToVFS deletes the source once the copy has landed. The target can be a storage you built with new VirtualFS(...), or one of this Connector's own storages fetched with GetVFSByName(), which needs no credentials in the script at all:
js
var resp = GetCurrentVFS().CopyToVFS(CtxRelPath(), GetVFSByName("Archive"), "/incoming/" + ExtractName(CtxRelPath()));js
var origin = GetCurrentVFS();
var archive = new VirtualFS(
VfsTypeS3,
"s3://acme-archive/incoming?region=eu-west-1",
JSON.stringify({ access_key: accessKey, access_secret: accessSecret, region: "eu-west-1" })
);
var resp = origin.MoveToVFS(CtxRelPath(), archive, "/2026/08/" + ExtractName(CtxRelPath()));
if (resp.Ok()) {
Log.Info("archived " + CtxRelPath());
} else {
Log.Error("archive failed: " + resp.ErrorMsg());
}Both sides decrypt and re encrypt as needed, so moving from an encrypted local virtual file system to a plain bucket produces a readable object in the bucket, and moving the other way produces an encrypted file.
A move is a copy then a delete, and it is not a transaction
If the copy succeeds and the delete fails, the file exists in both places. Check Ok(), and treat a failure as "the state is unknown" rather than "nothing happened".