FileSystemHandler
The FileSystemHandler class is the framework's abstraction over Node.js fs. It provides cross-platform file I/O, directory operations, glob pattern matching, HTTP downloads with retry, and archive management (ZIP/TAR).
Package: io-oidis-localhost
Namespace: Io.Oidis.Localhost.Connectors.FileSystemHandler
Availability: Backend only (Node.js)
All file system operations must go through FileSystemHandler. Direct usage of fs.readFileSync(), fs.writeFileSync(), fs.existsSync(), fs.mkdirSync(), fs.unlinkSync(), or any other fs method is a convention violation. FileSystemHandler handles platform differences, path normalization, recursive directory creation, file locking detection, and logging automatically.
Access
import { Loader } from "@io-oidis-localhost/Io/Oidis/Localhost/Loader.js";
import { FileSystemHandler } from "@io-oidis-localhost/Io/Oidis/Localhost/Connectors/FileSystemHandler.js";
const fileSystem : FileSystemHandler = Loader.getInstance().getFileSystemHandler();
The instance is a lazy-initialized singleton — first call to getFileSystemHandler() creates it, subsequent calls return the cached instance.
API Overview
Path Queries
| Method | Description |
|---|---|
getTempPath() | Returns system temporary directory path |
getProjectBasePath() | Returns project base path from program args |
getAppDataPath() | Returns app data path from program args |
getLocalAppDataPath() | Returns local app data path (Windows AppData\Local or system home) |
NormalizePath($source, $osSeparator?) | Normalizes path separators (/ by default, OS separator if $osSeparator=true) |
Status Checks
| Method | Returns | Description |
|---|---|---|
Exists($path) | boolean | Checks if file or directory exists |
IsEmpty($path) | boolean | Returns true if directory is empty or file has zero bytes |
IsFile($path) | boolean | Returns true if path is a file |
IsDirectory($path) | boolean | Returns true if path is a directory |
IsSymbolicLink($path) | boolean | Returns true if path is a symbolic link |
IsFileLocked($path) | boolean | Returns true if file is locked/in use |
File Operations
| Method | Returns | Description |
|---|---|---|
Read($path) | string | Buffer | Reads file content. Returns Buffer — always call .toString() when you need a string. |
Write($path, $data, $append?) | boolean | Writes/appends content to file. Creates parent directories automatically. |
Rename($oldPath, $newPath) | boolean | Renames/moves a file or directory |
Copy($source, $dest, $callback?) | void | Copies file or directory (callback-based) |
CopyAsync($source, $dest) | Promise<void> | Async version of Copy |
Directory Operations
| Method | Returns | Description |
|---|---|---|
CreateDirectory($path) | boolean | Creates directory recursively (includes parent dirs) |
Expand($pattern) | string[] | Glob pattern matching. Supports *, **, ! exclusion prefix. |
Delete Operations
| Method | Returns | Description |
|---|---|---|
Delete($pathOrOptions, $callback?) | boolean | Deletes file or directory (recursive) |
DeleteAsync($pathOrOptions) | Promise<void> | Async deletion with options support |
Download Operations
| Method | Returns | Description |
|---|---|---|
Download($urlOrOptions, $callback?, $onHttpError?) | void | Downloads from HTTP(S) or file:// URL with progress tracking |
DownloadAsync($urlOrOptions) | Promise<IDownloadResult> | Async download returning {headers, bodyOrPath} |
AbortDownload($id) | boolean | Aborts active download by response ID |
Archive Operations
| Method | Returns | Description |
|---|---|---|
Pack($path, $options?, $callback?) | void | Creates ZIP or TAR archive (with gzip/bzip2 compression) |
PackAsync($path, $options?) | Promise<string> | Async packing, returns archive path |
Unpack($path, $options?, $callback?) | void | Extracts ZIP/TAR archives |
UnpackAsync($path, $options?, $onProgress?) | Promise<string> | Async unpacking, returns extraction path; $onProgress reports entries when native is set |
Unpack()/UnpackAsync() are built on the decompress package, which reads the entire archive into a
Buffer before its plugins extract anything. That is fine for packages and build artefacts, and it is the
wrong tool for a 100 MB register export inside a container with a 256 MB limit. When the archive is large
and only one member matters, stream that member instead — Io.Oidis.Labs.Utils.ArchiveStreams opens a
zip member as a NodeJS.ReadableStream through the system unzip or an in-process zip.js reader that
pulls slices of the file by descriptor, and never holds more than the stream's own window.
For a full extraction to disk, opt into native: true (see Unpack): the system tar
streams straight to disk and reports per-entry progress.
When to Use Which Method
| Scenario | Method |
|---|---|
| Check if a file/directory exists | Exists() |
| Read a file as string | Read().toString() — Read() returns Buffer, call .toString() |
| Write/overwrite a file | Write($path, $data) — creates parent dirs automatically |
| Append to a file | Write($path, $data, true) |
| Create a directory tree | CreateDirectory() — recursive by default |
| Delete a file or directory | Delete() or DeleteAsync() |
| Find files by pattern | Expand("**/*.ts") |
| Download a file from URL | DownloadAsync() |
| Create an archive | PackAsync() |
Key Features
- Automatic parent directory creation —
Write()creates all missing parent directories before writing - Cross-platform path handling — normalizes separators, handles Windows/Linux differences
- File locking detection —
IsFileLocked()checks if a file is in use by another process - Glob pattern matching —
Expand()supports*,**, and exclusion patterns (prefix with!) - Download resilience — configurable retries, progress tracking, content-length validation
- Archive support — ZIP (Windows), TAR/TAR.GZ/TAR.BZ2 (Linux), with optional compression
Common Pitfall: Read() Returns Buffer
Read() uses fs.readFileSync() without encoding, so it returns a Buffer, not a string. Casting with <string> compiles but fails at runtime — methods like .trim(), .match(), .split() do not exist on Buffer.
// ❌ WRONG — type cast does not convert Buffer to string
const content : string = <string>fileSystem.Read(filePath);
content.trim(); // TypeError: content.trim is not a function
// ✅ CORRECT — .toString() converts Buffer to string
const content : string = fileSystem.Read(filePath).toString();
content.trim(); // works
Examples
Good: Using FileSystemHandler
const fileSystem : FileSystemHandler = Loader.getInstance().getFileSystemHandler();
// Read a file as string — always use .toString(), never cast with <string>
const content : string = fileSystem.Read(filePath).toString();
// Write a file (creates parent directories automatically)
fileSystem.Write(filePath, content);
// Check existence
if (fileSystem.Exists(filePath)) {
// ...
}
// Create a directory
fileSystem.CreateDirectory(dirPath);
// Delete a file
fileSystem.Delete(filePath);
// Find files by pattern
const tsFiles : string[] = fileSystem.Expand("**/*.ts");
Bad: Direct fs usage
// ❌ WRONG — direct fs usage
import * as fs from "fs";
fs.writeFileSync(filePath, content, "utf-8");
fs.existsSync(filePath);
fs.mkdirSync(dirPath, { recursive: true });
fs.unlinkSync(filePath);
API Reference
FileSystemHandler extends BaseConnector and is accessible via Loader.getInstance().getFileSystemHandler().
All path arguments accept both forward- and back-slash separators. Paths are normalised internally via NormalizePath().
Existence and Type Checks
// Returns true if the path exists (file or directory).
public Exists($path : string) : boolean
// Returns true if the path is an empty directory or empty file.
public IsEmpty($path : string) : boolean
// Returns true if the path exists and is a regular file.
public IsFile($path : string) : boolean
// Returns true if the path exists and is a directory.
public IsDirectory($path : string) : boolean
// Returns true if the path exists and is a symbolic link.
public IsSymbolicLink($path : string) : boolean
// Returns true if the file exists and is currently locked (EBUSY).
public IsFileLocked($path : string) : boolean
Path Utilities
// Normalises path separators. Pass $osSeparator = true to use the OS native separator.
public NormalizePath($source : string, $osSeparator : boolean = false) : string
// Returns the current system temp directory (resolved from config or OS default).
public getTempPath() : string
// Returns the project base path (from ProgramArgs.ProjectBase()).
public getProjectBasePath() : string
// Returns the application data path (from ProgramArgs.AppDataPath()).
public getAppDataPath() : string
// Returns the user local app data path (%USERPROFILE%\AppData\Local on Windows, $HOME on Unix).
public getLocalAppDataPath() : string
Glob Expansion
Expands glob patterns using the glob package. The current working directory is resolved from ProgramArgs.TargetBase().
public Expand($pattern : string | string[]) : string[]
- Accepts a single pattern string or an array of patterns.
- Patterns starting with
!are treated as exclusions. - Returns an array of resolved file paths.
// Example: find all TypeScript files excluding test directories
const files : string[] = fsh.Expand([
"source/**/*.ts",
"!source/**/*.spec.ts"
]);
Directory Operations
// Creates a directory and all parent directories (like mkdir -p).
public CreateDirectory($path : string) : boolean
File and Directory Operations
// Renames or moves a file or directory. Returns false if source does not exist.
public Rename($oldPath : string, $newPath : string) : boolean
Read / Write
// Reads a file and returns its content. Returns empty string if file does not exist.
// Relative paths are resolved against ProgramArgs.TargetBase().
public Read($path : string) : string | Buffer
// Writes data to a file. Creates parent directories automatically.
// Pass $append = true to append instead of overwrite.
public Write($path : string, $data : any, $append : boolean = false) : boolean
Example — read a JSON config file:
const raw : string = fsh.Read("config/app.json") as string;
const config : any = JSON.parse(raw.toString());
Copy
public Copy(
$sourcePath : string,
$destinationPath : string,
$callback? : (($status: boolean) => void) | IResponse
) : void
public async CopyAsync(
$sourcePath : string,
$destinationPath : string
) : Promise<void>
- For files: uses Node.js streams with original permission bits preserved.
- For directories: uses
Robocopy /E /MTon Windows,cp -Rpon Unix. - If destination is an existing directory, the source basename is appended.
Download
Downloads a file via HTTP/HTTPS or copies from a file:// URL.
public Download(
$urlOrOptions : string | IFileSystemDownloadOptions,
$callback? : (($headers: IncomingHttpHeaders, $bodyOrPath: string) => void) | IResponse,
$onHttpError? : ($httpResponse: any, $callback: IResponse) => boolean
) : void
public async DownloadAsync(
$urlOrOptions : string | IFileSystemDownloadOptions
) : Promise<IDownloadResult>
IFileSystemDownloadOptions key fields:
| Field | Type | Default | Description |
|---|---|---|---|
url | string | — | Target URL (http://, https://, or file://). |
method | HttpMethodType | GET | HTTP method. |
headers | object | {} | Request headers. |
body | string | "" | Request body (for POST/PUT). |
streamOutput | boolean | false | When true, returns content as string/Buffer instead of saving to file. |
streamReturnsBuffer | boolean | false | When streamOutput is true, return raw Buffer. |
strictSSL | boolean | true | Enforce SSL certificate validation. |
verbose | boolean | true | Log download progress. |
maxReconnection | number | 5 | Auto-reconnect attempts on dropped connection. |
mTLS | object | — | mTLS config: { cert, key, ca? } — paths resolved via Read(). |
proxy | false | — | Pass false to disable proxy for this request. |
The callback receives ($headers, $bodyOrPath) where $bodyOrPath is:
- The temp file path when
streamOutput = false. - The response body string or Buffer when
streamOutput = true.
// Example: download to temp file
const fsh = Loader.getInstance().getFileSystemHandler();
fsh.Download("https://example.com/file.zip", ($headers, $tmpPath) : void => {
LogIt.Info("Downloaded to: " + $tmpPath);
});
// Example: fetch JSON body in-memory
const result = await fsh.DownloadAsync({
url : "https://api.example.com/data",
streamOutput: true,
headers : { "Authorization": "Bearer token" }
});
const data = JSON.parse(result.bodyOrPath as string);
HTTP status handling
Download() treats only 200 and 201 as success (plus 301/302, which are followed as redirects). Every other status code — including the other 2xx codes such as 202 Accepted and 204 No Content — is routed to the $onHttpError handler instead of the success callback.
When no $onHttpError is supplied, the default handler fails the response with OnError("Bad response status code: <code> (<text>)"). DownloadAsync() always supplies an error handler that rejects the promise with Error("<statusMessage> (<statusCode>)") — so from DownloadAsync()'s perspective anything that is not 200/201 is a failure, even a 2xx acknowledgement.
A 202 Accepted — returned by async webhooks such as Power Automate "Workflows" Teams webhooks — makes DownloadAsync() reject with Error("Accepted (202)") even though the request actually succeeded. When a caller must accept the whole 2xx range, use the lower-level Download() with a custom $onHttpError rather than DownloadAsync().
$onHttpError($httpResponse, $callback) is the extension point for statuses Download() does not handle natively. It receives the raw Node response (or an Error object on URL/connection failures) plus the IResponse, and returns true once the status is handled. Resolve the request via $callback.OnComplete(...) (or, when wrapping in a promise, the surrounding $resolve) and fail it via $callback.OnError(...):
import { CallbackResponse } from "@io-oidis-localhost/Io/Oidis/Localhost/HttpProcessor/ResponseApi/Handlers/CallbackResponse.js";
import { ObjectValidator } from "@io-oidis-commons/Io/Oidis/Commons/Utils/ObjectValidator.js";
import { HttpMethodType } from "@io-oidis-commons/Io/Oidis/Commons/Enums/HttpMethodType.js";
// Accept the whole 2xx range (e.g. 202 from a Power Automate Workflows webhook)
await new Promise<void>(($resolve : () => void, $reject : ($error : Error) => void) : void => {
const callback : CallbackResponse = new CallbackResponse(() : void => {
$resolve();
});
callback.OnError = ($message : string | Error) : void => {
$reject(ObjectValidator.IsString($message) ? new Error(<string>$message) : <Error>$message);
};
fsh.Download({
url: webhookURL, method: HttpMethodType.POST, headers, body: payload, streamOutput: true, verbose: false
}, callback, ($httpResponse : any) : boolean => {
if (!ObjectValidator.IsEmptyOrNull($httpResponse) && !ObjectValidator.IsEmptyOrNull($httpResponse.statusCode)) {
if ($httpResponse.statusCode >= 200 && $httpResponse.statusCode < 300) {
$resolve();
} else {
$reject(new Error($httpResponse.statusMessage + " (" + $httpResponse.statusCode + ")"));
}
} else {
$reject($httpResponse instanceof Error ? $httpResponse : new Error("request failed"));
}
return true;
});
});
AbortDownload
public AbortDownload($id : number) : boolean
Aborts an in-progress download by the response ID returned from the active IResponse. Returns true if the download was found and aborted.
Delete
public Delete(
$pathOrOptions : string | IFSDeleteOptions,
$callback? : (($status: boolean) => void) | IResponse
) : boolean
public async DeleteAsync($pathOrOptions : string | IFSDeleteOptions) : Promise<void>
IFSDeleteOptions:
| Field | Type | Default | Description |
|---|---|---|---|
path | string | — | Path to delete. |
ignoreLocked | boolean | false | Skip locked files instead of throwing. Use with caution. |
The synchronous Delete() uses fs.rmSync with recursive: true internally and falls back to a manual recursive walk on locked-file errors when ignoreLocked is set. DeleteAsync() uses fs.rm.
Pack (Archive)
Creates a ZIP, TAR, TAR.GZ, or TAR.BZ2 archive from a path or list of paths.
public Pack(
$path : string | string[],
$options? : IArchiveOptions,
$callback?: (($tmpPath: string) => void) | IResponse
) : void
public async PackAsync(
$path : string | string[],
$options? : IArchiveOptions
) : Promise<string>
IArchiveOptions for packing:
| Field | Type | Default | Description |
|---|---|---|---|
output | string | Auto-generated in getTempPath() | Output archive file path. |
type | string | "zip" (Win) / "tar.bz2" (Unix) | Archive format: "zip", "tar", "tar.gz", "tar.bz2". |
override | boolean | true | Delete existing archive before creating. |
autoStrip | boolean | false | Strip root folder from archive entries. |
cwd | string | Derived from $path | Base directory for archive paths. |
The callback receives the path to the created archive file.
// Example: create a zip archive
const tmpPath : string = await fsh.PackAsync("dist/**/*", {
output : "/build/output/release.zip",
type : "zip",
override: true
});
Unpack (Extract)
Extracts a ZIP, TAR, TAR.GZ, or TAR.BZ2 archive.
public Unpack(
$path : string,
$options? : IUnpackOptions,
$callback?: (($tmpPath: string) => void) | IResponse
) : void
public async UnpackAsync(
$path : string,
$options? : IUnpackOptions,
$onProgress? : ($done : number, $total : number) => void
) : Promise<string>
IUnpackOptions (extends IArchiveOptions, exported from Io.Oidis.Localhost.Connectors.FileSystemHandler):
| Field | Type | Default | Description |
|---|---|---|---|
output | string | Auto-generated in getTempPath() | Directory to extract into. |
override | boolean | true | Delete destination directory before extracting. |
autoStrip | boolean | true | Strip first path component from archive entries. |
native | boolean | false | Extract with the system tar and report progress. |
The callback receives the output directory path. By default the extraction uses decompress with plugins for
ZIP, TAR, TAR.BZ2, and TAR.GZ, entirely in-process. UnpackAsync rejects when the extraction fails.
Native extraction (native: true)
Opt-in, for large archives and small machines. decompress holds the whole archive and every extracted file
in memory before writing anything (measured on a 56 MB release zip with 9 384 files: about 49 s and 600 MB
peak), and it cannot say how far it got. With native: true the system tar streams the entries to disk
(about 3 s for the same zip):
- Windows: only
%SystemRoot%\System32\tar.exe(bsdtar, Windows 10 1803+ and Windows 11), which also reads zip. A tar found onPATHis never used: the Git/MSYS GNU tar cannot read zip and parsesC:as a remote host. - macOS: the system bsdtar, zip included.
- Linux: GNU tar, so TAR, TAR.GZ and TAR.BZ2 only; a zip goes to the in-process path.
Progress is the count of extracted entries against an upfront tar -tf listing. The callback form
receives it through IResponse.OnChange({currentValue, rangeStart, rangeEnd}), the same shape as
Download; UnpackAsync passes it to $onProgress($done, $total), throttled to about four calls a second.
Whatever tar cannot do (no system tar, a zip under GNU tar, a failed listing or extraction) falls back to the
in-process decompress path after clearing the output, so opting in is never worse than not.
// Example: extract a zip to a specific path
const outDir : string = await fsh.UnpackAsync("/tmp/release.zip", {
output : "/app/release",
autoStrip: true
});
// Example: native extraction with progress
await fsh.UnpackAsync("/tmp/release.tar.gz", {native: true, output: "/app/staging"},
($done : number, $total : number) : void => {
LogIt.Info("unpacked {0} / {1}", String($done), String($total));
});
Security Restrictions
Read()validates that relative paths are resolved againstProgramArgs.TargetBase()and never traverse outside the configured base.Download()only supportshttp://,https://, andfile://protocols. Other protocols are rejected with an error.Delete()withignoreLocked: truelogs a warning before proceeding — this mode intentionally skips locked files and may leave the filesystem in a partially deleted state.NormalizePath()normalises both forward- and back-slashes but does not enforce containment. Path traversal validation must be applied at the caller level.
See also: Connectors Reference | Terminal | Utilities Overview | Vanilla vs Framework