Skip to content

Commit f364d20

Browse files
committed
feat: added generators for archiving a single file
1 parent 80947ab commit f364d20

File tree

6 files changed

+127
-2
lines changed

6 files changed

+127
-2
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,7 @@ dist
126126
.yarn/build-state.yml
127127
.yarn/install-state.gz
128128
.pnp.*
129+
130+
# editor
131+
.vscode/
132+
.idea/

src/Generator.ts

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { TarType } from './types';
2+
import fs from 'fs';
3+
4+
/**
5+
* The size for each tar block. This is usually 512 bytes.
6+
*/
7+
const BLOCK_SIZE = 512;
8+
9+
function computeChecksum(header: Buffer): number {
10+
let sum = 0;
11+
for (let i = 0; i < BLOCK_SIZE; i++) {
12+
sum += i >= 148 && i < 156 ? 32 : header[i]; // Fill checksum with spaces
13+
}
14+
return sum;
15+
}
16+
17+
function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer {
18+
const size = type === '0' ? stat.size : 0;
19+
20+
const header = Buffer.alloc(BLOCK_SIZE, 0);
21+
22+
// The TAR headers follow this structure
23+
// Start Size Description
24+
// ------------------------------
25+
// 0 100 File name (first 100 bytes)
26+
// 100 8 File permissions (null-padded octal)
27+
// 108 8 Owner UID (null-padded octal)
28+
// 116 8 Owner GID (null-padded octal)
29+
// 124 12 File size (null-padded octal, 0 for directories)
30+
// 136 12 Mtime (null-padded octal)
31+
// 148 8 Checksum (fill with ASCII spaces for computation)
32+
// 156 1 Type flag (0 for file, 5 for directory)
33+
// 157 100 File owner name (null-terminated ASCII/UTF-8)
34+
// 257 6 'ustar\0' (magic string)
35+
// 263 2 '00' (ustar version)
36+
// 265 32 Owner user name (null-terminated ASCII/UTF-8)
37+
// 297 32 Owner group name (null-terminated ASCII/UTF-8)
38+
// 329 8 Device major (unset in this implementation)
39+
// 337 8 Device minor (unset in this implementation)
40+
// 345 155 File name (last 155 bytes, total 255 bytes, null-padded)
41+
// 500 12 '\0' (unused)
42+
43+
// FIXME: Assuming file path is under 100 characters long
44+
header.write(filePath, 0, 100, 'utf8');
45+
// File permissions name will be null
46+
// Owner uid will be null
47+
// Owner gid will be null
48+
header.write(size.toString(8).padStart(7, '0') + '\0', 124, 12, 'ascii');
49+
// Mtime will be null
50+
header.write(' ', 148, 8, 'ascii'); // Placeholder for checksum
51+
header.write(type, 156, 1, 'ascii');
52+
// File owner name will be null
53+
header.write('ustar\0', 257, 'ascii');
54+
header.write('00', 263, 2, 'ascii');
55+
// Owner user name will be null
56+
// Owner group name will be null
57+
// Device major will be null
58+
// Device minor will be null
59+
// Extended file name will be null
60+
61+
// Updating with the new checksum
62+
const checksum = computeChecksum(header);
63+
header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148, 8, 'ascii');
64+
65+
return header;
66+
}
67+
68+
async function* readFile(filePath: string): AsyncGenerator<Buffer, void, void> {
69+
const fileHandle = await fs.promises.open(filePath, 'r');
70+
const buffer = Buffer.alloc(BLOCK_SIZE);
71+
let bytesRead = -1; // Initialisation value
72+
73+
try {
74+
while (bytesRead !== 0) {
75+
buffer.fill(0);
76+
const result = await fileHandle.read(buffer, 0, BLOCK_SIZE, null);
77+
bytesRead = result.bytesRead;
78+
79+
if (bytesRead === 0) break; // EOF reached
80+
if (bytesRead < 512) buffer.fill(0, bytesRead, BLOCK_SIZE);
81+
82+
yield buffer;
83+
}
84+
} finally {
85+
await fileHandle.close();
86+
}
87+
}
88+
89+
// TODO: change path from filepath to a basedir (plus get a fs)
90+
async function* createTar(filePath: string): AsyncGenerator<Buffer, void, void> {
91+
// Create header
92+
const stat = await fs.promises.stat(filePath);
93+
yield createHeader(filePath, stat, '0');
94+
// Get file contents
95+
yield *readFile(filePath);
96+
// End-of-archive marker
97+
yield Buffer.alloc(BLOCK_SIZE, 0);
98+
yield Buffer.alloc(BLOCK_SIZE, 0);
99+
}
100+
101+
async function writeArchive(inputFile: string, outputFile: string) {
102+
const fileHandle = await fs.promises.open(outputFile, 'w+');
103+
for await (const chunk of createTar(inputFile)) {
104+
await fileHandle.write(chunk);
105+
}
106+
await fileHandle.close();
107+
}
108+
109+
export { createHeader, readFile, createTar, writeArchive };

src/Parser.ts

Whitespace-only changes.

src/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// 0 = FILE
2+
// 5 = DIRECTORY
3+
type TarType = '0' | '5';
4+
5+
export { TarType };

src/utils.ts

Whitespace-only changes.

tests/index.test.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import { writeArchive } from '@/Generator';
2+
13
describe('index', () => {
2-
test('test', () => {
3-
expect(true).toBeTruthy();
4+
test('test', async () => {
5+
await expect(
6+
writeArchive(
7+
'/home/aryanj/Downloads/tar/FILE.txt',
8+
'/home/aryanj/Downloads/tar/FILE.tar',
9+
),
10+
).toResolve();
411
});
512
});

0 commit comments

Comments
 (0)