@tmlmobilidade/go-utils-exec 20260903.2201.22 → 20260904.1431.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 zipFilePath The path to the `.zip` file.
8
+ * @returns The SHA-256 hash of the `.zip` file contents.
9
+ */
10
+ export declare function calculateZipFileHash(filePath: string): Promise<string>;
@@ -0,0 +1,61 @@
1
+ /* * */
2
+ import { createHash } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import unzipper from 'unzipper';
6
+ import { getDirectoryFiles } from './get-directory-files.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 zipFilePath The path to the `.zip` file.
14
+ * @returns The SHA-256 hash of the `.zip` file contents.
15
+ */
16
+ export async function calculateZipFileHash(filePath) {
17
+ //
18
+ //
19
+ // Initialize a new temporary directory and extract the ZIP file into it.
20
+ const temporaryDirectory = fs.mkdtempDisposableSync('calculate-zip-file-hash');
21
+ await fs
22
+ .createReadStream(filePath)
23
+ .pipe(unzipper.Extract({ path: temporaryDirectory.path }))
24
+ .promise();
25
+ //
26
+ // Find all extracted files.
27
+ const sortedExtractedFilePaths = await getDirectoryFiles(temporaryDirectory.path);
28
+ //
29
+ // Initialize the list of files to hash.
30
+ const foundFiles = [];
31
+ for (const absolutePath of sortedExtractedFilePaths) {
32
+ // Create a new hash for the file
33
+ const hash = createHash('sha256');
34
+ // Open a read stream for the file.
35
+ const stream = fs.createReadStream(absolutePath);
36
+ // Stream the file contents and calculate the SHA-256 hash.
37
+ for await (const chunk of stream) {
38
+ hash.update(chunk);
39
+ }
40
+ // Add the file to the list of files to hash.
41
+ const relativeFilePath = path.relative(temporaryDirectory.path, absolutePath);
42
+ foundFiles.push({ hash: hash.digest('hex'), path: relativeFilePath });
43
+ }
44
+ //
45
+ // Make the result independent of the order of entries in the zip file.
46
+ foundFiles.sort((a, b) => a.path.localeCompare(b.path));
47
+ //
48
+ // Clean up the temporary directory.
49
+ temporaryDirectory.remove();
50
+ //
51
+ // Calculate the final hash by concatenating the sorted list
52
+ // of filenames and individual file hashes.
53
+ const finalHash = createHash('sha256');
54
+ for (const file of foundFiles) {
55
+ finalHash.update(file.path);
56
+ finalHash.update('\0');
57
+ finalHash.update(file.hash);
58
+ finalHash.update('\0');
59
+ }
60
+ return finalHash.digest('hex');
61
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Gets the files in a directory and its subdirectories recursively.
3
+ * @param dirPath The path to the directory to get the files from.
4
+ * @returns A promise that resolves to an array of file paths.
5
+ */
6
+ export declare function getDirectoryFiles(dirPath: string): Promise<string[]>;
@@ -0,0 +1,37 @@
1
+ /* * */
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ /**
5
+ * Gets the files in a directory and its subdirectories recursively.
6
+ * @param dirPath The path to the directory to get the files from.
7
+ * @returns A promise that resolves to an array of file paths.
8
+ */
9
+ export async function getDirectoryFiles(dirPath) {
10
+ //
11
+ //
12
+ // Read the directory contents.
13
+ const dirEntries = await fs.readdir(dirPath, { withFileTypes: true });
14
+ //
15
+ // Initialize the list of files.
16
+ const dirFiles = [];
17
+ for (const entry of dirEntries) {
18
+ // Get the path to the entry.
19
+ const entryPath = path.join(dirPath, entry.name);
20
+ // If the entry is a directory, recursively get
21
+ // the files in the subdirectory.
22
+ if (entry.isDirectory()) {
23
+ const subdirFiles = await getDirectoryFiles(entryPath);
24
+ dirFiles.push(...subdirFiles);
25
+ continue;
26
+ }
27
+ // Add the path to the list of files.
28
+ if (entry.isFile()) {
29
+ dirFiles.push(entryPath);
30
+ }
31
+ }
32
+ //
33
+ // Sort and return the list of files in the directory
34
+ // and its subdirectories.
35
+ const sortedDirFiles = dirFiles.sort((a, b) => a.localeCompare(b));
36
+ return sortedDirFiles;
37
+ }
@@ -0,0 +1,5 @@
1
+ export * from './calculate-zip-hash.js';
2
+ export * from './get-directory-files.js';
3
+ export * from './set-directory-permissions.js';
4
+ export * from './stream-csv-file.js';
5
+ export * from './unzip-file.js';
@@ -0,0 +1,5 @@
1
+ export * from './calculate-zip-hash.js';
2
+ export * from './get-directory-files.js';
3
+ export * from './set-directory-permissions.js';
4
+ export * from './stream-csv-file.js';
5
+ export * from './unzip-file.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Sets the permissions of a directory and its files.
3
+ * @param dirPath The path to the directory to set the permissions of.
4
+ * @param mode The mode to set the permissions of the directory and its files to.
5
+ */
6
+ export declare function setDirectoryPermissions(dirPath: string, mode: number): void;
@@ -0,0 +1,19 @@
1
+ /* * */
2
+ import fs from 'node:fs';
3
+ /**
4
+ * Sets the permissions of a directory and its files.
5
+ * @param dirPath The path to the directory to set the permissions of.
6
+ * @param mode The mode to set the permissions of the directory and its files to.
7
+ */
8
+ export function setDirectoryPermissions(dirPath, mode) {
9
+ const files = fs.readdirSync(dirPath, { withFileTypes: true });
10
+ for (const file of files) {
11
+ const filePath = `${dirPath}/${file.name}`;
12
+ if (file.isDirectory()) {
13
+ setDirectoryPermissions(filePath, mode);
14
+ }
15
+ else {
16
+ fs.chmodSync(filePath, mode);
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Parses a CSV file into a stream and calls a callback function for each row.
3
+ * @param filePath The path to the CSV file to parse.
4
+ * @param rowParser A callback function that will be called for each row.
5
+ * @returns A promise that resolves when the stream is closed.
6
+ */
7
+ export declare function streamCsvFile<T>(filePath: string, rowParser: (rowData: T) => Promise<void>): Promise<void>;
@@ -0,0 +1,25 @@
1
+ /* * */
2
+ import { parse as csvParser } from 'csv-parse';
3
+ import fs from 'fs';
4
+ /**
5
+ * Parses a CSV file into a stream and calls a callback function for each row.
6
+ * @param filePath The path to the CSV file to parse.
7
+ * @param rowParser A callback function that will be called for each row.
8
+ * @returns A promise that resolves when the stream is closed.
9
+ */
10
+ export async function streamCsvFile(filePath, rowParser) {
11
+ const parser = csvParser({
12
+ bom: true,
13
+ cast: value => value === '' ? undefined : value,
14
+ columns: true,
15
+ record_delimiter: ['\n', '\r', '\r\n'],
16
+ skip_empty_lines: true,
17
+ skipRecordsWithEmptyValues: true,
18
+ trim: true,
19
+ });
20
+ const fileStream = fs.createReadStream(filePath);
21
+ const stream = fileStream.pipe(parser);
22
+ for await (const rowData of stream) {
23
+ await rowParser(rowData);
24
+ }
25
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Unzips a zip file into a directory in stream mode, avoiding memory issues.
3
+ * This also calls the `setDirectoryPermissions` function to override
4
+ * the permissions of the unzipped files, if any were preserved in the zip file.
5
+ * @param zipFilePath The path to the zip file to unzip.
6
+ * @param outputDir The path to the directory to unzip the file to.
7
+ * @param dirPermissionsMode The mode to set the permissions of the unzipped directory to. 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?: number): Promise<void>;
@@ -0,0 +1,20 @@
1
+ /* * */
2
+ import fs from 'node:fs';
3
+ import unzipper from 'unzipper';
4
+ import { setDirectoryPermissions } from './set-directory-permissions.js';
5
+ /**
6
+ * Unzips a zip file into a directory in stream mode, avoiding memory issues.
7
+ * This also calls the `setDirectoryPermissions` function to override
8
+ * the permissions of the unzipped files, if any were preserved in the zip file.
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. Defaults to `0o666` (read and write for owner, group and others).
12
+ * @returns A promise that resolves when the file is unzipped.
13
+ */
14
+ export async function unzipFile(zipFilePath, outputDir, dirPermissionsMode = 0o666) {
15
+ await fs
16
+ .createReadStream(zipFilePath)
17
+ .pipe(unzipper.Extract({ path: outputDir }))
18
+ .promise();
19
+ setDirectoryPermissions(outputDir, dirPermissionsMode);
20
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './batch/index.js';
2
+ export * from './files/index.js';
2
3
  export * from './lifecycle/index.js';
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './batch/index.js';
2
+ export * from './files/index.js';
2
3
  export * from './lifecycle/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/go-utils-exec",
3
- "version": "20260903.2201.22",
3
+ "version": "20260904.1431.46",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"
@@ -39,11 +39,14 @@
39
39
  "@tmlmobilidade/go-types-geo": "*",
40
40
  "@tmlmobilidade/go-types-shared": "*",
41
41
  "@tmlmobilidade/go-utils-dates": "*",
42
- "@tmlmobilidade/timer": "*"
42
+ "@tmlmobilidade/timer": "*",
43
+ "csv-parse": "7.0.2",
44
+ "unzipper": "0.12.5"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@tmlmobilidade/go-utils-tsconfig": "*",
46
48
  "@types/node": "26.4.0",
49
+ "@types/unzipper": "0.10.11",
47
50
  "resolve-tspaths": "0.8.23",
48
51
  "tsc-watch": "7.2.1",
49
52
  "typescript": "6.0.3"