Project Files
src / filesystem.ts
import * as fs from "node:fs";
import * as fsp from "node:fs/promises";
import * as path from "node:path";
import { extractMetadataList } from "./extract";
import { flattenToMetadataList } from "./flatten";
import { updateImageExif } from "./exifEdit";
import { embedCharacterCardInPngFile, type PngCharacterKeyword } from "./pngCharacterWrite";
import { parseLocalMediaRoot } from "./localMedia";
import { configuredBaseDir, resolveUnderRoot, validateRelativeName } from "./paths";
type Row = { path: string; value: unknown };
export function fsListFiles(baseDirRaw: string): Row[] {
try {
const root = configuredBaseDir(baseDirRaw);
const names = fs.readdirSync(root).sort((a, b) => a.localeCompare(b));
if (names.length === 0) return [{ path: "list_files.message", value: "Directory is empty" }];
const rows: Row[] = [];
for (let i = 0; i < names.length; i++) {
rows.push({ path: `list_files.entries[${i}]`, value: names[i] });
}
return rows;
} catch (e) {
return [{ path: "list_files.error", value: String(e) }];
}
}
export async function fsReadFile(baseDirRaw: string, fileName: string): Promise<Row[]> {
try {
validateRelativeName(fileName, "file_name");
} catch (e) {
return [{ path: "read_file.error", value: String(e) }];
}
try {
const root = configuredBaseDir(baseDirRaw);
const fullPath = resolveUnderRoot(root, fileName);
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
return [{ path: "read_file.error", value: "Error: File does not exist" }];
}
const text = await fsp.readFile(fullPath, "utf8");
return [{ path: "read_file.content", value: text }];
} catch (e) {
return [{ path: "read_file.error", value: String(e) }];
}
}
export async function fsWriteFile(baseDirRaw: string, fileName: string, content: string): Promise<Row[]> {
try {
validateRelativeName(fileName, "file_name");
} catch (e) {
return [{ path: "write_file.error", value: String(e) }];
}
try {
const root = configuredBaseDir(baseDirRaw);
const fullPath = resolveUnderRoot(root, fileName);
await fsp.mkdir(path.dirname(fullPath), { recursive: true });
await fsp.writeFile(fullPath, content, "utf8");
return [{ path: "write_file.message", value: "File created or updated successfully" }];
} catch (e) {
return [{ path: "write_file.error", value: String(e) }];
}
}
export async function fsCreateDirectory(baseDirRaw: string, directoryName: string): Promise<Row[]> {
try {
validateRelativeName(directoryName, "directory_name");
} catch (e) {
return [{ path: "create_directory.error", value: String(e) }];
}
try {
const root = configuredBaseDir(baseDirRaw);
const fullPath = resolveUnderRoot(root, directoryName);
await fsp.mkdir(fullPath, { recursive: true });
return [{ path: "create_directory.message", value: `Directory '${directoryName}' created successfully` }];
} catch (e) {
return [{ path: "create_directory.error", value: String(e) }];
}
}
export async function fsExtractMetadata(
baseDirRaw: string,
sourceFileName: string,
outputJsonFileName: string | null | undefined,
includePiexif: boolean,
): Promise<Row[]> {
try {
validateRelativeName(sourceFileName, "source_file_name");
} catch (e) {
return [{ path: "extract_metadata.error", value: String(e) }];
}
if (outputJsonFileName != null && String(outputJsonFileName).trim() !== "") {
try {
validateRelativeName(String(outputJsonFileName), "output_json_file_name");
} catch (e) {
return [{ path: "extract_metadata.error", value: String(e) }];
}
}
try {
const root = configuredBaseDir(baseDirRaw);
const fullSource = resolveUnderRoot(root, sourceFileName);
if (!fs.existsSync(fullSource) || !fs.statSync(fullSource).isFile()) {
return [{ path: "extract_metadata.error", value: "Error: File does not exist" }];
}
const rows = await extractMetadataList(fullSource, includePiexif);
const outName = outputJsonFileName != null ? String(outputJsonFileName).trim() : "";
if (outName) {
const fullOut = resolveUnderRoot(root, outName);
await fsp.mkdir(path.dirname(fullOut), { recursive: true });
await fsp.writeFile(fullOut, JSON.stringify(rows, null, 2), "utf8");
}
return rows;
} catch (e) {
return [{ path: "extract_metadata.error", value: String(e) }];
}
}
export async function fsWritePngCharacterCard(
baseDirRaw: string,
fileName: string,
cardJson: string | Record<string, unknown>,
keyword: PngCharacterKeyword,
compress: boolean,
): Promise<Row[]> {
const err = "write_png_character_card.error";
try {
validateRelativeName(fileName, "file_name");
} catch (e) {
return [{ path: err, value: String(e) }];
}
try {
const root = configuredBaseDir(baseDirRaw);
const fullPath = resolveUnderRoot(root, fileName);
if (!fileName.toLowerCase().endsWith(".png")) {
return [{ path: err, value: "write_png_character_card requires a .png file_name" }];
}
const summary = embedCharacterCardInPngFile(fullPath, cardJson, { keyword, compress });
return flattenToMetadataList(summary, "write_png_character_card");
} catch (e) {
return [{ path: err, value: String(e) }];
}
}
export async function fsUpdateLocalMediaPngCharacterCard(
localMediaRoot: string,
imageRelativePath: string,
cardJson: string | Record<string, unknown>,
keyword: PngCharacterKeyword,
compress: boolean,
): Promise<Row[]> {
const err = "update_local_media_png_character_card.error";
try {
const localRoot = parseLocalMediaRoot(localMediaRoot);
const fullPath = resolveUnderRoot(localRoot, imageRelativePath);
if (!imageRelativePath.toLowerCase().endsWith(".png")) {
return [{ path: err, value: "update_local_media_png_character_card requires a .png path" }];
}
const summary = embedCharacterCardInPngFile(fullPath, cardJson, { keyword, compress });
return flattenToMetadataList(summary, "update_local_media_png_character_card");
} catch (e) {
return [{ path: err, value: String(e) }];
}
}
export async function fsWriteMetadata(
baseDirRaw: string,
fileName: string,
setTags: Record<string, Record<string, unknown>> | null | undefined,
removeTags: Record<string, string[]> | null | undefined,
): Promise<Row[]> {
const err = "write_metadata.error";
try {
validateRelativeName(fileName, "file_name");
} catch (e) {
return [{ path: err, value: String(e) }];
}
try {
const root = configuredBaseDir(baseDirRaw);
const fullPath = resolveUnderRoot(root, fileName);
const summary = updateImageExif(fullPath, setTags || {}, removeTags || {});
return flattenToMetadataList(summary, "write_metadata");
} catch (e) {
return [{ path: err, value: String(e) }];
}
}