Added utility methods for reading zipped files

This commit is contained in:
Kir_Antipov 2024-01-15 15:04:39 +03:00
parent e407797735
commit cf10a77ae0
2 changed files with 127 additions and 18 deletions

View file

@ -1,7 +1,8 @@
import { $i } from "@/utils/collections"; import { $i } from "@/utils/collections";
import { FileNotFoundError } from "@/utils/errors"; import { FileNotFoundError } from "@/utils/errors";
import glob from "fast-glob"; import glob from "fast-glob";
import { ReadStream, createReadStream, existsSync, readFileSync as readFileNodeSync, statSync } from "node:fs"; import StreamZip from "node-stream-zip";
import { PathLike, ReadStream, createReadStream, existsSync, readFileSync as readFileNodeSync, statSync } from "node:fs";
import { readFile as readFileNode } from "node:fs/promises"; import { readFile as readFileNode } from "node:fs/promises";
import { basename, dirname } from "node:path"; import { basename, dirname } from "node:path";
@ -195,13 +196,9 @@ export function findFilesSync(pattern: string | string[]): FileInfo[] {
* *
* @throws {FileNotFoundError} - If no files matching the pattern are found. * @throws {FileNotFoundError} - If no files matching the pattern are found.
*/ */
export async function readFile(pattern: string): Promise<Buffer> { export async function readFile(pattern: PathLike): Promise<Buffer> {
const files = await glob(pattern); const file = await getFileName(pattern);
if (!files?.length) { return await readFileNode(file);
throw new FileNotFoundError(pattern);
}
return await readFileNode(files[0]);
} }
/** /**
@ -214,10 +211,45 @@ export async function readFile(pattern: string): Promise<Buffer> {
* *
* @throws {FileNotFoundError} - If no files matching the pattern are found. * @throws {FileNotFoundError} - If no files matching the pattern are found.
*/ */
export async function readAllText(pattern: string, encoding?: BufferEncoding): Promise<string> { export async function readAllText(pattern: PathLike, encoding?: BufferEncoding): Promise<string> {
return (await readFile(pattern)).toString(encoding); return (await readFile(pattern)).toString(encoding);
} }
/**
* Reads a zipped file and returns its content as a Buffer.
*
* @param pattern - The glob pattern used to locate the zip archive.
* @param entry - The entry name of the file within the zip archive.
*
* @returns A promise that resolves to a Buffer containing the file contents.
*/
export async function readZippedFile(pattern: PathLike, entry: string) : Promise<Buffer> {
const file = await getFileName(pattern);
let zip = undefined as StreamZip.StreamZipAsync;
try {
// Dude, it's not my constructor, calm down.
// eslint-disable-next-line new-cap
zip = new StreamZip.async({ file });
return await zip.entryData(entry);
} finally {
await zip?.close().catch(() => undefined);
}
}
/**
* Reads a zipped file and returns its content as a string.
*
* @param pattern - The glob pattern used to locate the zip archive.
* @param entry - The entry name of the file within the zip archive.
* @param encoding - The optional encoding to use for reading the file. Defaults to `utf8`.
*
* @returns A promise that resolves to a string containing the file contents.
*/
export async function readAllZippedText(pattern: PathLike, entry: string, encoding?: BufferEncoding) : Promise<string> {
return (await readZippedFile(pattern, entry)).toString(encoding);
}
/** /**
* Reads the contents of the first file matching the specified glob pattern synchronously. * Reads the contents of the first file matching the specified glob pattern synchronously.
* *
@ -227,13 +259,9 @@ export async function readAllText(pattern: string, encoding?: BufferEncoding): P
* *
* @throws {FileNotFoundError} - If no files matching the pattern are found. * @throws {FileNotFoundError} - If no files matching the pattern are found.
*/ */
export function readFileSync(pattern: string): Buffer { export function readFileSync(pattern: PathLike): Buffer {
const files = glob.sync(pattern); const file = getFileNameSync(pattern);
if (!files?.length) { return readFileNodeSync(file);
throw new FileNotFoundError(pattern);
}
return readFileNodeSync(files[0]);
} }
/** /**
@ -246,6 +274,50 @@ export function readFileSync(pattern: string): Buffer {
* *
* @throws {FileNotFoundError} - If no files matching the pattern are found. * @throws {FileNotFoundError} - If no files matching the pattern are found.
*/ */
export function readAllTextSync(pattern: string, encoding?: BufferEncoding): string { export function readAllTextSync(pattern: PathLike, encoding?: BufferEncoding): string {
return readFileSync(pattern).toString(encoding); return readFileSync(pattern).toString(encoding);
} }
/**
* Retrieves the name of the first file that matches the specified glob pattern.
*
* @param pattern - The file path or glob pattern.
*
* @returns The name of the first matching file.
*
* @throws {FileNotFoundError} - If no matching file is found.
*/
async function getFileName(pattern: PathLike): Promise<string> {
if (existsSync(pattern)) {
return pattern.toString();
}
const files = await glob(pattern.toString());
if (files?.[0]) {
return files[0];
}
throw new FileNotFoundError(pattern.toString());
}
/**
* Synchronously retrieves the name of the first file that matches the specified glob pattern.
*
* @param pattern - The file path or glob pattern.
*
* @returns The name of the first matching file.
*
* @throws {FileNotFoundError} - If no matching file is found.
*/
function getFileNameSync(pattern: PathLike): string {
if (existsSync(pattern)) {
return pattern.toString();
}
const files = glob.sync(pattern.toString());
if (files?.[0]) {
return files[0];
}
throw new FileNotFoundError(pattern.toString());
}

View file

@ -1,5 +1,6 @@
import { statSync } from "node:fs"; import { statSync } from "node:fs";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { zipContent } from "../../../utils/zip-utils";
import { import {
FileInfo, FileInfo,
fileEquals, fileEquals,
@ -7,15 +8,18 @@ import {
findFilesSync, findFilesSync,
readAllText, readAllText,
readAllTextSync, readAllTextSync,
readAllZippedText,
readFile, readFile,
readFileSync, readFileSync,
readZippedFile,
} from "@/utils/io/file-info"; } from "@/utils/io/file-info";
beforeEach(() => { beforeEach(async () => {
mockFs({ mockFs({
"path/to": { "path/to": {
"test.txt": "test", "test.txt": "test",
"test.json": JSON.stringify({ foo: 42 }), "test.json": JSON.stringify({ foo: 42 }),
"test.zip": await zipContent("test", "test.txt"),
}, },
}); });
}); });
@ -281,3 +285,36 @@ describe("readAllTextSync", () => {
expect(() => readAllTextSync("path/from/*.txt")).toThrow(/path\/from\/\*\.txt/); expect(() => readAllTextSync("path/from/*.txt")).toThrow(/path\/from\/\*\.txt/);
}); });
}); });
describe("readZippedFile", () => {
test("reads the contents of the first matching file", async () => {
const content = await readZippedFile("path/to/*.zip", "test.txt");
expect(Buffer.isBuffer(content)).toBe(true);
expect(content.toString()).toEqual("test");
});
test("throws if no files were found", async () => {
await expect(readZippedFile("path/from/*.zip", "")).rejects.toThrow(/path\/from\/\*\.zip/);
});
test("throws if the entry does not exist within the zip", async () => {
await expect(readZippedFile("path/to/test.zip", "not-test.txt")).rejects.toThrow(/Entry not found/);
});
});
describe("readAllZippedText", () => {
test("reads the contents of the first matching file", async () => {
const content = await readAllZippedText("path/to/*.zip", "test.txt");
expect(content).toEqual("test");
});
test("throws if no files were found", async () => {
await expect(readAllZippedText("path/from/*.zip", "")).rejects.toThrow(/path\/from\/\*\.zip/);
});
test("throws if the entry does not exist within the zip", async () => {
await expect(readAllZippedText("path/to/test.zip", "not-test.txt")).rejects.toThrow(/Entry not found/);
});
});