UNPKG

farjs-app

Version:

FAR.js - Cross-platform File and Archive Manager app in your terminal

235 lines (203 loc) 7.12 kB
/** * @import StreamReader from "@farjs/filelist/util/StreamReader.mjs" * @typedef {import("@farjs/filelist/api/FileListDir.mjs").FileListDir} FileListDir * @typedef {import("@farjs/filelist/api/FileListItem.mjs").FileListItem} FileListItem * @typedef {import("@farjs/filelist/util/SubProcess.mjs").SubProcess} SubProcess */ import FileListApi from "@farjs/filelist/api/FileListApi.mjs"; import FileListDir from "@farjs/filelist/api/FileListDir.mjs"; import FileListItem from "@farjs/filelist/api/FileListItem.mjs"; import FileListCapability from "@farjs/filelist/api/FileListCapability.mjs"; import { stripPrefix } from "@farjs/filelist/utils.mjs"; import SubProcess from "@farjs/filelist/util/SubProcess.mjs"; import ZipEntry from "./ZipEntry.mjs"; class ZipApi extends FileListApi { /** * @param {string} zipPath * @param {string} rootPath * @param {Promise<Map<string, readonly FileListItem[]>>} entriesByParentP */ constructor(zipPath, rootPath, entriesByParentP) { super(false, new Set([FileListCapability.read, FileListCapability.delete])); /** @readonly @type {string} */ this.zipPath = zipPath; /** @readonly @type {string} */ this.rootPath = rootPath; /** @private @type {Promise<Map<string, readonly FileListItem[]>>} */ this.entriesByParentP = entriesByParentP; } /** @type {FileListApi['readDir']} */ async readDir(parent, dir) { const path = parent === "" ? this.rootPath : parent; const targetDir = dir === undefined ? path : (() => { if (dir === FileListItem.up.name) { const lastSlash = path.lastIndexOf("/"); return path.substring(0, Math.max(lastSlash, 0)); } return dir === FileListItem.currDir.name ? path : `${path}/${dir}`; })(); const entriesByParent = await this.entriesByParentP; const parentPath = stripPrefix(stripPrefix(targetDir, this.rootPath), "/"); const entries = entriesByParent.get(parentPath) ?? []; return FileListDir(targetDir, false, entries); } /** @type {FileListApi['readFile']} */ async readFile(parent, item) { const filePath = stripPrefix( stripPrefix(`${parent}/${item.name}`, this.rootPath), "/", ); const { stdout, exitP } = await this.extract(this.zipPath, filePath); let pos = 0; return { file: filePath, readNextBytes: async (buff) => { const content = await stdout.readNextBytes(buff.length); if (content !== undefined) { const bytesRead = content.length; content.copy(buff, 0, 0, bytesRead); pos += bytesRead; return bytesRead; } if (pos !== item.size) { const error = await exitP; if (error) { throw error; } } return 0; }, close: async () => { if (pos !== item.size) { stdout.readable.destroy(); } await exitP; }, }; } /** @type {FileListApi['delete']} */ delete(parent, items) { const self = this; /** @type {(parent: string, items: readonly FileListItem[]) => void} */ function deleteFromState(parent, items) { self.entriesByParentP = self.entriesByParentP.then((entriesByParent) => { return items.reduce((entries, item) => { const currItems = entries.get(parent); if (currItems !== undefined) { const newItems = currItems.filter((_) => _.name !== item.name); entries.set(parent, newItems); } if (item.isDir) { entries.delete(stripPrefix(`${parent}/${item.name}`, "/")); } return entries; }, new Map(entriesByParent)); }); } /** @type {(parent: string, items: readonly FileListItem[]) => Promise<void>} */ async function delDirItems(parent, items) { await items.reduce(async (resP, item) => { await resP; if (item.isDir) { const dir = stripPrefix(`${parent}/${item.name}`, "/"); const fileListDir = await self.readDir(`${self.rootPath}/${dir}`); if (fileListDir.items.length > 0) { return await delDirItems(dir, fileListDir.items); } deleteFromState(parent, [item]); } }, Promise.resolve()); const paths = items.map((item) => { const name = item.isDir ? `${item.name}/` : item.name; return stripPrefix(`${parent}/${name}`, "/"); }); const subProcessP = SubProcess.wrap( SubProcess.spawn("zip", ["-qd", self.zipPath, ...paths], { windowsHide: true, }), ); deleteFromState(parent, items); const s = await subProcessP; s.stdout.readable.destroy(); const error = await s.exitP; if (error) { throw error; } } const parentPath = stripPrefix(stripPrefix(parent, self.rootPath), "/"); return delDirItems(parentPath, items); } /** * @param {string} zipPath * @param {string} filePath * @returns {Promise<SubProcess>} */ extract(zipPath, filePath) { return SubProcess.wrap( SubProcess.spawn("unzip", ["-p", zipPath, filePath], { windowsHide: true, }), ); } /** * @param {string} zipFile * @param {string} parent * @param {Set<string>} items * @param {() => void} onNextItem * @returns {Promise<void>} */ static async addToZip(zipFile, parent, items, onNextItem) { const subProcess = await SubProcess.wrap( SubProcess.spawn("zip", ["-r", zipFile, ...items], { cwd: parent, windowsHide: true, }), ); await subProcess.stdout.readAllLines((line) => { if (line.includes("adding: ")) { onNextItem(); } }); const error = await subProcess.exitP; if (error && error.exitCode !== 0) { throw error; } } /** * @param {string} zipPath * @returns {Promise<Map<string, readonly FileListItem[]>>} */ static async readZip(zipPath) { /** @type {(reader: StreamReader, result: Buffer[]) => Promise<readonly Buffer[]>} */ async function loop(reader, result) { const content = await reader.readNextBytes(64 * 1024); if (content) { result.push(content); return loop(reader, result); } return result; } const subProcess = await SubProcess.wrap( SubProcess.spawn("unzip", ["-ZT", zipPath], { windowsHide: true, }), ); const chunks = await loop(subProcess.stdout, []); const output = Buffer.concat(chunks).toString(); const error = await subProcess.exitP; if (error && (error.exitCode !== 1 || !output.includes("Empty zipfile."))) { throw error; } return ZipEntry.groupByParent(ZipEntry.fromUnzipCommand(output)); } /** * @type {(zipPath: string, rootPath: string, entriesByParentP: Promise<Map<string, readonly FileListItem[]>>) => ZipApi} */ static create = (zipPath, rootPath, entriesByParentP) => { return new ZipApi(zipPath, rootPath, entriesByParentP); }; } export default ZipApi;