Skip to content

Commit

Permalink
feat: support compression and decompression
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Dec 9, 2023
1 parent 14289aa commit aac7d21
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 21 deletions.
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@

Tiny and fast [Tar](<https://en.wikipedia.org/wiki/Tar_(computing)>) utils for any JavaScript runtime!

🌳 Tiny (less than 1.5KB minified + gzipped) and tree-shakable
🌳 Tiny (less than 2KB minified + gzipped) and tree-shakable

✨ Written with modern TypeScript and ESM format

✅ Works in any JavaScript runtime Node.js (18+), Bun, Deno, Browsers, and Edge Workers

🌐 Web Standard Compatible

🗜️ Built-in compression and decompression support

## Installation

Install package:
Expand All @@ -37,10 +39,16 @@ Import:

```js
// ESM
import { parseTar, createTar } from "mircrotar";
import {
createTar,
createTarGzip,
createTarGzipStream,
parseTar,
parseTarGzip,
} from "mircrotar";

// CommonJS
const { parseTar, createTar } = require("mircrotar");
const { createTar } = require("mircrotar");
```

## Creating a tar archive
Expand Down Expand Up @@ -81,6 +89,18 @@ const data = createTar(
// Data is a Uint8Array view you can send or write to a file
```

### Compression

You can optionaly use `createTarGzip` or `createTarGzipStream` to create a compressed tar data stream (returned value is a [`Promise<Uint8Array>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) or [`RedableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) piped to [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream))

```js
import { createTarGzip, createTarGzipStream } from "mircrotar";

createTarGzip([]); // Promise<Uint8Array>

createTarGzipStream([]); // RedableStream
```

## Parsing a tar archive

Easily parse a tar archive using `parseTar` utility.
Expand Down Expand Up @@ -118,6 +138,16 @@ const files = parseTar(data);

Parsed files array has two additional properties: `size` file size and `text`, a lazy getter that decodes `data` view as a string.

### Decompression

If input is compressed, you can use `parseTarGzip` utility instead to parse it (it used [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) internally and return a `Promise<Uint8Array>` value)

```js
import { parseTarGzip } from "mircrotar";

parseTarGzip(data); // Promise<Uint8Array>
```

## Development

- Clone this repository
Expand Down
28 changes: 17 additions & 11 deletions playground/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { execSync } from "node:child_process";
import { parseTar, createTar } from "../src";
import { createTarGzip, parseTarGzip } from "../src";

const data = createTar(
[
{ name: "README.md", data: "# Hello World!" },
{ name: "test", attrs: { mode: "777", mtime: 0 } },
{ name: "src/index.js", data: "console.log('wow!')" },
],
{ attrs: { user: "js", group: "js" } },
);
async function main() {
const data = await createTarGzip(
[
{ name: "README.md", data: "# Hello World!" },
{ name: "test", attrs: { mode: "777", mtime: 0 } },
{ name: "src/index.js", data: "console.log('wow!')" },
],
{ attrs: { user: "js", group: "js" } },
);

console.log(execSync("tar -tvf-", { input: data }).toString());
console.log("Len:", data.length);

console.log(parseTar(data));
console.log(execSync("tar -tvzf-", { input: data }).toString());

console.log(await parseTarGzip(data));
}

main();
27 changes: 26 additions & 1 deletion src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ export interface CreateTarOptions {
attrs?: TarFileAttrs;
}

export type TarFileInput = TarFileItem<string | Uint8Array | ArrayBuffer>;

export function createTar(
files: TarFileItem<string | Uint8Array | ArrayBuffer>[],
files: TarFileInput[],
opts: CreateTarOptions = {},
): Uint8Array {
// Normalize file data in order to allow calculating final size
Expand Down Expand Up @@ -112,6 +114,29 @@ export function createTar(
return new Uint8Array(buffer);
}

export function createTarGzipStream(
files: TarFileInput[],
opts: CreateTarOptions & { compression?: CompressionFormat } = {},
): ReadableStream {
const buffer = createTar(files, opts);
return new ReadableStream({
start(controller) {
controller.enqueue(buffer);
controller.close();
},
}).pipeThrough(new CompressionStream(opts.compression ?? "gzip"));
}

export async function createTarGzip(
files: TarFileInput[],
opts: CreateTarOptions & { compression?: CompressionFormat } = {},
): Promise<Uint8Array> {
const data = await new Response(createTarGzipStream(files, opts))
.arrayBuffer()
.then((buffer) => new Uint8Array(buffer));
return data;
}

function _writeString(
buffer: ArrayBuffer,
str: string,
Expand Down
16 changes: 16 additions & 0 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ export function parseTar(data: ArrayBuffer | Uint8Array): TarFileItem[] {
return files;
}

export async function parseTarGzip(
data: ArrayBuffer | Uint8Array,
opts: { compression?: CompressionFormat } = {},
): Promise<TarFileItem[]> {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(data));
controller.close();
},
}).pipeThrough(new DecompressionStream(opts.compression ?? "gzip"));

const decompressedData = await new Response(stream).arrayBuffer();

return parseTar(decompressedData);
}

function _readString(buffer: ArrayBuffer, offset: number, size: number) {
const view = new Uint8Array(buffer, offset, size);
const i = view.indexOf(0);
Expand Down
15 changes: 9 additions & 6 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execSync } from "node:child_process";
import { expect, it, describe } from "vitest";
import { createTar, parseTar, TarFileItem } from "../src";
import { createTarGzip, parseTarGzip, TarFileItem } from "../src";

const mtime = 1_700_000_000_000;

Expand All @@ -11,8 +11,8 @@ const fixture: TarFileItem<any>[] = [
];

describe("mircrotar", () => {
it("createTar", () => {
const data = createTar(fixture);
it("createTar", async () => {
const data = await createTarGzip(fixture);
expect(data).toBeInstanceOf(Uint8Array);
expect(execSync("tar -tvf-", { input: data }).toString())
.toMatchInlineSnapshot(`
Expand All @@ -23,9 +23,12 @@ describe("mircrotar", () => {
`);
});

it("parseTar", () => {
const data = createTar(fixture);
const files = parseTar(data).map((f) => ({ ...f, data: "<hidden>" }));
it("parseTar", async () => {
const data = await createTarGzip(fixture);
const files = (await parseTarGzip(data)).map((f) => ({
...f,
data: "<hidden>",
}));
expect(files).toMatchInlineSnapshot(`
[
{
Expand Down

0 comments on commit aac7d21

Please sign in to comment.