Skip to content

Commit d12445c

Browse files
committed
wip: changing js-virtualtar to use local version to fix import problems, adding some prototype tests to scratch.test.ts to test vtar functionality
1 parent 7cbd2e6 commit d12445c

File tree

3 files changed

+213
-85
lines changed

3 files changed

+213
-85
lines changed

package-lock.json

Lines changed: 36 additions & 82 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
"@matrixai/errors": "^2.1.3",
9090
"@matrixai/events": "^4.0.1",
9191
"@matrixai/id": "^4.0.0",
92-
"@matrixai/js-virtualtar": "^2.0.0",
92+
"@matrixai/js-virtualtar": "../js-virtualtar",
9393
"@matrixai/logger": "^4.0.3",
9494
"@matrixai/mdns": "^2.0.7",
9595
"@matrixai/quic": "^2.0.9",

tests/scratch.test.ts

Lines changed: 176 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,182 @@
1+
import fs from 'fs';
2+
import os from 'os';
3+
import path from 'path';
4+
import {
5+
VirtualTarGenerator,
6+
VirtualTarParser,
7+
types as vtarTypes,
8+
} from '@matrixai/js-virtualtar';
9+
10+
// Default chunk size for reading files from the filesystem.
11+
const DEFAULT_CHUNK_SIZE = 64 * 1024;
12+
113
/**
2-
* This is a 'scratch paper' test file for quickly running tests in the CI
14+
* Creates an AsyncGenerator that yields Uint8Array chunks of a tar archive
15+
* containing a single specified file, streamed directly from the file system.
16+
* This is the core function for the "generation" part of the task.
17+
* @param localFilePath The path to the file on the local filesystem.
18+
* @param pathInArchive The desired path (including name) of the file within the tar archive.
19+
* @param chunkSize Optional chunk size for reading the file.
20+
* @returns An AsyncGenerator yielding Uint8Array tar chunks.
21+
*/
22+
async function* streamFileAsTar(
23+
localFilePath: string,
24+
pathInArchive: string,
25+
chunkSize: number = DEFAULT_CHUNK_SIZE,
26+
): AsyncGenerator<Uint8Array, void, void> {
27+
const vtar = new VirtualTarGenerator();
28+
29+
// 1. Get file statistics for the tar header.
30+
let fileStats: fs.Stats;
31+
try {
32+
fileStats = await fs.promises.stat(localFilePath);
33+
if (!fileStats.isFile()) {
34+
throw new Error(`Path is not a file: ${localFilePath}`);
35+
}
36+
} catch (err) {
37+
console.error(`Error getting stats for ${localFilePath}:`, err);
38+
throw err;
39+
}
40+
41+
// 2. Prepare the stats object for virtualtar.
42+
const tarFileStats: vtarTypes.FileStat = {
43+
size: fileStats.size,
44+
mode: fileStats.mode,
45+
mtime: fileStats.mtime,
46+
uid: fileStats.uid,
47+
gid: fileStats.gid,
48+
};
49+
50+
// 3. Create a dedicated async generator to stream the file's content.
51+
async function* fileContentStreamer(): AsyncGenerator<Buffer, void, void> {
52+
let fd: fs.promises.FileHandle | undefined;
53+
try {
54+
fd = await fs.promises.open(localFilePath, 'r');
55+
const buffer = Buffer.alloc(chunkSize);
56+
while (true) {
57+
const { bytesRead } = await fd.read(buffer, 0, chunkSize, null);
58+
if (bytesRead === 0) {
59+
break;
60+
}
61+
yield buffer.subarray(0, bytesRead);
62+
}
63+
} finally {
64+
if (fd) {
65+
await fd.close();
66+
}
67+
}
68+
}
69+
70+
// 4. Add the file entry to the tar generator.
71+
vtar.addFile(
72+
pathInArchive,
73+
tarFileStats,
74+
() => fileContentStreamer(),
75+
);
76+
77+
// 5. Finalize the tar archive.
78+
vtar.finalize();
79+
80+
// 6. Yield all the generated tar chunks.
81+
yield* vtar.yieldChunks();
82+
}
83+
84+
/**
85+
* Parses a tar stream and writes the contents (files and directories)
86+
* to a specified destination on the local filesystem.
87+
* This is the core function for the "parsing" part of the task.
88+
* @param tarStream An AsyncIterable that yields Uint8Array chunks of a tar archive.
89+
* @param destDir The destination directory to extract the contents to.
90+
*/
91+
async function parseTarStreamToFS(
92+
tarStream: AsyncIterable<Uint8Array>,
93+
destDir: string,
94+
): Promise<void> {
95+
console.log(`--- Parsing Tar Stream to Directory: ${destDir} ---`);
96+
97+
const vtarParser = new VirtualTarParser({
98+
// This callback runs when the parser finds a file header.
99+
onFile: async (header, dataStream) => {
100+
console.log(` -> Found file in archive: '${header.path}'`);
101+
const fullDestPath = path.join(destDir, header.path);
102+
103+
// Ensure the directory for the file exists.
104+
await fs.promises.mkdir(path.dirname(fullDestPath), { recursive: true });
105+
106+
// Open a file handle for writing.
107+
let fd: fs.promises.FileHandle | undefined;
108+
try {
109+
fd = await fs.promises.open(fullDestPath, 'w');
110+
// Stream the file's content chunks directly to the file on disk.
111+
for await (const chunk of dataStream()) {
112+
await fd.write(chunk);
113+
}
114+
console.log(` -> Wrote file to: '${fullDestPath}'`);
115+
} finally {
116+
if (fd) {
117+
await fd.close();
118+
}
119+
}
120+
},
121+
// This callback runs when the parser finds a directory header.
122+
onDirectory: async (header) => {
123+
console.log(` -> Found directory in archive: '${header.path}'`);
124+
const fullDestPath = path.join(destDir, header.path);
125+
await fs.promises.mkdir(fullDestPath, { recursive: true });
126+
},
127+
onEnd: () => {
128+
console.log('--- Tar Parsing Finished ---\n');
129+
},
130+
});
131+
132+
// Feed the generated tar chunks from the stream into the parser.
133+
for await (const chunk of tarStream) {
134+
await vtarParser.write(chunk);
135+
}
136+
// Wait for all asynchronous parsing operations (like onFile) to complete.
137+
await vtarParser.settled();
138+
}
139+
140+
141+
/**
142+
* This is a 'scratch paper' test file for quickly running tests in the CI.
3143
*/
4144
describe('scratch', () => {
5-
test('', async () => {
145+
let tempDir: string;
146+
147+
beforeEach(async () => {
148+
tempDir = await fs.promises.mkdtemp(
149+
path.join(os.tmpdir(), 'polykey-tar-test-'),
150+
);
151+
});
152+
153+
afterEach(async () => {
154+
await fs.promises.rm(tempDir, { recursive: true, force: true });
155+
});
156+
157+
test('should stream a file as a tar, then parse it back and verify content', async () => {
158+
// --- SETUP ---
159+
const originalFileName = 'source-file.txt';
160+
const originalFileContent = 'This is a test of streaming a file with virtualtar!';
161+
const localFilePath = path.join(tempDir, originalFileName);
162+
const pathInArchive = 'test/file-in-tar.txt';
163+
await fs.promises.writeFile(localFilePath, originalFileContent);
164+
console.log(`--- Original File Content ---\n'${originalFileContent}'\n`);
165+
166+
// --- GENERATION (STREAMING -> TAR) ---
167+
const tarStreamGenerator = streamFileAsTar(localFilePath, pathInArchive);
168+
169+
// --- PARSING (TAR -> FILE) ---
170+
const extractionDir = path.join(tempDir, 'extracted');
171+
await fs.promises.mkdir(extractionDir);
172+
// Call our new utility function to handle the parsing and file writing.
173+
await parseTarStreamToFS(tarStreamGenerator, extractionDir);
174+
175+
// --- VERIFICATION ---
176+
const extractedFilePath = path.join(extractionDir, pathInArchive);
177+
const extractedFileContent = await fs.promises.readFile(extractedFilePath, 'utf-8');
6178

179+
expect(extractedFileContent).toEqual(originalFileContent);
180+
console.log('✅ Verification successful: Original and parsed content match!');
7181
});
8182
});

0 commit comments

Comments
 (0)