add code for generating and comparing FileTreeSpecs
This commit is contained in:
parent
c84d027c23
commit
73c1f665f3
1 changed files with 93 additions and 0 deletions
93
src/util/file-tree-spec.ts
Normal file
93
src/util/file-tree-spec.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import assert from 'assert'
|
||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import hasha from 'hasha'
|
||||||
|
import path from 'path'
|
||||||
|
import YAML from 'yaml'
|
||||||
|
|
||||||
|
import { yamlStringifyNoFold } from './yaml'
|
||||||
|
|
||||||
|
// FileTreeSpec is a data structure for verifying and comparing all files in a directory.
|
||||||
|
// FileTreeSpec maps file path to:
|
||||||
|
// - SHA-256 hash if file is a regular file
|
||||||
|
// - DIR_SPEC_PLACEHOLDER value if file is a directory
|
||||||
|
//
|
||||||
|
// Other file types are not supported.
|
||||||
|
export type FileTreeSpec = Map<string, string>
|
||||||
|
|
||||||
|
export const DIR_SPEC_PLACEHOLDER = 'dir'
|
||||||
|
|
||||||
|
export async function getFileTreeSpec(dir: string) {
|
||||||
|
let res: FileTreeSpec = new Map<string, string>()
|
||||||
|
await getFileTreeSpecStep(dir, dir, res)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileTreeSpecToYaml(fth: FileTreeSpec) {
|
||||||
|
return yamlStringifyNoFold(new Map([...fth.entries()].sort()))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFileTreeSpecYaml(yamlStr: string) {
|
||||||
|
return YAML.parse(yamlStr, { mapAsMap: true }) as FileTreeSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileTreeSpecStep(baseDir: string, dir: string, dst: FileTreeSpec) {
|
||||||
|
let dirents = await fs.readdir(dir, { withFileTypes: true })
|
||||||
|
let promises: Promise<void>[] = []
|
||||||
|
for (const dirent of dirents) {
|
||||||
|
let dePath = path.join(dir, dirent.name)
|
||||||
|
if (dirent.isDirectory()) {
|
||||||
|
dst.set(path.relative(baseDir, dePath), DIR_SPEC_PLACEHOLDER)
|
||||||
|
promises.push(getFileTreeSpecStep(baseDir, dePath, dst))
|
||||||
|
} else if (dirent.isFile()) {
|
||||||
|
promises.push(hashFile(baseDir, dePath, dst))
|
||||||
|
} else {
|
||||||
|
throw new Error('unexpected Dirent type ' + dirent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hashFile(baseDir: string, filePath: string, dst: FileTreeSpec) {
|
||||||
|
let hash = await hasha.fromFile(filePath, { algorithm: 'sha256' })
|
||||||
|
dst.set(path.relative(baseDir, filePath), hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileTreeComparison {
|
||||||
|
constructor(readonly a: FileTreeSpec, readonly b: FileTreeSpec) {}
|
||||||
|
|
||||||
|
// present in B, but missing in A
|
||||||
|
readonly newEntries = new Map<string, string>()
|
||||||
|
// present in A, but missing in B
|
||||||
|
readonly missingEntries = new Map<string, string>()
|
||||||
|
readonly changedEntries: string[] = []
|
||||||
|
|
||||||
|
numDiffs() {
|
||||||
|
return this.newEntries.size + this.missingEntries.size + this.changedEntries.length
|
||||||
|
}
|
||||||
|
|
||||||
|
static async get(a: FileTreeSpec, b: FileTreeSpec) {
|
||||||
|
let allPaths = new Set([...a.keys(), ...b.keys()])
|
||||||
|
|
||||||
|
let res = new FileTreeComparison(a, b)
|
||||||
|
|
||||||
|
for (let entry of allPaths) {
|
||||||
|
let valA = a.get(entry)
|
||||||
|
let valB = b.get(entry)
|
||||||
|
|
||||||
|
if (valA === valB) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (valA === undefined) {
|
||||||
|
assert(valB !== undefined)
|
||||||
|
res.newEntries.set(entry, valB)
|
||||||
|
} else if (valB === undefined) {
|
||||||
|
res.missingEntries.set(entry, valA)
|
||||||
|
} else {
|
||||||
|
res.changedEntries.push(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue