@tmlmobilidade/go-utils-zip 20260914.1727.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Calculates a deterministic SHA-256 hash of the contents of a `.zip` file.
3
+ * The hash is independent of `.zip` file metadata such as file modification timestamps,
4
+ * compression settings, and `.zip` file entry ordering.
5
+ * Files are hashed individually in streaming mode, then the filenames and
6
+ * individual hashes are sorted and combined into the final hash.
7
+ * @param filePath The path to the `.zip` file.
8
+ * @returns The SHA-256 hash of the `.zip` file contents.
9
+ */
10
+ export declare function getZipFileHash(filePath: string): Promise<string>;
@@ -0,0 +1,67 @@
1
+ /* * */
2
+ import { getDirectoryFiles } from '@tmlmobilidade/go-utils-fs';
3
+ import { createHash } from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { unzipFile } from './unzip-file.js';
7
+ /**
8
+ * Calculates a deterministic SHA-256 hash of the contents of a `.zip` file.
9
+ * The hash is independent of `.zip` file metadata such as file modification timestamps,
10
+ * compression settings, and `.zip` file entry ordering.
11
+ * Files are hashed individually in streaming mode, then the filenames and
12
+ * individual hashes are sorted and combined into the final hash.
13
+ * @param filePath The path to the `.zip` file.
14
+ * @returns The SHA-256 hash of the `.zip` file contents.
15
+ */
16
+ export async function getZipFileHash(filePath) {
17
+ //
18
+ //
19
+ // Initialize a new temporary directory
20
+ const temporaryDirectory = fs.mkdtempDisposableSync('get-zip-file-hash-');
21
+ try {
22
+ //
23
+ //
24
+ // Extract the ZIP file into the temporary directory.
25
+ await unzipFile(filePath, temporaryDirectory.path);
26
+ //
27
+ // Find all extracted files.
28
+ const sortedExtractedFilePaths = getDirectoryFiles(temporaryDirectory.path);
29
+ //
30
+ // Initialize the list of files to hash.
31
+ const foundFiles = [];
32
+ for (const absolutePath of sortedExtractedFilePaths) {
33
+ // Create a new hash for the file
34
+ const hash = createHash('sha256');
35
+ // Open a read stream for the file.
36
+ const stream = fs.createReadStream(absolutePath);
37
+ // Stream the file contents and calculate the SHA-256 hash.
38
+ for await (const chunk of stream) {
39
+ hash.update(chunk);
40
+ }
41
+ // Add the file to the list of files to hash.
42
+ const relativeFilePath = path.relative(temporaryDirectory.path, absolutePath);
43
+ foundFiles.push({ hash: hash.digest('hex'), path: relativeFilePath });
44
+ }
45
+ //
46
+ // Make the result independent of the order of entries in the zip file.
47
+ foundFiles.sort((a, b) => a.path.localeCompare(b.path));
48
+ //
49
+ // Clean up the temporary directory.
50
+ temporaryDirectory.remove();
51
+ //
52
+ // Calculate the final hash by concatenating the sorted list
53
+ // of filenames and individual file hashes.
54
+ const finalHash = createHash('sha256');
55
+ for (const file of foundFiles) {
56
+ finalHash.update(file.path);
57
+ finalHash.update('\0');
58
+ finalHash.update(file.hash);
59
+ finalHash.update('\0');
60
+ }
61
+ return finalHash.digest('hex');
62
+ //
63
+ }
64
+ finally {
65
+ temporaryDirectory.remove();
66
+ }
67
+ }
@@ -0,0 +1,3 @@
1
+ export * from './get-zip-file-hash.js';
2
+ export * from './unzip-file.js';
3
+ export * from './zip-directory.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './get-zip-file-hash.js';
2
+ export * from './unzip-file.js';
3
+ export * from './zip-directory.js';
@@ -0,0 +1,10 @@
1
+ import fs from 'node:fs';
2
+ /**
3
+ * Unzips a zip file into a directory using Yauzl.
4
+ * @param zipFilePath The path to the zip file to unzip.
5
+ * @param outputDir The path to the directory to unzip the file to.
6
+ * @param dirPermissionsMode The mode to set the permissions of the unzipped directory to.
7
+ * Defaults to `0o666` (read and write for owner, group and others).
8
+ * @returns A promise that resolves when the file is unzipped.
9
+ */
10
+ export declare function unzipFile(zipFilePath: string, outputDir: string, dirPermissionsMode?: fs.Mode): Promise<void>;
@@ -0,0 +1,44 @@
1
+ /* * */
2
+ import { setDirectoryPermissions } from '@tmlmobilidade/go-utils-fs';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import yauzl from 'yauzl';
7
+ /**
8
+ * Unzips a zip file into a directory using Yauzl.
9
+ * @param zipFilePath The path to the zip file to unzip.
10
+ * @param outputDir The path to the directory to unzip the file to.
11
+ * @param dirPermissionsMode The mode to set the permissions of the unzipped directory to.
12
+ * Defaults to `0o666` (read and write for owner, group and others).
13
+ * @returns A promise that resolves when the file is unzipped.
14
+ */
15
+ export async function unzipFile(zipFilePath, outputDir, dirPermissionsMode = 0o666) {
16
+ //
17
+ //
18
+ // Create the output directory if it doesn't exist
19
+ if (fs.existsSync(outputDir))
20
+ throw new Error(`Output directory ${outputDir} already exists`);
21
+ fs.mkdirSync(outputDir, { recursive: true });
22
+ //
23
+ // Open the zip file
24
+ const zipfile = await yauzl.openPromise(zipFilePath);
25
+ //
26
+ // Iterate over each entry in the zip file
27
+ for await (const entry of zipfile.eachEntry()) {
28
+ //
29
+ //
30
+ // Directory file names end with '/'.
31
+ // Note that entries for directories themselves are optional.
32
+ // An entry's fileName implicitly requires its parent directories to exist.
33
+ if (entry.fileName.endsWith('/'))
34
+ continue;
35
+ //
36
+ // file entry
37
+ const readStream = await zipfile.openReadStreamPromise(entry);
38
+ const filePath = path.join(outputDir, entry.fileName);
39
+ await pipeline(readStream, fs.createWriteStream(filePath));
40
+ }
41
+ //
42
+ // Set the permissions of the output directory
43
+ setDirectoryPermissions(outputDir, dirPermissionsMode);
44
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Zips a directory into a zip file using Yazl.
3
+ * @param inputDir The path to the directory to zip.
4
+ * @param outputZipFilePath The path to the zip file to create.
5
+ * @returns A promise that resolves when the directory is zipped.
6
+ */
7
+ export declare function zipDirectory(inputDir: string, outputZipFilePath: string): Promise<void>;
@@ -0,0 +1,52 @@
1
+ /* * */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { ZipFile } from 'yazl';
5
+ /**
6
+ * Zips a directory into a zip file using Yazl.
7
+ * @param inputDir The path to the directory to zip.
8
+ * @param outputZipFilePath The path to the zip file to create.
9
+ * @returns A promise that resolves when the directory is zipped.
10
+ */
11
+ export async function zipDirectory(inputDir, outputZipFilePath) {
12
+ //
13
+ //
14
+ // Check if the input directory exists
15
+ if (!fs.existsSync(inputDir))
16
+ throw new Error(`Input directory ${inputDir} does not exist`);
17
+ //
18
+ // Setup a new instance of Yazl and include all files in the input directory
19
+ const outputZip = new ZipFile();
20
+ await new Promise((resolve, reject) => {
21
+ try {
22
+ //
23
+ //
24
+ // Read the working directory contents and filter
25
+ // to keep only files. Throw an error if there are no files,
26
+ // as it would produce an invalid zip file.
27
+ const inputDirContents = fs.readdirSync(inputDir, { withFileTypes: true });
28
+ const inputDirFiles = inputDirContents.filter(file => file.isFile());
29
+ if (!inputDirFiles.length)
30
+ throw new Error('No files found in input directory');
31
+ //
32
+ // Add each file to the zip
33
+ for (const inputDirFile of inputDirFiles) {
34
+ const filePath = path.join(inputDir, inputDirFile.name);
35
+ outputZip.addFile(filePath, inputDirFile.name, { compress: true });
36
+ }
37
+ //
38
+ // Setup a write stream to the final zip file
39
+ outputZip.outputStream
40
+ .pipe(fs.createWriteStream(outputZipFilePath))
41
+ .on('close', resolve);
42
+ //
43
+ // Finalize the zip creation, which triggers
44
+ // the piping and writing process.
45
+ outputZip.end();
46
+ //
47
+ }
48
+ catch (error) {
49
+ reject(error);
50
+ }
51
+ });
52
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@tmlmobilidade/go-utils-zip",
3
+ "version": "20260914.1727.46",
4
+ "author": {
5
+ "email": "iso@tmlmobilidade.pt",
6
+ "name": "TML-ISO"
7
+ },
8
+ "license": "AGPL-3.0-or-later",
9
+ "homepage": "https://go.tmlmobilidade.pt",
10
+ "bugs": {
11
+ "url": "https://github.com/tmlmobilidade/go/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/tmlmobilidade/go.git"
16
+ },
17
+ "keywords": [
18
+ "public transit",
19
+ "tml",
20
+ "transportes metropolitanos de lisboa",
21
+ "go"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "scripts": {
33
+ "build": "tsc && resolve-tspaths",
34
+ "lint": "eslint ./src/ && tsc --noEmit",
35
+ "lint:fix": "eslint ./src/ --fix",
36
+ "watch": "tsc-watch --onSuccess 'resolve-tspaths'"
37
+ },
38
+ "dependencies": {
39
+ "@tmlmobilidade/go-utils-fs": "*",
40
+ "yauzl": "3.4.0",
41
+ "yazl": "3.3.1"
42
+ },
43
+ "devDependencies": {
44
+ "@tmlmobilidade/go-utils-tsconfig": "*",
45
+ "@types/node": "26.4.0",
46
+ "@types/yauzl": "3.4.0",
47
+ "@types/yazl": "3.3.1",
48
+ "resolve-tspaths": "0.8.23",
49
+ "tsc-watch": "7.2.1",
50
+ "typescript": "6.0.3"
51
+ }
52
+ }