UNPKG

9.19 kBJavaScriptView Raw
1'use strict';
2
3const os = require('os');
4const path = require('path');
5
6const Directory = require('./directory');
7const File = require('./file');
8const {FSError} = require('./error');
9const SymbolicLink = require('./symlink');
10
11const isWindows = process.platform === 'win32';
12
13function toNamespacedPath(filePath) {
14 return path.toNamespacedPath
15 ? path.toNamespacedPath(filePath)
16 : path._makeLong(filePath);
17}
18
19function getPathParts(filepath) {
20 const parts = toNamespacedPath(path.resolve(filepath)).split(path.sep);
21 parts.shift();
22 if (isWindows) {
23 // parts currently looks like ['', '?', 'c:', ...]
24 parts.shift();
25 const q = parts.shift(); // should be '?'
26 const base = '\\\\' + q + '\\' + parts.shift().toLowerCase();
27 parts.unshift(base);
28 }
29 if (parts[parts.length - 1] === '') {
30 parts.pop();
31 }
32 return parts;
33}
34
35/**
36 * Create a new file system.
37 * @param {Object} options Any filesystem options.
38 * @param {boolean} options.createCwd Create a directory for `process.cwd()`
39 * (defaults to `true`).
40 * @param {boolean} options.createTmp Create a directory for `os.tmpdir()`
41 * (defaults to `true`).
42 * @constructor
43 */
44function FileSystem(options) {
45 options = options || {};
46
47 const createCwd = 'createCwd' in options ? options.createCwd : true;
48 const createTmp = 'createTmp' in options ? options.createTmp : true;
49
50 const root = new Directory();
51
52 // populate with default directories
53 const defaults = [];
54 if (createCwd) {
55 defaults.push(process.cwd());
56 }
57
58 if (createTmp) {
59 defaults.push((os.tmpdir && os.tmpdir()) || os.tmpDir());
60 }
61
62 defaults.forEach(function(dir) {
63 const parts = getPathParts(dir);
64 let directory = root;
65 for (let i = 0, ii = parts.length; i < ii; ++i) {
66 const name = parts[i];
67 const candidate = directory.getItem(name);
68 if (!candidate) {
69 directory = directory.addItem(name, new Directory());
70 } else if (candidate instanceof Directory) {
71 directory = candidate;
72 } else {
73 throw new Error('Failed to create directory: ' + dir);
74 }
75 }
76 });
77
78 /**
79 * Root directory.
80 * @type {Directory}
81 */
82 this._root = root;
83}
84
85/**
86 * Get the root directory.
87 * @return {Directory} The root directory.
88 */
89FileSystem.prototype.getRoot = function() {
90 return this._root;
91};
92
93/**
94 * Get a file system item.
95 * @param {string} filepath Path to item.
96 * @return {Item} The item (or null if not found).
97 */
98FileSystem.prototype.getItem = function(filepath) {
99 const parts = getPathParts(filepath);
100 const currentParts = getPathParts(process.cwd());
101 let item = this._root;
102 let itemPath = '/';
103 for (let i = 0, ii = parts.length; i < ii; ++i) {
104 const name = parts[i];
105 while (item instanceof SymbolicLink) {
106 // Symbolic link being traversed as a directory --- If link targets
107 // another symbolic link, resolve target's path relative to the original
108 // link's target, otherwise relative to the current item.
109 itemPath = path.resolve(path.dirname(itemPath), item.getPath());
110 item = this.getItem(itemPath);
111 }
112 if (item) {
113 if (item instanceof Directory && name !== currentParts[i]) {
114 // make sure traversal is allowed
115 // This fails for Windows directories which do not have execute permission, by default. It may be a good idea
116 // to change this logic to windows-friendly. See notes in mock.createDirectoryInfoFromPaths()
117 if (!item.canExecute()) {
118 throw new FSError('EACCES', filepath);
119 }
120 }
121 if (item instanceof File) {
122 throw new FSError('ENOTDIR', filepath);
123 }
124 item = item.getItem(name);
125 }
126 if (!item) {
127 break;
128 }
129 itemPath = path.resolve(itemPath, name);
130 }
131 return item;
132};
133
134/**
135 * Populate a directory with an item.
136 * @param {Directory} directory The directory to populate.
137 * @param {string} name The name of the item.
138 * @param {string|Buffer|function|Object} obj Instructions for creating the
139 * item.
140 */
141function populate(directory, name, obj) {
142 let item;
143 if (typeof obj === 'string' || Buffer.isBuffer(obj)) {
144 // contents for a file
145 item = new File();
146 item.setContent(obj);
147 } else if (typeof obj === 'function') {
148 // item factory
149 item = obj();
150 } else if (typeof obj === 'object') {
151 // directory with more to populate
152 item = new Directory();
153 for (const key in obj) {
154 populate(item, key, obj[key]);
155 }
156 } else {
157 throw new Error('Unsupported type: ' + typeof obj + ' of item ' + name);
158 }
159
160 /**
161 * Special exception for redundant adding of empty directories.
162 */
163 if (
164 item instanceof Directory &&
165 item.list().length === 0 &&
166 directory.getItem(name) instanceof Directory
167 ) {
168 // pass
169 } else {
170 directory.addItem(name, item);
171 }
172}
173
174/**
175 * Configure a mock file system.
176 * @param {Object} paths Config object.
177 * @param {Object} options Any filesystem options.
178 * @param {boolean} options.createCwd Create a directory for `process.cwd()`
179 * (defaults to `true`).
180 * @param {boolean} options.createTmp Create a directory for `os.tmpdir()`
181 * (defaults to `true`).
182 * @return {FileSystem} Mock file system.
183 */
184FileSystem.create = function(paths, options) {
185 const system = new FileSystem(options);
186
187 for (const filepath in paths) {
188 const parts = getPathParts(filepath);
189 let directory = system._root;
190 for (let i = 0, ii = parts.length - 1; i < ii; ++i) {
191 const name = parts[i];
192 const candidate = directory.getItem(name);
193 if (!candidate) {
194 directory = directory.addItem(name, new Directory());
195 } else if (candidate instanceof Directory) {
196 directory = candidate;
197 } else {
198 throw new Error('Failed to create directory: ' + filepath);
199 }
200 }
201 populate(directory, parts[parts.length - 1], paths[filepath]);
202 }
203
204 return system;
205};
206
207/**
208 * Generate a factory for new files.
209 * @param {Object} config File config.
210 * @return {function():File} Factory that creates a new file.
211 */
212FileSystem.file = function(config) {
213 config = config || {};
214 return function() {
215 const file = new File();
216 if (config.hasOwnProperty('content')) {
217 file.setContent(config.content);
218 }
219 if (config.hasOwnProperty('mode')) {
220 file.setMode(config.mode);
221 } else {
222 file.setMode(438); // 0666
223 }
224 if (config.hasOwnProperty('uid')) {
225 file.setUid(config.uid);
226 }
227 if (config.hasOwnProperty('gid')) {
228 file.setGid(config.gid);
229 }
230 if (config.hasOwnProperty('atime')) {
231 file.setATime(config.atime);
232 }
233 if (config.hasOwnProperty('ctime')) {
234 file.setCTime(config.ctime);
235 }
236 if (config.hasOwnProperty('mtime')) {
237 file.setMTime(config.mtime);
238 }
239 if (config.hasOwnProperty('birthtime')) {
240 file.setBirthtime(config.birthtime);
241 }
242 return file;
243 };
244};
245
246/**
247 * Generate a factory for new symbolic links.
248 * @param {Object} config File config.
249 * @return {function():File} Factory that creates a new symbolic link.
250 */
251FileSystem.symlink = function(config) {
252 config = config || {};
253 return function() {
254 const link = new SymbolicLink();
255 if (config.hasOwnProperty('mode')) {
256 link.setMode(config.mode);
257 } else {
258 link.setMode(438); // 0666
259 }
260 if (config.hasOwnProperty('uid')) {
261 link.setUid(config.uid);
262 }
263 if (config.hasOwnProperty('gid')) {
264 link.setGid(config.gid);
265 }
266 if (config.hasOwnProperty('path')) {
267 link.setPath(config.path);
268 } else {
269 throw new Error('Missing "path" property');
270 }
271 if (config.hasOwnProperty('atime')) {
272 link.setATime(config.atime);
273 }
274 if (config.hasOwnProperty('ctime')) {
275 link.setCTime(config.ctime);
276 }
277 if (config.hasOwnProperty('mtime')) {
278 link.setMTime(config.mtime);
279 }
280 if (config.hasOwnProperty('birthtime')) {
281 link.setBirthtime(config.birthtime);
282 }
283 return link;
284 };
285};
286
287/**
288 * Generate a factory for new directories.
289 * @param {Object} config File config.
290 * @return {function():Directory} Factory that creates a new directory.
291 */
292FileSystem.directory = function(config) {
293 config = config || {};
294 return function() {
295 const dir = new Directory();
296 if (config.hasOwnProperty('mode')) {
297 dir.setMode(config.mode);
298 }
299 if (config.hasOwnProperty('uid')) {
300 dir.setUid(config.uid);
301 }
302 if (config.hasOwnProperty('gid')) {
303 dir.setGid(config.gid);
304 }
305 if (config.hasOwnProperty('items')) {
306 for (const name in config.items) {
307 populate(dir, name, config.items[name]);
308 }
309 }
310 if (config.hasOwnProperty('atime')) {
311 dir.setATime(config.atime);
312 }
313 if (config.hasOwnProperty('ctime')) {
314 dir.setCTime(config.ctime);
315 }
316 if (config.hasOwnProperty('mtime')) {
317 dir.setMTime(config.mtime);
318 }
319 if (config.hasOwnProperty('birthtime')) {
320 dir.setBirthtime(config.birthtime);
321 }
322 return dir;
323 };
324};
325
326/**
327 * Module exports.
328 * @type {function}
329 */
330exports = module.exports = FileSystem;
331exports.getPathParts = getPathParts;
332exports.toNamespacedPath = toNamespacedPath;