Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/interfaces/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@ export interface IFileSystem {
* @param cwd default `process.cwd()`
*/
zip(glob: string | string[], cwd?: string): Promise<Buffer>

/**
* @param cwd default `process.cwd()`
*/
zipGenerator(glob: string | string[], cwd?: string): AsyncGenerator<Buffer, void, unknown>
}
37 changes: 23 additions & 14 deletions src/structures/filesystem/fs.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { exec } from "child_process";
import { spawn } from "child_process";
import { on } from "events";
import { existsSync, type Dirent } from "fs";
import { readdir, readFile, writeFile } from "fs/promises";
import { glob } from "glob";
import { type } from "os";
import { join, relative } from "path";
import type Core from "../../core";
import { type FileSystemReadDirWithFileTypesOptions, type IFileSystem } from "../../interfaces/filesystem";
import { MAX_STRING_LENGTH, MINUTE_IN_MILLISECONDS } from "../../utils/constants";
import { MINUTE_IN_MILLISECONDS } from "../../utils/constants";
import Ignore from "./ignore";

export default class FileSystem implements IFileSystem {
Expand Down Expand Up @@ -57,23 +58,31 @@ export default class FileSystem implements IFileSystem {
}

async zip(glob: string | string[], cwd: string = this.core.workspaceFolder) {
return Buffer.concat(await Array.fromAsync(this.zipGenerator(glob, cwd)));
}

zipGenerator(glob: string | string[], cwd?: string): AsyncGenerator<Buffer>
async* zipGenerator(glob: string | string[], cwd: string = this.core.workspaceFolder) {
if (Array.isArray(glob)) glob = glob.join(" ");

const encoding = "base64";
const encoding = "buffer";
const zipCommand = "discloud zip";

const response = await new Promise<string>(function (resolve, reject) {
exec(`${zipCommand} -e=${encoding} -g=${glob || "**"}`, {
cwd,
maxBuffer: MAX_STRING_LENGTH,
timeout: MINUTE_IN_MILLISECONDS,
}, function (error, stdout, _stderr) {
if (error) return reject(error);
const parts = stdout.split("\n");
resolve(parts[parts[0].includes(zipCommand) ? 1 : 0]);
});
const child = spawn(zipCommand, ["-e", encoding, "-g", glob], {
cwd,
shell: true,
stdio: "pipe",
timeout: MINUTE_IN_MILLISECONDS,
});

return Buffer.from(response, encoding);
let notSkippedFirstLine = true;
for await (const [chunk] of on(child.stdout, "data", { close: ["end"] })) {
if (notSkippedFirstLine) {
notSkippedFirstLine = false;
if (`${chunk}`.includes(zipCommand)) continue;
}

yield chunk;
}
}
}
14 changes: 11 additions & 3 deletions src/structures/filesystem/zip.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import AdmZip from "adm-zip";
import { readFile, stat } from "fs/promises";
import { join } from "path";
import { setTimeout as sleep } from "timers/promises";
import { type IZip } from "../../interfaces/zip";

export default class Zip implements IZip {
Expand All @@ -20,9 +20,17 @@ export default class Zip implements IZip {
for (let i = 0; i < files.length; i++) {
const file = files[i];

this.zip.addLocalFile(join(cwd, file), void 0, file);
const filePath = join(cwd, file);

await sleep();
let stats;
try { stats = await stat(filePath); }
catch { continue; }

if (!stats.isFile()) continue;

const buffer = await readFile(filePath);

this.zip.addFile(file, buffer);
}
}

Expand Down
63 changes: 38 additions & 25 deletions test/commands/zip.test.mjs
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
import AdmZip from "adm-zip";
import { exec } from "child_process";
import { spawn } from "child_process";
import { on } from "events";
import { existsSync, rmSync } from "fs";
import { stat } from "fs/promises";
import { suite, test } from "node:test";

suite("Testing zip command", async () => {
await test("Getting a empty zip base64", async (t) => {
const encoding = "base64";
await test("Getting a empty zip buffer", async (t) => {
const encoding = "buffer";
const filePath = `__not_expected_files__${Math.random()}`;
const expectedEntryCount = 0;

/** @type {string} */
const responseBase64 = await executeZipCommand(filePath, { encoding });

const buffer = Buffer.from(responseBase64, encoding);
const buffer = await executeZipCommand(filePath, { encoding });

const zipper = new AdmZip(buffer);

t.assert.strictEqual(zipper.getEntryCount(), expectedEntryCount);
});

await test("Getting a zip base64 with a file", async (t) => {
const encoding = "base64";
await test("Getting a zip buffer with a file", async (t) => {
const encoding = "buffer";
const filePath = "test/mock/zip_tester.txt";
const expectedEntryCount = 1;
const expectedEntryName = filePath;
const expectedContent = "# DO NOT REMOVE";

/** @type {string} */
const responseBase64 = await executeZipCommand(filePath, { encoding });

const buffer = Buffer.from(responseBase64, encoding);
const buffer = await executeZipCommand(filePath, { encoding });

const zipper = new AdmZip(buffer);

Expand Down Expand Up @@ -89,7 +84,7 @@ suite("Testing zip command", async () => {
* @overload
* @param {string} [glob]
* @param {OptionsWithEncoding} options
* @returns {Promise<string>}
* @returns {Promise<Buffer>}
* @typedef OptionsWithEncoding
* @prop {BufferEncoding} encoding
* @prop {number} [maxBuffer]
Expand All @@ -101,25 +96,43 @@ suite("Testing zip command", async () => {
* @typedef OptionsWithOut
* @prop {string} out
*/
function executeZipCommand(glob, options) {
async function executeZipCommand(glob, options) {
return Buffer.concat(await Array.fromAsync(zipGenerator(glob, options)));
}

/**
* @param {string} [glob]
* @param {OptionsWithEncoding} [options]
* @returns {AsyncGenerator<Buffer>}
*
* @typedef OptionsWithEncoding
* @prop {BufferEncoding | "buffer"} encoding
*/
async function* zipGenerator(glob, options) {
const MINUTE_IN_MILLISECONDS = 60_000;

const zipCommand = "discloud zip";
const localBinCommand = "bin/" + zipCommand;

const command = [
"node",
const child = spawn("node", [
localBinCommand,
...options?.encoding ? ["--encoding", options.encoding] : [],
...options?.out ? ["--out", options.out] : [],
glob,
].join(" ");

return new Promise(function (resolve, reject) {
exec(command, { timeout: MINUTE_IN_MILLISECONDS }, function (error, stdout, _stderr) {
if (error) return reject(error);
const parts = stdout.split("\n");
resolve(parts[parts[0].includes(zipCommand) ? 1 : 0]);
});
], {
shell: true,
stdio: "pipe",
timeout: MINUTE_IN_MILLISECONDS,
});

let notSkippedFirstLine = true;
for await (const [chunk] of on(child.stdout, "data", { close: ["end"] })) {
if (notSkippedFirstLine) {
notSkippedFirstLine = false;
if (`${chunk}`.includes(zipCommand)) continue;
}

yield chunk;
}
}
});
Loading