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 |
|
4 | FSTree provides the means to calculate a patch (set of operations) between one file system tree and another.
|
5 |
|
6 | The 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 |
|
14 | The operations chosen aim to minimize the amount of IO required to apply a given patch.
|
15 | For example, a naive `rm -rf` of a directory tree is actually quite costly, as child directories
|
16 | must be recursively traversed, entries stated.. etc, all to figure out what first must be deleted.
|
17 | Since we patch from tree to tree, discovering new files is both wasteful and un-needed.
|
18 |
|
19 | The operations will also be provided in a correct order, allowing us to safely
|
20 | replay operations without having to first confirm the FS is as we expect. For
|
21 | example, `unlink`s for files will occur before a `rmdir` of those files' parent
|
22 | dir. Although the ordering will be safe, a specific order is not guaranteed.
|
23 |
|
24 | A simple example:
|
25 |
|
26 | ```js
|
27 | const FSTree = require('fs-tree-diff');
|
28 | const current = FSTree.fromPaths([
|
29 | 'a.js'
|
30 | ]);
|
31 |
|
32 | const next = FSTree.fromPaths([
|
33 | 'b.js'
|
34 | ]);
|
35 |
|
36 | current.calculatePatch(next) === [
|
37 | ['unlink', 'a.js', entryA],
|
38 | ['create', 'b.js', entryB]
|
39 | ];
|
40 | ```
|
41 |
|
42 | A slightly more complicated example:
|
43 |
|
44 | ```js
|
45 | const FSTree = require('fs-tree-diff');
|
46 | const current = FSTree.fromPaths([
|
47 | 'a.js',
|
48 | 'b/',
|
49 | 'b/f.js'
|
50 | ]);
|
51 |
|
52 | const next = FSTree.fromPaths([
|
53 | 'b.js',
|
54 | 'b/',
|
55 | 'b/c/',
|
56 | 'b/c/d.js',
|
57 | 'b/e.js'
|
58 | ]);
|
59 |
|
60 | current.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 |
|
70 | Now, the above examples do not demonstrate `change` operations. This is because
|
71 | when providing only paths, we do not have sufficient information to check if
|
72 | one entry is merely different from another with the same relativePath.
|
73 |
|
74 | For this, FSTree supports more complex input structure. To demonstrate, we
|
75 | will use the [walk-sync](https://github.com/joliss/node-walk-sync) module,
|
76 | which 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
|
81 | const walkSync = require('walk-sync');
|
82 |
|
83 | // path/to/root/foo.js
|
84 | // path/to/root/bar.js
|
85 | const current = new FSTree({
|
86 | entries: walkSync.entries('path/to/root')
|
87 | });
|
88 |
|
89 | writeFileSync('path/to/root/foo.js', 'new content');
|
90 | writeFileSync('path/to/root/baz.js', 'new file');
|
91 |
|
92 | const next = new FSTree({
|
93 | entries: walkSync.entries('path/to/root')
|
94 | });
|
95 |
|
96 | current.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 |
|
103 | The entry objects provided depend on the operation. For `rmdir` and `unlink`
|
104 | operations, the current entry is provided. For `mkdir`, `change` and `create`
|
105 | operations the new entry is provided.
|
106 |
|
107 | ## API
|
108 |
|
109 | The 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 |
|
138 | The trees returned from `fromPaths` and `fromEntries` are relative to some base
|
139 | directory. `calculatePatch`, `applyPatch` and `calculateAndApplyPatch` all
|
140 | assume that the base directory has not changed.
|
141 |
|
142 | ## Input
|
143 |
|
144 | `FSTree.fromPaths`, `FSTree.fromEntries`, `FSTree.prototype.addPaths`,
|
145 | and `FSTree.prototype.addEntries` all validate their inputs. Inputs
|
146 | must be sorted, path-unique (i.e. two entries with the same `relativePath` but
|
147 | different `size`s would still be illegal input) and include intermediate
|
148 | directories.
|
149 |
|
150 | For example, the following input is **invalid**
|
151 |
|
152 | ```js
|
153 | FSTree.fromPaths([
|
154 | // => missing a/ and a/b/
|
155 | 'a/b/c.js'
|
156 | ]);
|
157 | ```
|
158 |
|
159 | To have FSTree sort and expand (include intermediate directories) for you, add
|
160 | the option `sortAndExpand`).
|
161 |
|
162 | ```js
|
163 | FStree.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 |
|
170 | FSTree.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
|
185 | entry objects **must** contain the following properties:
|
186 |
|
187 | - `relativePath`
|
188 | - `mode`
|
189 | - `size`
|
190 | - `mtime`
|
191 |
|
192 | They 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
|
199 | const walkSync = require('walk-sync');
|
200 |
|
201 | // path/to/root/foo.js
|
202 | // path/to/root/bar.js
|
203 | const current = FSTree.fromEntries(walkSync.entries('path/to/root'));
|
204 | ```
|
205 |
|
206 | ## Change Calculation
|
207 |
|
208 | When a prior entry has a `relativePath` that matches that of a current entry, a
|
209 | change operation is included if the new entry is different from the previous
|
210 | entry. This is determined by calling `isEqual`, the optional second argument
|
211 | to `calculatePatch`. If no `isEqual` is provided, a default `isEqual` is used.
|
212 |
|
213 | The default `isEqual` treats directories as always equal and files as different
|
214 | if any of the following properties have changed.
|
215 |
|
216 | - `mode`
|
217 | - `size`
|
218 | - `mtime`
|
219 |
|
220 | User specified `isEqual` will often want to use the default `isEqual`, so it is exported on `FSTree`.
|
221 |
|
222 | Example
|
223 |
|
224 | ```js
|
225 | const defaultIsEqual = FSTree.defaultIsEqual;
|
226 |
|
227 | function isEqualCheckingMeta(a, b) {
|
228 | return defaultIsEqual(a, b) && isMetaEqual(a, b);
|
229 | }
|
230 |
|
231 | function isMetaEqual(a, b) {
|
232 | // ...
|
233 | }
|
234 | ```
|
235 |
|
236 | ## Patch Application
|
237 |
|
238 | When 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
|
242 | const patch = oldInputTree.calculatePatch(newInputTree);
|
243 | const inputDir = 'src';
|
244 | const outputDir = 'dist';
|
245 | FSTree.applyPatch(inputDir, outputDir, patch);
|
246 | ```
|
247 |
|
248 | It will apply the patch changes to `dist` while using `src` as a reference for
|
249 | non-destructive operations (`mkdir`, `create`, `change`). If you want to calculate
|
250 | and apply a patch without any intermediate operations, you can do:
|
251 |
|
252 | ```js
|
253 | const inputDir = 'src';
|
254 | const outputDir = 'dist';
|
255 | oldInputTree.calculateAndApplyPatch(newInputTree, inputDir, outputDir);
|
256 | ```
|
257 |
|
258 | You can optionally provide a delegate object to handle applying specific types
|
259 | of operations:
|
260 |
|
261 | ```js
|
262 | let createCount = 0;
|
263 | FSTree.applyPatch(inputDir, outputDir, patch, {
|
264 | create: function(inputPath, outputPath, relativePath) {
|
265 | createCount++;
|
266 | copy(inputPath, outputPath);
|
267 | }
|
268 | });
|
269 | ```
|
270 |
|
271 | The available delegate functions are the same as the supported operations:
|
272 | `unlink`, `rmdir`, `mkdir`, `create`, and `change`. Each delegate function
|
273 | receives the reference `inputPath`, the `outputPath`, and `relativePath` of the file
|
274 | or directory for which to apply the operation.
|
275 |
|