Skip to main content

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)

Never use fs directly

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​

MethodDescription
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​

MethodReturnsDescription
Exists($path)booleanChecks if file or directory exists
IsEmpty($path)booleanReturns true if directory is empty or file has zero bytes
IsFile($path)booleanReturns true if path is a file
IsDirectory($path)booleanReturns true if path is a directory
IsSymbolicLink($path)booleanReturns true if path is a symbolic link
IsFileLocked($path)booleanReturns true if file is locked/in use

File Operations​

MethodReturnsDescription
Read($path)string | BufferReads file content. Returns Buffer — always call .toString() when you need a string.
Write($path, $data, $append?)booleanWrites/appends content to file. Creates parent directories automatically.
Rename($oldPath, $newPath)booleanRenames/moves a file or directory
Copy($source, $dest, $callback?)voidCopies file or directory (callback-based)
CopyAsync($source, $dest)Promise<void>Async version of Copy

Directory Operations​

MethodReturnsDescription
CreateDirectory($path)booleanCreates directory recursively (includes parent dirs)
Expand($pattern)string[]Glob pattern matching. Supports *, **, ! exclusion prefix.

Delete Operations​

MethodReturnsDescription
Delete($pathOrOptions, $callback?)booleanDeletes file or directory (recursive)
DeleteAsync($pathOrOptions)Promise<void>Async deletion with options support

Download Operations​

MethodReturnsDescription
Download($urlOrOptions, $callback?, $onHttpError?)voidDownloads from HTTP(S) or file:// URL with progress tracking
DownloadAsync($urlOrOptions)Promise<IDownloadResult>Async download returning {headers, bodyOrPath}
AbortDownload($id)booleanAborts active download by response ID

Archive Operations​

MethodReturnsDescription
Pack($path, $options?, $callback?)voidCreates ZIP or TAR archive (with gzip/bzip2 compression)
PackAsync($path, $options?)Promise<string>Async packing, returns archive path
Unpack($path, $options?, $callback?)voidExtracts ZIP/TAR archives
UnpackAsync($path, $options?, $onProgress?)Promise<string>Async unpacking, returns extraction path; $onProgress reports entries when native is set
Unpack reads the whole archive into memory

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​

ScenarioMethod
Check if a file/directory existsExists()
Read a file as stringRead().toString() — Read() returns Buffer, call .toString()
Write/overwrite a fileWrite($path, $data) — creates parent dirs automatically
Append to a fileWrite($path, $data, true)
Create a directory treeCreateDirectory() — recursive by default
Delete a file or directoryDelete() or DeleteAsync()
Find files by patternExpand("**/*.ts")
Download a file from URLDownloadAsync()
Create an archivePackAsync()

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​

Always call .toString() on Read() results

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 /MT on Windows, cp -Rp on 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:

FieldTypeDefaultDescription
urlstring—Target URL (http://, https://, or file://).
methodHttpMethodTypeGETHTTP method.
headersobject{}Request headers.
bodystring""Request body (for POST/PUT).
streamOutputbooleanfalseWhen true, returns content as string/Buffer instead of saving to file.
streamReturnsBufferbooleanfalseWhen streamOutput is true, return raw Buffer.
strictSSLbooleantrueEnforce SSL certificate validation.
verbosebooleantrueLog download progress.
maxReconnectionnumber5Auto-reconnect attempts on dropped connection.
mTLSobject—mTLS config: { cert, key, ca? } — paths resolved via Read().
proxyfalse—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.

2xx is not always success for DownloadAsync

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:

FieldTypeDefaultDescription
pathstring—Path to delete.
ignoreLockedbooleanfalseSkip 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:

FieldTypeDefaultDescription
outputstringAuto-generated in getTempPath()Output archive file path.
typestring"zip" (Win) / "tar.bz2" (Unix)Archive format: "zip", "tar", "tar.gz", "tar.bz2".
overridebooleantrueDelete existing archive before creating.
autoStripbooleanfalseStrip root folder from archive entries.
cwdstringDerived from $pathBase 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):

FieldTypeDefaultDescription
outputstringAuto-generated in getTempPath()Directory to extract into.
overridebooleantrueDelete destination directory before extracting.
autoStripbooleantrueStrip first path component from archive entries.
nativebooleanfalseExtract 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 on PATH is never used: the Git/MSYS GNU tar cannot read zip and parses C: 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 against ProgramArgs.TargetBase() and never traverse outside the configured base.
  • Download() only supports http://, https://, and file:// protocols. Other protocols are rejected with an error.
  • Delete() with ignoreLocked: true logs 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