UNPKG

8.56 kBMarkdownView Raw
1# fs-tree-diff [![Build Status](https://travis-ci.org/stefanpenner/fs-tree-diff.svg?branch=master)](https://travis-ci.org/stefanpenner/fs-tree-diff) [![Build status](https://ci.appveyor.com/api/projects/status/qmhx48hrquq08fam/branch/master?svg=true)](https://ci.appveyor.com/project/embercli/fs-tree-diff/branch/master)
2
3
4FSTree provides the means to calculate a patch (set of operations) between one file system tree and another.
5
6The possible operations are:
7
8* `unlink` – remove the specified file
9* `rmdir` – remove the specified folder
10* `mkdir` – create the specified folder
11* `create` – create the specified file
12* `change` – update the specified file to reflect changes
13
14The operations chosen aim to minimize the amount of IO required to apply a given patch.
15For example, a naive `rm -rf` of a directory tree is actually quite costly, as child directories
16must be recursively traversed, entries stated.. etc, all to figure out what first must be deleted.
17Since we patch from tree to tree, discovering new files is both wasteful and un-needed.
18
19The operations will also be provided in a correct order, allowing us to safely
20replay operations without having to first confirm the FS is as we expect. For
21example, `unlink`s for files will occur before a `rmdir` of those files' parent
22dir. Although the ordering will be safe, a specific order is not guaranteed.
23
24A simple example:
25
26```js
27const FSTree = require('fs-tree-diff');
28const current = FSTree.fromPaths([
29 'a.js'
30]);
31
32const next = FSTree.fromPaths([
33 'b.js'
34]);
35
36current.calculatePatch(next) === [
37 ['unlink', 'a.js', entryA],
38 ['create', 'b.js', entryB]
39];
40```
41
42A slightly more complicated example:
43
44```js
45const FSTree = require('fs-tree-diff');
46const current = FSTree.fromPaths([
47 'a.js',
48 'b/',
49 'b/f.js'
50]);
51
52const next = FSTree.fromPaths([
53 'b.js',
54 'b/',
55 'b/c/',
56 'b/c/d.js',
57 'b/e.js'
58]);
59
60current.calculatePatch(next) === [
61 ['unlink', 'a.js', entryA],
62 ['create', 'b.js', entryB],
63 ['mkdir', 'b/c', entryBC],
64 ['create', 'b/c/d.js', entryBCD],
65 ['create', 'b/e.js', entryBE]
66 ['unlink', 'b/f.js', entryBF],
67]
68```
69
70Now, the above examples do not demonstrate `change` operations. This is because
71when providing only paths, we do not have sufficient information to check if
72one entry is merely different from another with the same relativePath.
73
74For this, FSTree supports more complex input structure. To demonstrate, we
75will use the [walk-sync](https://github.com/joliss/node-walk-sync) module,
76which provides higher fidelity input, allowing FSTree to also detect changes.
77(See also the documentation for
78[walkSync.entries](https://github.com/joliss/node-walk-sync#entries).)
79
80```js
81const walkSync = require('walk-sync');
82
83// path/to/root/foo.js
84// path/to/root/bar.js
85const current = new FSTree({
86 entries: walkSync.entries('path/to/root')
87});
88
89writeFileSync('path/to/root/foo.js', 'new content');
90writeFileSync('path/to/root/baz.js', 'new file');
91
92const next = new FSTree({
93 entries: walkSync.entries('path/to/root')
94});
95
96current.calculatePatch(next) === [
97 ['change', 'foo.js', entryFoo], // mtime + size changed, so this input is stale and needs updating.
98 ['create', 'baz.js', entryBaz] // new file, so we should create it
99 /* bar stays the same and is left inert*/
100];
101```
102
103The entry objects provided depend on the operation. For `rmdir` and `unlink`
104operations, the current entry is provided. For `mkdir`, `change` and `create`
105operations the new entry is provided.
106
107## API
108
109The public API is:
110
111- `FSTree.fromPaths` initialize a tree from an array of string paths.
112- `FSTree.fromEntries` initialize a tree from an array of `Entry` objects.
113 Each entry must have the following properties (but may have more):
114
115 - `relativePath`
116 - `mode`
117 - `size`
118 - `mtime`
119- `FSTree.applyPatch(inputDir, outputDir, patch, delegate)` applies the given
120 patch from the input directory to the output directory. You can optionally
121 provide a delegate object to handle individual types of patch operations.
122- `FSTree.prototype.calculatePatch(newTree, isEqual)` calculate a patch against
123 `newTree`. Optionally specify a custom `isEqual` (see Change Calculation).
124- `FSTree.prototype.calculateAndApplyPatch(newTree, inputDir, outputDir, delegate)`
125 does a `calculatePatch` followed by `applyPatch`.
126- `FSTree.prototype.addEntries(entries, options)` adds entries to an
127 existing tree. Options are the same as for `FSTree.fromEntries`.
128 Entries added with the same path will overwrite any existing entries.
129- `FSTree.prototype.addPaths(paths, options)` adds paths to an
130 existing tree. Options are the same as for `FSTree.fromPaths`.
131 If entries already exist for any of the paths added, those entries will
132 be updated.
133- `Entry.fromStat(relativePath, stat)` creates an `Entry` from a given path and
134 [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object. It can
135 then be used with `fromEntries` or `addEntries`.
136
137
138The trees returned from `fromPaths` and `fromEntries` are relative to some base
139directory. `calculatePatch`, `applyPatch` and `calculateAndApplyPatch` all
140assume that the base directory has not changed.
141
142## Input
143
144`FSTree.fromPaths`, `FSTree.fromEntries`, `FSTree.prototype.addPaths`,
145and `FSTree.prototype.addEntries` all validate their inputs. Inputs
146must be sorted, path-unique (i.e. two entries with the same `relativePath` but
147different `size`s would still be illegal input) and include intermediate
148directories.
149
150For example, the following input is **invalid**
151
152```js
153FSTree.fromPaths([
154 // => missing a/ and a/b/
155 'a/b/c.js'
156]);
157```
158
159To have FSTree sort and expand (include intermediate directories) for you, add
160the option `sortAndExpand`).
161
162```js
163FStree.fromPaths([
164 'a/b/q/r/bar.js',
165 'a/b/c/d/foo.js',
166], { sortAndExpand: true });
167
168// The above is equivalent to
169
170FSTree.fromPaths([
171 'a/',
172 'a/b/',
173 'a/b/c/',
174 'a/b/c/d/',
175 'a/b/c/d/foo.js',
176 'a/b/q/',
177 'a/b/q/r/',
178 'a/b/q/r/bar.js',
179]);
180```
181
182## Entry
183
184`FSTree.fromEntries` requires you to supply your own `Entry` objects. Your
185entry objects **must** contain the following properties:
186
187 - `relativePath`
188 - `mode`
189 - `size`
190 - `mtime`
191
192They must also implement the following API:
193
194 - `isDirectory()` `true` *iff* this entry is a directory
195
196`FSTree.fromEntries` composes well with the output of `walkSync.entries`:
197
198```js
199const walkSync = require('walk-sync');
200
201// path/to/root/foo.js
202// path/to/root/bar.js
203const current = FSTree.fromEntries(walkSync.entries('path/to/root'));
204```
205
206## Change Calculation
207
208When a prior entry has a `relativePath` that matches that of a current entry, a
209change operation is included if the new entry is different from the previous
210entry. This is determined by calling `isEqual`, the optional second argument
211to `calculatePatch`. If no `isEqual` is provided, a default `isEqual` is used.
212
213The default `isEqual` treats directories as always equal and files as different
214if any of the following properties have changed.
215
216 - `mode`
217 - `size`
218 - `mtime`
219
220User specified `isEqual` will often want to use the default `isEqual`, so it is exported on `FSTree`.
221
222Example
223
224```js
225const defaultIsEqual = FSTree.defaultIsEqual;
226
227function isEqualCheckingMeta(a, b) {
228 return defaultIsEqual(a, b) && isMetaEqual(a, b);
229}
230
231function isMetaEqual(a, b) {
232 // ...
233}
234```
235
236## Patch Application
237
238When you want to apply changes from one tree to another easily, you can use the
239`FSTree.applyPatch` method. For example, given:
240
241```js
242const patch = oldInputTree.calculatePatch(newInputTree);
243const inputDir = 'src';
244const outputDir = 'dist';
245FSTree.applyPatch(inputDir, outputDir, patch);
246```
247
248It will apply the patch changes to `dist` while using `src` as a reference for
249non-destructive operations (`mkdir`, `create`, `change`). If you want to calculate
250and apply a patch without any intermediate operations, you can do:
251
252```js
253const inputDir = 'src';
254const outputDir = 'dist';
255oldInputTree.calculateAndApplyPatch(newInputTree, inputDir, outputDir);
256```
257
258You can optionally provide a delegate object to handle applying specific types
259of operations:
260
261```js
262let createCount = 0;
263FSTree.applyPatch(inputDir, outputDir, patch, {
264 create: function(inputPath, outputPath, relativePath) {
265 createCount++;
266 copy(inputPath, outputPath);
267 }
268});
269```
270
271The available delegate functions are the same as the supported operations:
272`unlink`, `rmdir`, `mkdir`, `create`, and `change`. Each delegate function
273receives the reference `inputPath`, the `outputPath`, and `relativePath` of the file
274or directory for which to apply the operation.
275