1 | "use strict";
|
2 |
|
3 | const path = require('path');
|
4 |
|
5 | const {
|
6 | createHash
|
7 | } = require('crypto');
|
8 |
|
9 | const errors = require('errno');
|
10 |
|
11 | const util = require('util');
|
12 |
|
13 | const {
|
14 | ReadableStream,
|
15 | WritableStream
|
16 | } = require('stream');
|
17 |
|
18 | const MemoryFileSystemError = require('memory-fs/lib/MemoryFileSystem');
|
19 |
|
20 | const debug = util.debuglog('astroturf:memory-fs');
|
21 |
|
22 | const returnsTrue = () => true;
|
23 |
|
24 | const returnsFalse = () => false;
|
25 |
|
26 | const md5 = input => {
|
27 | const hash = createHash('md5');
|
28 | hash.update(input);
|
29 | return hash.digest('hex');
|
30 | };
|
31 |
|
32 | const read = (file, optsOrEncoding) => {
|
33 | const encoding = (optsOrEncoding === null || optsOrEncoding === void 0 ? void 0 : optsOrEncoding.encoding) || optsOrEncoding;
|
34 | file.atime = new Date();
|
35 | return encoding ? file.contents.toString(encoding) : file.contents;
|
36 | };
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | class MemoryFs {
|
45 | constructor() {
|
46 | this.addFile = (p, data, updateMtime = false) => {
|
47 | p = path.normalize(p);
|
48 | const hash = md5(data);
|
49 | const existing = this.paths.get(p);
|
50 | const keepTime = !updateMtime && existing && existing.hash === hash;
|
51 | if (!keepTime) debug(`${existing ? 'modifying' : 'writing'} file ${path.relative(process.cwd(), p)} [${hash}]`);
|
52 | const mtime = keepTime ? existing.mtime : new Date();
|
53 | this.paths.set(p, {
|
54 | hash,
|
55 | contents: Buffer.isBuffer(data) ? data : Buffer.from(data),
|
56 | birthtime: existing ? existing.birthtime : new Date(),
|
57 | ctime: existing ? existing.ctime : new Date(),
|
58 | atime: existing ? existing.atime : new Date(),
|
59 | mtime
|
60 | });
|
61 | return mtime;
|
62 | };
|
63 |
|
64 | this.getPaths = () => this.paths;
|
65 |
|
66 | this.exists = (p, cb) => cb(this.existsSync(p));
|
67 |
|
68 | this.existsSync = p => this.paths.has(path.normalize(p));
|
69 |
|
70 | this.statSync = p => {
|
71 | const file = this.paths.get(path.normalize(p));
|
72 | if (file) return {
|
73 | mtime: file.mtime,
|
74 | atime: file.atime,
|
75 | ctime: file.ctime,
|
76 | birthtime: file.birthtime,
|
77 | mtimeMs: file.mtime.getTime(),
|
78 | atimeMs: file.atime.getTime(),
|
79 | ctimeMs: file.ctime.getTime(),
|
80 | birthtimeMs: file.birthtime.getTime(),
|
81 | isFile: returnsTrue,
|
82 | isDirectory: returnsFalse,
|
83 | isBlockDevice: returnsFalse,
|
84 | isCharacterDevice: returnsFalse,
|
85 | isSymbolicLink: returnsFalse,
|
86 | isFIFO: returnsFalse,
|
87 | isSocket: returnsFalse
|
88 | };
|
89 | throw new MemoryFileSystemError(errors.code.ENOENT, p, 'stat');
|
90 | };
|
91 |
|
92 | this.readFileSync = (p, optsOrEncoding) => {
|
93 | p = path.normalize(p);
|
94 | if (!this.existsSync(p)) throw new MemoryFileSystemError(errors.code.ENOENT, p, 'readFile');
|
95 | return read(this.paths.get(p), optsOrEncoding);
|
96 | };
|
97 |
|
98 | this.readdirSync = p => {
|
99 | const results = [];
|
100 | p = path.normalize(p);
|
101 | this.paths.forEach((_, key) => {
|
102 | if (key.startsWith(p)) results.push(key);
|
103 | });
|
104 | };
|
105 |
|
106 | this.rmdirSync = p => {
|
107 | p = path.normalize(p);
|
108 | this.paths.forEach((_, key) => {
|
109 | if (p.startsWith(key)) this.unlinkSync(key);
|
110 | });
|
111 | };
|
112 |
|
113 | this.unlinkSync = p => this.paths.delete(p);
|
114 |
|
115 | this.writeFileSync = (p, data) => {
|
116 | this.addFile(p, data, true);
|
117 | };
|
118 |
|
119 | this.paths = new Map();
|
120 | ['stat', 'readdir', 'rmdir', 'unlink', 'readFile', 'writeFile'].forEach(fn => {
|
121 | this[fn] = (...args) => {
|
122 | const cb = args.pop();
|
123 | let result;
|
124 |
|
125 | try {
|
126 | result = this[`${fn}Sync`](...args);
|
127 | } catch (err) {
|
128 | setImmediate(() => cb(err));
|
129 | return;
|
130 | }
|
131 |
|
132 | setImmediate(() => cb(null, result));
|
133 | };
|
134 | });
|
135 | }
|
136 |
|
137 |
|
138 | createReadStream(p, options) {
|
139 | const stream = new ReadableStream();
|
140 | let done = false;
|
141 | let data;
|
142 |
|
143 | try {
|
144 | data = this.readFileSync(p);
|
145 | } catch (e) {
|
146 |
|
147 | stream._read = function $read() {
|
148 | if (done) return;
|
149 | done = true;
|
150 | this.emit('error', e);
|
151 | this.push(null);
|
152 | };
|
153 |
|
154 | return stream;
|
155 | }
|
156 |
|
157 | options = options || {};
|
158 | options.start = options.start || 0;
|
159 | options.end = options.end || data.length;
|
160 |
|
161 | stream._read = function $read() {
|
162 | if (done) return;
|
163 | done = true;
|
164 | this.push(data.slice(options.start, options.end));
|
165 | this.push(null);
|
166 | };
|
167 |
|
168 | return stream;
|
169 | }
|
170 |
|
171 | createWriteStream(p) {
|
172 | const stream = new WritableStream();
|
173 |
|
174 | try {
|
175 | this.writeFileSync(p, Buffer.from(0));
|
176 | } catch (e) {
|
177 | stream.once('prefinish', () => {
|
178 | stream.emit('error', e);
|
179 | });
|
180 | return stream;
|
181 | }
|
182 |
|
183 | const bl = [];
|
184 | let len = 0;
|
185 |
|
186 | stream._write = (chunk, encoding, callback) => {
|
187 | bl.push(chunk);
|
188 | len += chunk.length;
|
189 | this.writeFile(p, Buffer.concat(bl, len), callback);
|
190 | };
|
191 |
|
192 | return stream;
|
193 | }
|
194 |
|
195 | }
|
196 |
|
197 | module.exports = MemoryFs; |
\ | No newline at end of file |