Skip to content

Commit

Permalink
feat: checksum directory (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahradelahi committed Apr 19, 2024
1 parent 6cc9440 commit 5f9c0ee
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 36 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ npm i -g @litehex/node-checksum
Usage: checksum [options] [command] [file...]
Arguments:
file file to hash (default: [])
file file or directory to hash (default: [])
Options:
-a, --algorithm <algorithm> hash algorithm (default: "sha256")
-C, --context <context> context to hash
-r, --recursive hash directories recursively (default: false)
-x, --exclude <exclude> exclude patterns
--cwd <cwd> current working directory (default: ".")
-h, --help display help for command
Expand All @@ -38,6 +40,14 @@ $ checksum package.json README.md --algorithm md5
> 85f96e23f8adb7e1b1faf6f6341fe768 package.json
> ddc66b29b08d70b9accaa797d98ccdcc README.md

$ checksum --exclude "**/{.git,node_modules}/**" .
> 62bbd7fec4e27c35cbf9a5058913c1e76ebe5d9dcb7ae6644f79c4cec1c6821b .gitignore
> 88f36901e0a2735c58156120e9887ed0456918c8ffbf3a60eca0a9f221faa6ab .mocharc.json
> 5f0fd6fa76c54ca557bdecdb6de94cec59745b4cd90469d79210c1554b679a18 .prettierignore
> ...

$ checksum --recursive src > checksum.txt

$ checksum -C "Hello World" --algorithm md5
> b10a8db164e0754105b7a99be72e3fe5

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
},
"dependencies": {
"commander": "^12.0.0",
"crc-32": "^1.2.2"
"crc-32": "^1.2.2",
"fast-glob": "^3.3.2"
},
"keywords": [
"checksum",
Expand Down
21 changes: 3 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 37 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
#!/usr/bin/env node

import { verify } from '@/commands';
import { hash, hashFile } from '@/lib';
import { hash, hashDirectory, hashFile } from '@/lib';
import { toBuffer } from '@/utils/buffer';
import { isDirectory } from '@/utils/fs-extra';
import { handleError } from '@/utils/handle-error';
import { Command } from 'commander';
import { resolve } from 'node:path';
import { relative, resolve } from 'node:path';

async function main() {
const program = new Command();

program.addCommand(verify);

program
.argument('[file...]', 'file to hash', [])
.argument('[file...]', 'file or directory to hash', [])
.option('-a, --algorithm <algorithm>', 'hash algorithm', 'sha256')
.option('-C, --context <context>', 'context to hash')
.option('-r, --recursive', 'hash directories recursively', false)
.option('-x, --exclude <exclude>', 'exclude patterns')
.option('--cwd <cwd>', 'current working directory', process.cwd())
.action(async (file, options) => {
const { algorithm, context } = options;
Expand All @@ -30,8 +34,35 @@ async function main() {

if (file.length > 0) {
for (const filePath of file) {
const hashed = await hashFile(algorithm, resolve(options.cwd, filePath));
console.log(`${hashed}${file.length > 1 ? ` ${filePath}` : ''}`);
const { default: fg } = await import('fast-glob');

const excluded = await fg.glob(options.exclude ?? [], { onlyFiles: false });

async function logHashed(filePath: string, named: boolean) {
const hashed = await hashFile(algorithm, resolve(options.cwd, filePath));
console.log(`${hashed}${named ? ` ${relative(options.cwd, filePath)}` : ''}`);
}

function isExcluded(filePath: string) {
return excluded.some((glob) => glob === filePath);
}

if (await isDirectory(filePath)) {
const hashed = await hashDirectory(algorithm, filePath, {
exclude: options.exclude,
recursive: options.recursive,
});
for (const { filename } of hashed) {
await logHashed(filename, true);
}
continue;
}

if (isExcluded(filePath)) {
continue;
}

await logHashed(filePath, file.length > 1);
}

process.exitCode = 0;
Expand All @@ -47,7 +78,7 @@ async function main() {

const chunks: Buffer[] = [];
for await (const chunk of stdin) {
chunks.push(chunk);
chunks.push(toBuffer(chunk));
}

const data = Buffer.concat(chunks);
Expand Down
59 changes: 49 additions & 10 deletions src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { BufferLike, HashAlgorithm } from '@/typings';
import type { BufferLike, HashAlgorithm, HashedFile } from '@/typings';
import { toBuffer } from '@/utils/buffer';
import { fsAccess } from '@/utils/fs-extra';
import { fsAccess, readDirectory } from '@/utils/fs-extra';
import crc32 from 'crc-32';

import { type BinaryToTextEncoding, createHash } from 'node:crypto';
import { promises } from 'node:fs';
import { resolve } from 'node:path';

export function hash<Algorithm extends string = HashAlgorithm>(
algorithm: Algorithm,
Expand All @@ -28,14 +29,6 @@ export function hash<Algorithm extends string = HashAlgorithm>(

// --------------

export function sha256(data: Buffer | string): string {
return hash('sha256', data);
}

export function md5(data: Buffer | string): string {
return hash('md5', data);
}

export async function hashFile<Algorithm extends string = HashAlgorithm>(
algorithm: Algorithm,
filePath: string,
Expand All @@ -53,6 +46,52 @@ export async function hashFile<Algorithm extends string = HashAlgorithm>(
return hash(algorithm, content);
}

export async function hashDirectory<Algorithm extends string = HashAlgorithm>(
algorithm: Algorithm,
directory: string,
options: {
recursive: boolean;
exclude?: string[];
} = { recursive: false },
): Promise<HashedFile[]> {
const { exclude = [] } = options;

const { default: fg } = await import('fast-glob');
const excluded = await fg.glob(exclude, { onlyFiles: false });

function isExcluded(filePath: string) {
return excluded.some((path) => path === filePath);
}

const files = (await readDirectory(directory, options.recursive || false))
// Files only
.filter((adf) => !adf.directory && !adf.symlink)
// Filter out excluded files
.filter(({ path }) => !isExcluded(path));

const results: HashedFile[] = [];

for (const { path } of files) {
const hashed = await hashFile(algorithm, path);
results.push({
filename: resolve(directory, path),
hash: hashed,
});
}

return results;
}

// --------------

export function sha256(data: BufferLike): string {
return hash('sha256', data);
}

export function md5(data: BufferLike): string {
return hash('md5', data);
}

export async function sha256File(filePath: string): Promise<string> {
return hashFile('sha256', filePath);
}
Expand Down
5 changes: 5 additions & 0 deletions src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export type HashAlgorithm =
| 'crc32';

export type BufferLike = ArrayBuffer | Buffer | string;

export interface HashedFile {
filename: string;
hash: string;
}
Loading

0 comments on commit 5f9c0ee

Please sign in to comment.