Skip to content

Commit f10e844

Browse files
committed
feat: added recursive directory walking
1 parent f364d20 commit f10e844

File tree

7 files changed

+119
-25
lines changed

7 files changed

+119
-25
lines changed

package-lock.json

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src"
3636
},
3737
"dependencies": {
38+
"@matrixai/errors": "^1.1.7",
3839
"@matrixai/logger": "^4.0.3",
3940
"threads": "^1.7.0",
4041
"uuid": "^11.0.5"

src/Generator.ts

+52-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { TarType } from './types';
1+
import type { TarType, DirectoryContent } from './types';
22
import fs from 'fs';
3+
import path from 'path';
4+
import * as errors from './errors';
35

46
/**
57
* The size for each tar block. This is usually 512 bytes.
@@ -15,8 +17,13 @@ function computeChecksum(header: Buffer): number {
1517
}
1618

1719
function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer {
18-
const size = type === '0' ? stat.size : 0;
20+
if (filePath.length < 1 || filePath.length > 255) {
21+
throw new errors.ErrorVirtualTarInvalidFileName(
22+
'The file name must be longer than 1 character and shorter than 255 characters',
23+
);
24+
}
1925

26+
const size = type === '0' ? stat.size : 0;
2027
const header = Buffer.alloc(BLOCK_SIZE, 0);
2128

2229
// The TAR headers follow this structure
@@ -41,10 +48,10 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer {
4148
// 500 12 '\0' (unused)
4249

4350
// 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
51+
header.write(filePath.slice(0, 99).padEnd(100, '\0'), 0, 100, 'utf8');
52+
header.write(stat.mode.toString(8).padStart(7, '0') + '\0', 100, 12, 'ascii');
53+
header.write(stat.uid.toString(8).padStart(7, '0') + '\0', 108, 12, 'ascii');
54+
header.write(stat.gid.toString(8).padStart(7, '0') + '\0', 116, 12, 'ascii');
4855
header.write(size.toString(8).padStart(7, '0') + '\0', 124, 12, 'ascii');
4956
// Mtime will be null
5057
header.write(' ', 148, 8, 'ascii'); // Placeholder for checksum
@@ -56,7 +63,7 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer {
5663
// Owner group name will be null
5764
// Device major will be null
5865
// Device minor will be null
59-
// Extended file name will be null
66+
header.write(filePath.slice(100).padEnd(155, '\0'), 345, 155, 'utf8');
6067

6168
// Updating with the new checksum
6269
const checksum = computeChecksum(header);
@@ -86,18 +93,48 @@ async function* readFile(filePath: string): AsyncGenerator<Buffer, void, void> {
8693
}
8794
}
8895

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
96+
/**
97+
* Traverse a directory recursively and yield file entries.
98+
*/
99+
async function* walkDirectory(
100+
baseDir: string,
101+
relativePath: string = '',
102+
): AsyncGenerator<DirectoryContent> {
103+
const entries = await fs.promises.readdir(path.join(baseDir, relativePath));
104+
105+
// Sort the entries lexicographically
106+
for (const entry of entries.sort()) {
107+
const fullPath = path.join(baseDir, relativePath, entry);
108+
const stat = await fs.promises.stat(fullPath);
109+
const tarPath = path.join(relativePath, entry);
110+
111+
if (stat.isDirectory()) {
112+
yield { path: tarPath + '/', stat: stat, type: '5' };
113+
yield* walkDirectory(baseDir, path.join(relativePath, entry));
114+
} else if (stat.isFile()) {
115+
yield { path: tarPath, stat: stat, type: '0' };
116+
}
117+
}
118+
}
119+
120+
async function* createTar(baseDir: string): AsyncGenerator<Buffer, void, void> {
121+
for await (const entry of walkDirectory(baseDir)) {
122+
// Create header
123+
yield createHeader(entry.path, entry.stat, entry.type);
124+
125+
if (entry.type === '0') {
126+
// Get file contents
127+
yield* readFile(path.join(baseDir, entry.path));
128+
}
129+
}
130+
131+
// End-of-archive marker - two 512-byte null blocks
97132
yield Buffer.alloc(BLOCK_SIZE, 0);
98133
yield Buffer.alloc(BLOCK_SIZE, 0);
99134
}
100135

136+
// NOTE: probably need to remove this, idk
137+
// this is a library and should only worry about tarring itself and not writing to fs
101138
async function writeArchive(inputFile: string, outputFile: string) {
102139
const fileHandle = await fs.promises.open(outputFile, 'w+');
103140
for await (const chunk of createTar(inputFile)) {

src/errors.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { AbstractError } from '@matrixai/errors';
2+
3+
class ErrorVirtualTar<T> extends AbstractError<T> {
4+
static description = 'VirtualTar errors';
5+
}
6+
7+
class ErrorVirtualTarUndefinedBehaviour<T> extends ErrorVirtualTar<T> {
8+
static description = 'You should never see this error';
9+
}
10+
11+
class ErrorVirtualTarInvalidFileName<T> extends ErrorVirtualTar<T> {
12+
static description = 'The provided file name is invalid';
13+
}
14+
15+
export {
16+
ErrorVirtualTar,
17+
ErrorVirtualTarUndefinedBehaviour,
18+
ErrorVirtualTarInvalidFileName,
19+
};

src/types.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1-
// 0 = FILE
2-
// 5 = DIRECTORY
3-
type TarType = '0' | '5';
1+
import type { Stats } from 'fs';
42

5-
export { TarType };
3+
// FIXME: Using 0s and 5s for files and directories isn't a good way to handle
4+
// this. I need to make it simpler so that I can assign and test using strings.
5+
// A potential solution is enums, but they don't work with types, so it's a bit
6+
// weird.
7+
type TarFile = '0';
8+
9+
type TarDirectory = '5';
10+
11+
type TarType = TarFile | TarDirectory;
12+
13+
type DirectoryContent = {
14+
path: string;
15+
stat: Stats;
16+
type: TarType;
17+
};
18+
19+
export type { TarFile, TarDirectory, TarType, DirectoryContent };

src/utils.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as errors from './errors';
2+
3+
function never(message: string): never {
4+
throw new errors.ErrorVirtualTarUndefinedBehaviour(message);
5+
}
6+
7+
export { never };

tests/index.test.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { writeArchive } from '@/Generator';
22

33
describe('index', () => {
4-
test('test', async () => {
4+
test.skip('test', async () => {
55
await expect(
6-
writeArchive(
7-
'/home/aryanj/Downloads/tar/FILE.txt',
8-
'/home/aryanj/Downloads/tar/FILE.tar',
9-
),
6+
writeArchive('/home/aryanj/Downloads', '/home/aryanj/archive.tar'),
107
).toResolve();
11-
});
8+
}, 60000);
129
});

0 commit comments

Comments
 (0)