@bouko/fs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export declare const RangeSchema: z.ZodObject<{
3
+ min: z.ZodNumber;
4
+ max: z.ZodNumber;
5
+ }, z.core.$strip>;
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export const RangeSchema = z.object({
3
+ min: z.number().int(),
4
+ max: z.number().int()
5
+ });
@@ -0,0 +1,25 @@
1
+ import { EnsureFileOptions } from "../types/file";
2
+ export declare function ensureFile(filePath: string, options?: EnsureFileOptions): Promise<void>;
3
+ export declare function readJson<T>(filePath: string, fallback?: object): Promise<T>;
4
+ type SpecialFile = {
5
+ buffer: ArrayBuffer;
6
+ last_modified: Date;
7
+ };
8
+ type Mp3File = SpecialFile & {
9
+ duration: number;
10
+ metadata: {
11
+ title: string;
12
+ artwork: string | null;
13
+ artists: string[];
14
+ };
15
+ };
16
+ type FileReturnMap = {
17
+ json: SpecialFile;
18
+ mp3: Mp3File;
19
+ };
20
+ type FileKind = keyof FileReturnMap;
21
+ type InferKind<P extends string> = P extends `${string}.mp3` ? "mp3" : P extends `${string}.json` ? "json" : never;
22
+ export declare function readFile<P extends string>(filePath: P, options?: EnsureFileOptions): Promise<FileReturnMap[InferKind<P>]>;
23
+ export declare function readFile<T extends FileKind>(filePath: string, options?: EnsureFileOptions): Promise<FileReturnMap[T]>;
24
+ export declare function saveFile(filePath: string, options: EnsureFileOptions): Promise<void>;
25
+ export {};
@@ -0,0 +1,86 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import * as mm from "music-metadata";
4
+ import { sanitizeArrayMap } from "@bouko/ts";
5
+ export async function ensureFile(filePath, options) {
6
+ const dirPath = path.dirname(filePath);
7
+ await fs.mkdir(dirPath, { recursive: true });
8
+ let contents = null;
9
+ let encoding = "utf8";
10
+ if (options?.data) {
11
+ switch (options.type) {
12
+ case "json":
13
+ contents =
14
+ JSON.stringify(options.data, null, options.pretty === false ? undefined : 2) + "\n";
15
+ break;
16
+ case "text":
17
+ contents = options.data;
18
+ break;
19
+ case "buffer":
20
+ if (!options.data)
21
+ throw new Error("Not found");
22
+ contents = options.data;
23
+ encoding = undefined;
24
+ break;
25
+ }
26
+ }
27
+ try {
28
+ if (options && contents)
29
+ await fs.writeFile(filePath, contents, {
30
+ encoding,
31
+ flag: "wx",
32
+ });
33
+ else
34
+ await fs.readFile(filePath);
35
+ }
36
+ catch (err) {
37
+ if (err.code !== "EEXIST") {
38
+ throw err;
39
+ }
40
+ }
41
+ }
42
+ export async function readJson(filePath, fallback) {
43
+ await ensureFile(filePath, {
44
+ type: "json",
45
+ data: fallback || {}
46
+ });
47
+ const raw = await fs.readFile(filePath, "utf8");
48
+ const parsed = JSON.parse(raw);
49
+ return sanitizeArrayMap(parsed);
50
+ }
51
+ export async function readFile(filePath, options) {
52
+ await ensureFile(filePath, options);
53
+ const fileType = filePath.split(".")[1];
54
+ const { mtime: last_modified } = await fs.stat(filePath);
55
+ if (fileType === "json") {
56
+ const raw = await fs.readFile(filePath, "utf8");
57
+ const parsed = JSON.parse(raw);
58
+ const sanitized = sanitizeArrayMap(parsed);
59
+ const buffer = await fs.readFile(filePath);
60
+ return {
61
+ buffer: buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
62
+ last_modified
63
+ };
64
+ }
65
+ if (fileType === "mp3") {
66
+ const buffer = await fs.readFile(filePath);
67
+ const { common, format } = await mm.parseFile(filePath, { duration: true });
68
+ const pic = common.picture?.[0] ?? null;
69
+ return {
70
+ buffer: buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
71
+ metadata: {
72
+ title: common.title ?? path.basename(filePath),
73
+ artists: common.artists ?? (common.artist ? [common.artist] : []),
74
+ artwork: pic ? `data:${pic.format};base64,${Buffer.from(pic.data).toString("base64")}` : null
75
+ },
76
+ duration: format.duration ?? 0,
77
+ last_modified
78
+ };
79
+ }
80
+ throw new Error(`Unsupported file type: ${filePath}`);
81
+ }
82
+ export async function saveFile(filePath, options) {
83
+ const dirPath = path.dirname(filePath);
84
+ await fs.mkdir(dirPath, { recursive: true });
85
+ await fs.writeFile(filePath, JSON.stringify(options.data, null, 2), "utf8");
86
+ }
@@ -0,0 +1,12 @@
1
+ export declare const createFolder: (folderPath: string) => Promise<string | undefined>;
2
+ export declare const fetchFiles: ({ folderPath, ext, page, pageSize }: {
3
+ folderPath: string;
4
+ ext: string;
5
+ page: number;
6
+ pageSize: number;
7
+ }) => Promise<{
8
+ items: string[];
9
+ page: number;
10
+ totalItems: number;
11
+ totalPages: number;
12
+ }>;
@@ -0,0 +1,22 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { byName } from "@bouko/ts";
4
+ export const createFolder = (folderPath) => fs.mkdir(folderPath, { recursive: true });
5
+ export const fetchFiles = async ({ folderPath, ext, page, pageSize }) => {
6
+ const entries = await fs.readdir(folderPath, { withFileTypes: true });
7
+ const filePaths = entries
8
+ .filter((e) => e.isFile() && e.name.toLowerCase().endsWith("." + ext))
9
+ .map((e) => path.join(folderPath, e.name))
10
+ .sort(byName);
11
+ const totalItems = filePaths.length;
12
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
13
+ const safePage = Math.min(Math.max(page, 0), totalPages - 1);
14
+ const start = safePage * pageSize;
15
+ console.log(filePaths.slice(start, start + pageSize));
16
+ return {
17
+ items: filePaths.slice(start, start + pageSize),
18
+ page: safePage,
19
+ totalItems,
20
+ totalPages
21
+ };
22
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./functions/folder";
2
+ export * from "./functions/file";
3
+ export * from "./types/file";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./functions/folder";
2
+ export * from "./functions/file";
3
+ export * from "./types/file";
@@ -0,0 +1,11 @@
1
+ export type EnsureFileOptions = {
2
+ type: "json";
3
+ data: object;
4
+ pretty?: boolean;
5
+ } | {
6
+ type: "text";
7
+ data: string;
8
+ } | {
9
+ type: "buffer";
10
+ data?: Uint8Array;
11
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Type definitions for common data structures.
3
+ *
4
+ * - `StringMap`: Map of string keys to string values.
5
+ * - `JsonMap`: Object of primitives, arrays, or nested maps.
6
+ * - `JsonValue`: Union type representing a valid JSON value.
7
+ *
8
+ * They are intended to facilitate type-safe handling
9
+ * of generic objects (ex. forms).
10
+ **/
11
+ export type StringMap<T = string> = Record<string, T>;
12
+ export type ArrayMap<T = string> = Record<string, T[]>;
13
+ export type JsonMap = {
14
+ [key: string]: JsonValue | JsonValue[];
15
+ };
16
+ export type JsonValue = string | number | boolean | null | undefined | JsonMap | JsonValue[];
17
+ export type PageQuery = {
18
+ page: number;
19
+ pageSize: number;
20
+ refresh?: boolean;
21
+ };
22
+ export type Page<T> = {
23
+ page: number;
24
+ items: T[];
25
+ pageSize: number;
26
+ totalItems: number;
27
+ totalPages: number;
28
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Type definitions for common data structures.
3
+ *
4
+ * - `StringMap`: Map of string keys to string values.
5
+ * - `JsonMap`: Object of primitives, arrays, or nested maps.
6
+ * - `JsonValue`: Union type representing a valid JSON value.
7
+ *
8
+ * They are intended to facilitate type-safe handling
9
+ * of generic objects (ex. forms).
10
+ **/
11
+ export {};
12
+ //
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Type definitions for math/numbers.
3
+ *
4
+ * - `Range`: Object with `min` and `max` properties.
5
+ *
6
+ * They are intended to facilitate math utilities
7
+ * such as clamping, normalization, etc.
8
+ **/
9
+ export type Range = {
10
+ id?: string;
11
+ min: number;
12
+ max: number;
13
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type definitions for math/numbers.
3
+ *
4
+ * - `Range`: Object with `min` and `max` properties.
5
+ *
6
+ * They are intended to facilitate math utilities
7
+ * such as clamping, normalization, etc.
8
+ **/
9
+ export {};
10
+ // clean
@@ -0,0 +1,3 @@
1
+ export type Job = {
2
+ job_id: string;
3
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare const isNum: (x: string) => boolean;
2
+ export declare const isBool: (x: string) => boolean;
@@ -0,0 +1,5 @@
1
+ export const isNum = (x) => !isNaN(Number(x));
2
+ export const isBool = (x) => {
3
+ const lower = x.trim().toLowerCase();
4
+ return lower === "true" || lower === "false";
5
+ };
@@ -0,0 +1,21 @@
1
+ export declare function bufferToData(uint8: Uint8Array, mimetype?: string): string;
2
+ export declare function bufferToData(uint8: Uint8Array | undefined, mimetype?: string): string | undefined;
3
+ export declare function base64ToBuffer(base64: string): Uint8Array;
4
+ export declare function base64ToFile(data: string, opts?: {
5
+ name?: string;
6
+ type?: string;
7
+ }): File;
8
+ export declare function base64ToFile(data: undefined, opts?: {
9
+ name?: string;
10
+ type?: string;
11
+ }): undefined;
12
+ export declare function base64ToFile(data: string | undefined, opts?: {
13
+ name?: string;
14
+ type?: string;
15
+ }): File | undefined;
16
+ export declare function dataToFile(data: string, opts?: {
17
+ name?: string;
18
+ }): File;
19
+ export declare function dataToFile(data: string | undefined, opts?: {
20
+ name?: string;
21
+ }): File | undefined;
@@ -0,0 +1,48 @@
1
+ export function bufferToData(uint8, mimetype) {
2
+ if (!uint8)
3
+ return;
4
+ const len = uint8.byteLength;
5
+ let binary = "";
6
+ for (let i = 0; i < len; i++)
7
+ binary += String.fromCharCode(uint8[i]);
8
+ const decoded = btoa(binary);
9
+ if (mimetype)
10
+ return `data:${mimetype};base64,${decoded}`;
11
+ return decoded;
12
+ }
13
+ export function base64ToBuffer(base64) {
14
+ const binaryString = atob(base64); // decode base64 into binary string
15
+ const len = binaryString.length;
16
+ const bytes = new Uint8Array(len);
17
+ for (let i = 0; i < len; i++) {
18
+ bytes[i] = binaryString.charCodeAt(i);
19
+ }
20
+ return bytes;
21
+ }
22
+ export function base64ToFile(data, opts = {}) {
23
+ if (!data)
24
+ return;
25
+ const { name, type } = opts;
26
+ const byteArray = Uint8Array.from(atob(data), c => c.charCodeAt(0));
27
+ return new File([byteArray], name || "file", { type });
28
+ }
29
+ export function dataToFile(data, opts) {
30
+ if (!data)
31
+ return;
32
+ // If the data is in "data:[mime];base64,..." format, extract it
33
+ const matches = data.match(/^data:(.*?);base64,(.*)$/);
34
+ let mime;
35
+ let base64Data;
36
+ if (matches) {
37
+ mime = matches[1];
38
+ base64Data = matches[2];
39
+ }
40
+ else {
41
+ // If not a full data URI, just assume it's raw base64 without metadata
42
+ base64Data = data;
43
+ }
44
+ const byteArray = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
45
+ return new File([byteArray], opts?.name || "file", {
46
+ type: mime || "application/octet-stream",
47
+ });
48
+ }
@@ -0,0 +1,5 @@
1
+ export declare function getEnv(key: string): string;
2
+ export declare function getEnv(key: string, fallback: string): string;
3
+ export declare function getEnv(key: string, fallback: number): number;
4
+ export declare function getEnv(key: string, fallback: boolean): boolean;
5
+ export declare function getEnv<T>(key: string): T;
@@ -0,0 +1,23 @@
1
+ import { parseStr } from "./parsers";
2
+ /**
3
+ * Retrieves and parses an environment variable.
4
+ *
5
+ * @param key - The variable name
6
+ * @param fallback - Backup if no value is found (optional)
7
+ *
8
+ * @returns {JsonValue} The value parsed based on whichever
9
+ * primitive type the string matches.
10
+ *
11
+ * @throws {Error} If the variable is not found in the
12
+ * environment and there is no fallback set.
13
+ **/
14
+ export function getEnv(key, fallback) {
15
+ const raw = process.env[key];
16
+ if (!raw || raw === "") {
17
+ if (fallback !== undefined)
18
+ return fallback;
19
+ throw new Error(`ENV Missing: '${key}'`);
20
+ }
21
+ return parseStr(raw);
22
+ }
23
+ // clean
@@ -0,0 +1,10 @@
1
+ export declare const shortenStr: (str: string) => string;
2
+ export declare const shortenNum: (x: number) => string;
3
+ export declare const cleanStr: (str: string) => string;
4
+ export declare const stripStr: (str: string, noise: string[]) => string;
5
+ export type CanonicalTermsMap = Record<string, string[]>;
6
+ export declare function getCanonicalMatch(input: string, map: CanonicalTermsMap): {
7
+ canonical: string;
8
+ matchedTerm: string;
9
+ } | undefined;
10
+ export declare function removeAll(input: string, needle: string, flags?: string): string;
@@ -0,0 +1,38 @@
1
+ export const shortenStr = (str) => {
2
+ if (str.length <= 6)
3
+ return str;
4
+ return `${str.slice(0, 3)}...${str.slice(-3)}`;
5
+ };
6
+ export const shortenNum = (x) => {
7
+ if (x < 1000)
8
+ return x.toString();
9
+ if (x < 1_000_000)
10
+ return `${Math.floor(x / 1000)}k`;
11
+ return `${Math.floor(x / 1_000_000)}m`;
12
+ };
13
+ export const cleanStr = (str) => str
14
+ .trim()
15
+ .replace(/^[^\w(]+/g, "") // leading junk
16
+ .replace(/[^\w!?.)]+$/g, "") // trailing junk
17
+ .replace(/\s+/g, " ") // collapse spaces
18
+ .trim();
19
+ export const stripStr = (str, noise) => {
20
+ for (const word of noise)
21
+ str = str.replace(new RegExp(word, "gi"), "");
22
+ return cleanStr(str.replace("()", ""));
23
+ };
24
+ export function getCanonicalMatch(input, map) {
25
+ const lower = input.toLowerCase();
26
+ for (const [canonical, terms] of Object.entries(map)) {
27
+ const searchTerms = [canonical, ...(terms ?? [])].filter(Boolean);
28
+ const match = searchTerms.find(t => lower.includes(t.toLowerCase()));
29
+ if (match)
30
+ return { canonical, matchedTerm: match };
31
+ }
32
+ }
33
+ export function removeAll(input, needle, flags = "gi") {
34
+ return needle ? input.replace(new RegExp(escapeRegExp(needle), flags), "") : input;
35
+ }
36
+ function escapeRegExp(s) {
37
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38
+ }
@@ -0,0 +1,4 @@
1
+ type Logger = (...messages: string[]) => void;
2
+ type Config = Record<string, Logger>;
3
+ export declare const log: Config;
4
+ export {};
@@ -0,0 +1,5 @@
1
+ import chalk from "chalk";
2
+ export const log = {
3
+ info: (...msgs) => console.warn(chalk.blue.bold("[INFO]", ...msgs)),
4
+ error: (...msgs) => console.error(chalk.red.bold("[ERROR]", ...msgs))
5
+ };
@@ -0,0 +1,11 @@
1
+ import { JsonValue } from "../types/generic";
2
+ /**
3
+ * Parses a string value into it's appropriate
4
+ * type, or at least cleans up the string.
5
+ *
6
+ * @param {string} input - The text to parse.
7
+ *
8
+ * @returns {JsonValue} The value parsed based on whichever
9
+ * primitive type the string matches.
10
+ **/
11
+ export declare const parseStr: (input: string) => JsonValue;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Parses a string value into it's appropriate
3
+ * type, or at least cleans up the string.
4
+ *
5
+ * @param {string} input - The text to parse.
6
+ *
7
+ * @returns {JsonValue} The value parsed based on whichever
8
+ * primitive type the string matches.
9
+ **/
10
+ export const parseStr = (input) => {
11
+ const trimmed = input.trim();
12
+ if (/^-?\d+(\.\d+)?$/.test(trimmed) && !isNaN(Number(trimmed)))
13
+ return Number(trimmed);
14
+ if (/^(true|false)$/i.test(trimmed))
15
+ return trimmed.toLowerCase() === "true";
16
+ try {
17
+ return JSON.parse(trimmed);
18
+ }
19
+ catch {
20
+ return trimmed;
21
+ }
22
+ };
23
+ // clean
@@ -0,0 +1 @@
1
+ export declare const byName: (a: string, b: string) => number;
@@ -0,0 +1 @@
1
+ export const byName = (a, b) => a.localeCompare(b, undefined, { sensitivity: "base" });
@@ -0,0 +1,6 @@
1
+ export declare function mmSS(timestamp: number): string;
2
+ export declare const secToMs: (sec: number) => number;
3
+ export declare function isoToMs(duration: string): number;
4
+ export declare const sleep: (ms: number) => Promise<unknown>;
5
+ export declare const isExpired: (date: Date | string) => boolean;
6
+ export declare const fromNow: (ms: number) => Date;
@@ -0,0 +1,36 @@
1
+ export function mmSS(timestamp) {
2
+ const totalSeconds = Math.floor(timestamp / 1000);
3
+ const minutes = Math.floor(totalSeconds / 60);
4
+ const seconds = totalSeconds % 60;
5
+ const padSeconds = (n) => n.toString().padStart(2, "0");
6
+ return `${minutes}:${padSeconds(seconds)}`;
7
+ }
8
+ export const secToMs = (sec) => {
9
+ const converted = sec * 1000;
10
+ return Math.floor(converted);
11
+ };
12
+ export function isoToMs(duration) {
13
+ const regex = /P(?:([\d.]+)Y)?(?:([\d.]+)M)?(?:([\d.]+)W)?(?:([\d.]+)D)?(?:T(?:([\d.]+)H)?(?:([\d.]+)M)?(?:([\d.]+)S)?)?/;
14
+ const matches = duration.match(regex);
15
+ if (!matches)
16
+ return 0;
17
+ const [, years, months, weeks, days, hours, minutes, seconds] = matches;
18
+ // Approximate conversions for years/months if needed
19
+ const ms = (parseFloat(years || "0") * 365 * 24 * 60 * 60 +
20
+ parseFloat(months || "0") * 30 * 24 * 60 * 60 +
21
+ parseFloat(weeks || "0") * 7 * 24 * 60 * 60 +
22
+ parseFloat(days || "0") * 24 * 60 * 60 +
23
+ parseFloat(hours || "0") * 60 * 60 +
24
+ parseFloat(minutes || "0") * 60 +
25
+ parseFloat(seconds || "0")) * 1000;
26
+ return ms;
27
+ }
28
+ export const sleep = (ms) => {
29
+ return new Promise((res) => setTimeout(res, ms));
30
+ };
31
+ export const isExpired = (date) => {
32
+ if (typeof date === "string")
33
+ date = new Date(date);
34
+ return new Date() > date;
35
+ };
36
+ export const fromNow = (ms) => new Date(Date.now() + ms);
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+
3
+ "name": "@bouko/fs",
4
+
5
+ "version": "0.0.1",
6
+
7
+ "main": "./dist/index.js",
8
+
9
+ "types": "./dist/index.d.ts",
10
+
11
+ "license": "MIT",
12
+
13
+ "files": [
14
+
15
+ "dist"
16
+
17
+ ],
18
+
19
+ "publishConfig": {
20
+
21
+ "access": "public"
22
+
23
+ },
24
+
25
+ "author": "",
26
+
27
+ "description": "",
28
+
29
+ "engines": {},
30
+
31
+ "scripts": {
32
+
33
+ "build": "tsc"
34
+
35
+ },
36
+
37
+ "dependencies": {
38
+
39
+ "@bouko/ts": "^0.4.7",
40
+
41
+ "fs": "^0.0.1-security",
42
+
43
+ "music-metadata": "^11.12.1",
44
+
45
+ "path": "^0.12.7"
46
+
47
+ },
48
+
49
+ "devDependencies": {
50
+
51
+ "@types/node": "^24.3.0",
52
+
53
+ "typescript": "^5.9.2"
54
+
55
+ }
56
+
57
+ }
58
+