Skip to content

Commit

Permalink
feat: adds areFilesEqual / actOn / getContentHash from toolbelt to core
Browse files Browse the repository at this point in the history
  • Loading branch information
rvwatch committed Jun 30, 2020
1 parent eadeec4 commit 807dc72
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 1 deletion.
66 changes: 66 additions & 0 deletions src/util/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

import { parseJson, parseJsonMap } from '@salesforce/kit';
import { AnyJson, JsonMap, Optional } from '@salesforce/ts-types';
import * as crypto from 'crypto';
import * as fsLib from 'fs';
import * as mkdirpLib from 'mkdirp';
import * as path from 'path';
import { promisify } from 'util';
import { SfdxError } from '../sfdxError';

type PerformFunction = (filePath: string, file?: string, dir?: string) => Promise<void>;

export const fs = {
/**
* The default file system mode to use when creating directories.
Expand Down Expand Up @@ -172,5 +175,68 @@ export const fs = {
} catch (err) {
return false;
}
},

/**
* Recursively act on all files or directories in a directory
*
* @param dir path to directory
* @param perform function to be run on contents of dir
* @param onType optional parameter to specify type to actOn
* @returns void
*/

actOn: async (dir: string, perform: PerformFunction, onType: 'file' | 'dir' = 'file'): Promise<void> => {
for (const file of await fs.readdir(dir)) {
const filePath = path.join(dir, file);
const stat = await fs.stat(filePath);

if (stat) {
if (stat.isDirectory()) {
await fs.actOn(filePath, perform, onType);
if (onType === 'dir') {
await perform(filePath);
}
} else if (stat.isFile() && onType === 'file') {
await perform(filePath, file, dir);
}
}
}
},

/**
* Checks if files are the same
*
* @param file1Path the first file path to check
* @param file2Path the second file path to check
* @returns boolean
*/
areFilesEqual: async (file1Path: string, file2Path: string): Promise<boolean> => {
try {
const file1Size = (await fs.stat(file1Path)).size;
const file2Size = (await fs.stat(file2Path)).size;
if (file1Size !== file2Size) {
return false;
}

const contentA = await fs.readFile(file1Path);
const contentB = await fs.readFile(file2Path);

return fs.getContentHash(contentA) === fs.getContentHash(contentB);
} catch (err) {
throw new SfdxError(`The path: ${err.path} doesn't exist or access is denied.`, 'DirMissingOrNoAccess');
}
},

/**
* Creates a hash for the string that's passed in
* @param contents The string passed into the function
* @returns string
*/
getContentHash(contents: string | Buffer) {
return crypto
.createHash('sha1')
.update(contents)
.digest('hex');
}
};
126 changes: 126 additions & 0 deletions test/unit/util/fsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,130 @@ describe('util/fs', () => {
expect(exists).to.be.false;
});
});

describe('areFilesEqual', () => {
afterEach(() => {
$$.SANDBOX.restore();
});

it('should return false if the files stat.size are different', async () => {
$$.SANDBOX.stub(fs, 'readFile')
.onCall(0)
.resolves({})
.onCall(1)
.resolves({});
$$.SANDBOX.stub(fs, 'stat')
.onCall(0)
.resolves({
size: 1
})
.onCall(1)
.resolves({
size: 2
});

const results = await fs.areFilesEqual('foo/bar.json', 'foo/bar2.json');
expect(results).to.be.false;
});

it('should return true if the file hashes are the same', async () => {
$$.SANDBOX.stub(fs, 'readFile')
.onCall(0)
.resolves(
`{
"key": 12345,
"value": true,
}`
)
.onCall(1)
.resolves(
`{
"key": 12345,
"value": true,
}`
);
$$.SANDBOX.stub(fs, 'stat')
.onCall(0)
.resolves({})
.onCall(1)
.resolves({});

const results = await fs.areFilesEqual('foo/bar.json', 'foo/bar2.json');
expect(results).to.be.true;
});

it('should return false if the file hashes are different', async () => {
$$.SANDBOX.stub(fs, 'readFile')
.onCall(0)
.resolves(
`{
"key": 12345,
"value": true,
}`
)
.onCall(1)
.resolves(
`{
"key": 12345,
"value": false,
}`
);

$$.SANDBOX.stub(fs, 'stat')
.onCall(0)
.resolves({})
.onCall(1)
.resolves({});
const results = await fs.areFilesEqual('foo/bar.json', 'foo/bsar2.json');
expect(results).to.be.false;
});

it('should return error when fs.readFile throws error', async () => {
$$.SANDBOX.stub(fs, 'stat')
.onCall(0)
.resolves({
size: 1
})
.onCall(1)
.resolves({
size: 2
});
try {
await fs.areFilesEqual('foo', 'bar');
} catch (e) {
expect(e.name).to.equal('DirMissingOrNoAccess');
}
});

it('should return error when fs.stat throws error', async () => {
try {
await fs.areFilesEqual('foo', 'bar');
} catch (e) {
expect(e.name).to.equal('DirMissingOrNoAccess');
}
});
});

describe('actOn', () => {
afterEach(() => {
$$.SANDBOX.restore();
});

it('should run custom functions against contents of a directory', async () => {
const actedOnArray = [];
$$.SANDBOX.stub(fs, 'readdir').resolves(['test1.json', 'test2.json']);
$$.SANDBOX.stub(fs, 'stat').resolves({
isDirectory: () => false,
isFile: () => true
});
const pathToFolder = pathJoin(osTmpdir(), 'foo');

await fs.actOn(pathToFolder, async file => {
actedOnArray.push(file), 'file';
});
const example = [pathJoin(pathToFolder, 'test1.json'), pathJoin(pathToFolder, 'test2.json')];

expect(actedOnArray).to.eql(example);
});
});
});
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@
typedoc-plugin-external-module-name "1.1.3"
xunit-file "^1"

"@salesforce/kit@^1.0.0":
"@salesforce/kit@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@salesforce/kit/-/kit-1.2.2.tgz#9efaee2f520fba4c87a1e138fb31579cc5de69bd"
integrity sha512-MQXUh8Ka8oB1SOPUF1kOynXQWcLeZ8YWOyqPtg6j8U9NxLlkTF2vfUfhZEm//jXjejDy7CEes5rEKRHm2df/OA==
Expand Down

0 comments on commit 807dc72

Please sign in to comment.