1 | import {Correlation, FileSystem, isDisposable, UnexpectedErrorEvent} from './api';
|
2 | import {Directory, DirectoryContent, File, FileSystemNode, isDir, isFile, ShallowDirectory} from "./model";
|
3 |
|
4 | import {MemoryFileSystem} from './memory-fs';
|
5 | import {InternalEventsEmitter, makeEventsEmitter} from './utils';
|
6 | import {FileSystemReadSync} from './browser';
|
7 |
|
8 | type PathInCache = {
|
9 | [prop: string]: boolean
|
10 | };
|
11 |
|
12 | interface FileSystemNodesMap {
|
13 | [key: string]: FileSystemNode
|
14 | }
|
15 |
|
16 | interface TreesDiff {
|
17 | toAdd: FileSystemNode[],
|
18 | toDelete: FileSystemNode[],
|
19 | toChange: string[]
|
20 | }
|
21 |
|
22 | function nodesToMap(tree: FileSystemNode[], accumulator: FileSystemNodesMap = {}): FileSystemNodesMap {
|
23 | tree.forEach(node => {
|
24 | if (isDir(node)) nodesToMap(node.children, accumulator);
|
25 | accumulator[node.fullPath] = node;
|
26 | })
|
27 |
|
28 | return accumulator;
|
29 | };
|
30 |
|
31 | function getTreesDiff(cached: FileSystemNodesMap, real: FileSystemNodesMap): TreesDiff {
|
32 | const diff: TreesDiff = {
|
33 | toAdd: [],
|
34 | toDelete: [],
|
35 | toChange: []
|
36 | };
|
37 |
|
38 | Object.keys(cached).forEach(cachedPath => {
|
39 | if (!real[cachedPath]) {
|
40 | diff.toDelete.push(cached[cachedPath]);
|
41 | diff.toChange = diff.toChange.filter(path => path !== cachedPath);
|
42 | } else {
|
43 | const node = cached[cachedPath];
|
44 | if (isFile(node) && node.content) diff.toChange.push(cachedPath);
|
45 | }
|
46 | });
|
47 |
|
48 | Object.keys(real).forEach(realPath => {
|
49 | if (!cached[realPath]) diff.toAdd.push(real[realPath]);
|
50 | });
|
51 |
|
52 | return diff;
|
53 | }
|
54 |
|
55 | export class CacheFileSystem implements FileSystemReadSync, FileSystem {
|
56 | public readonly events: InternalEventsEmitter = makeEventsEmitter();
|
57 |
|
58 | public baseUrl: string;
|
59 | private cache: MemoryFileSystem;
|
60 | private isTreeCached: boolean = false;
|
61 | private pathsInCache: PathInCache = {};
|
62 | private onFsError = ({stack}: Error | UnexpectedErrorEvent) => {
|
63 | this.shouldRescanOnError ?
|
64 | this.rescanOnError() :
|
65 | this.emit('unexpectedError', {stack});
|
66 | };
|
67 |
|
68 | constructor(private fs: FileSystem, private shouldRescanOnError: boolean = true) {
|
69 | this.baseUrl = fs.baseUrl;
|
70 | this.cache = new MemoryFileSystem();
|
71 |
|
72 | this.fs.events.on('unexpectedError', this.onFsError);
|
73 |
|
74 | this.fs.events.on('fileCreated', event => {
|
75 | const {fullPath, newContent} = event;
|
76 |
|
77 | try {
|
78 | this.cache.saveFileSync(fullPath, newContent);
|
79 | this.pathsInCache[fullPath] = true;
|
80 | this.events.emit('fileCreated', event)
|
81 | } catch (e) {
|
82 | this.onFsError(e)
|
83 | }
|
84 | });
|
85 |
|
86 | this.fs.events.on('fileChanged', event => {
|
87 | const {fullPath, newContent} = event;
|
88 | try {
|
89 | this.cache.saveFileSync(fullPath, newContent);
|
90 | this.pathsInCache[fullPath] = true;
|
91 | this.events.emit('fileChanged', event);
|
92 | } catch (e) {
|
93 | this.onFsError(e)
|
94 | }
|
95 | });
|
96 |
|
97 | this.fs.events.on('fileDeleted', event => {
|
98 | try {
|
99 | this.cache.deleteFileSync(event.fullPath);
|
100 | this.pathsInCache[event.fullPath] = true;
|
101 | this.events.emit('fileDeleted', event);
|
102 | } catch (e) {
|
103 | this.onFsError(e)
|
104 | }
|
105 | });
|
106 |
|
107 | this.fs.events.on('directoryCreated', event => {
|
108 | try {
|
109 | this.cache.ensureDirectorySync(event.fullPath);
|
110 | this.events.emit('directoryCreated', event);
|
111 | } catch (e) {
|
112 | this.onFsError(e)
|
113 | }
|
114 | });
|
115 |
|
116 | this.fs.events.on('directoryDeleted', event => {
|
117 | try {
|
118 | this.cache.deleteDirectorySync(event.fullPath, true);
|
119 | this.events.emit('directoryDeleted', event);
|
120 | } catch (e) {
|
121 | this.onFsError(e)
|
122 | }
|
123 | });
|
124 | }
|
125 |
|
126 | async saveFile(fullPath: string, newContent: string): Promise<Correlation> {
|
127 | const correlation = await this.fs.saveFile(fullPath, newContent);
|
128 | this.cache.saveFileSync(fullPath, newContent);
|
129 | this.pathsInCache[fullPath] = true;
|
130 | return correlation;
|
131 | }
|
132 |
|
133 | async deleteFile(fullPath: string): Promise<Correlation> {
|
134 | const correlation = await this.fs.deleteFile(fullPath);
|
135 | this.cache.deleteFileSync(fullPath);
|
136 | return correlation;
|
137 | }
|
138 |
|
139 | async deleteDirectory(fullPath: string, recursive: boolean = false): Promise<Correlation> {
|
140 | const correlation = await this.fs.deleteDirectory(fullPath, recursive);
|
141 | this.cache.deleteDirectorySync(fullPath, recursive);
|
142 | return correlation;
|
143 | }
|
144 |
|
145 | async ensureDirectory(fullPath: string): Promise<Correlation> {
|
146 | const correlation = await this.fs.ensureDirectory(fullPath);
|
147 | this.cache.ensureDirectorySync(fullPath);
|
148 | return correlation;
|
149 | }
|
150 |
|
151 | async loadTextFile(fullPath: string): Promise<string> {
|
152 | if (this.pathsInCache[fullPath]) {
|
153 | return this.cache.loadTextFileSync(fullPath);
|
154 | }
|
155 |
|
156 | const file = await this.fs.loadTextFile(fullPath);
|
157 | this.cache.saveFileSync(fullPath, file);
|
158 | return this.cache.loadTextFileSync(fullPath);
|
159 | }
|
160 |
|
161 | loadTextFileSync(fullPath: string): string {
|
162 | return this.cache.loadTextFileSync(fullPath);
|
163 | }
|
164 |
|
165 |
|
166 | async loadDirectoryTree(fullPath?: string): Promise<Directory> {
|
167 | if (this.isTreeCached) {
|
168 | return this.cache.loadDirectoryTreeSync(fullPath);
|
169 | }
|
170 |
|
171 | await this.cacheTree();
|
172 | this.isTreeCached = true;
|
173 | return this.cache.loadDirectoryTreeSync(fullPath);
|
174 | }
|
175 |
|
176 | loadDirectoryTreeSync(fullPath: string): Directory {
|
177 | return this.cache.loadDirectoryTreeSync(fullPath);
|
178 | }
|
179 |
|
180 | loadDirectoryContentSync(fullPath: string = ''): DirectoryContent {
|
181 | return this.cache.loadDirectoryContentSync(fullPath);
|
182 | }
|
183 |
|
184 | async loadDirectoryChildren(fullPath: string): Promise<(File | ShallowDirectory)[]> {
|
185 | if (this.isTreeCached) {
|
186 | return this.cache.loadDirectoryChildrenSync(fullPath);
|
187 | }
|
188 |
|
189 | await this.cacheTree();
|
190 | this.isTreeCached = true;
|
191 | return this.cache.loadDirectoryChildrenSync(fullPath);
|
192 | }
|
193 |
|
194 | loadDirectoryChildrenSync(fullPath: string): Array<File | ShallowDirectory> {
|
195 | return this.cache.loadDirectoryChildrenSync(fullPath);
|
196 | }
|
197 |
|
198 |
|
199 | dispose() {
|
200 | if (isDisposable(this.fs)) this.fs.dispose();
|
201 | }
|
202 |
|
203 | private rescanOnError() {
|
204 | const cachedTree = this.cache.loadDirectoryTreeSync();
|
205 | this.isTreeCached = false;
|
206 |
|
207 | this.loadDirectoryTree().then(realTree => {
|
208 | const {toDelete, toAdd, toChange} = getTreesDiff(
|
209 | nodesToMap(cachedTree.children),
|
210 | nodesToMap(realTree.children)
|
211 | );
|
212 |
|
213 | toDelete.forEach(node => {
|
214 | this.emit(
|
215 | `${isDir(node) ? 'directory' : 'file'}Deleted`,
|
216 | {fullPath: node.fullPath}
|
217 | );
|
218 | });
|
219 |
|
220 | toAdd.forEach(node => {
|
221 | if (isDir(node)) {
|
222 | this.emit(
|
223 | 'directoryCreated',
|
224 | {fullPath: node.fullPath}
|
225 | );
|
226 | } else {
|
227 | this.loadTextFile(node.fullPath).then(newContent => {
|
228 | this.emit('fileCreated', {
|
229 | fullPath: node.fullPath,
|
230 | newContent
|
231 | });
|
232 | });
|
233 | }
|
234 | });
|
235 |
|
236 | toChange.forEach(fullPath => this.loadTextFile(fullPath).then(newContent => {
|
237 | this.emit('fileChanged', {fullPath, newContent});
|
238 | }))
|
239 | })
|
240 | }
|
241 |
|
242 | private emit(type: string, data: object) {
|
243 | this.events.emit(type, {...data, type});
|
244 | }
|
245 |
|
246 | private async cacheTree(): Promise<FileSystem> {
|
247 | this.cache = new MemoryFileSystem();
|
248 | this.pathsInCache = {};
|
249 | const tree = await this.fs.loadDirectoryTree();
|
250 |
|
251 | return this.fill(tree);
|
252 | }
|
253 |
|
254 | private async fill(tree: FileSystemNode): Promise<FileSystem> {
|
255 | if (isDir(tree)) {
|
256 | this.cache.ensureDirectorySync(tree.fullPath);
|
257 | await Promise.all(tree.children.map(child => this.fill(child)));
|
258 | return this.cache;
|
259 | }
|
260 |
|
261 | this.cache.saveFileSync(tree.fullPath, '');
|
262 | return this.cache;
|
263 | }
|
264 | }
|