UNPKG

16.9 kBJavaScriptView Raw
1var path = require('path');
2
3var File = require('./file').File;
4var FileDescriptor = require('./descriptor').FileDescriptor;
5var Directory = require('./directory').Directory;
6
7var constants = process.binding('constants');
8
9
10/**
11 * Call the provided function and either return the result or call the callback
12 * with it (depending on if a callback is provided).
13 * @param {function()} callback Optional callback.
14 * @param {Object} thisArg This argument for the following function.
15 * @param {function()} func Function to call.
16 * @return {*} Return (if callback is not provided).
17 */
18function maybeCallback(callback, thisArg, func) {
19 if (callback) {
20 process.nextTick(function() {
21 var err = null;
22 var val;
23 try {
24 val = func.call(thisArg);
25 } catch (e) {
26 err = e;
27 }
28 callback(err, val);
29 });
30 } else {
31 return func.call(thisArg);
32 }
33}
34
35function notImplemented() {
36 throw new Error('Method not implemented');
37}
38
39function noSuchFile(filepath, source) {
40 var code = 'ENOENT';
41 var errno = 34;
42 var error = new Error(code + ', ' + source + ' \'' + filepath + '\'');
43 error.code = code;
44 error.errno = errno;
45 return error;
46}
47
48
49
50/**
51 * Create a new stats object.
52 * @param {Object} config Stats properties.
53 * @constructor
54 */
55function Stats(config) {
56 for (var key in config) {
57 this[key] = config[key];
58 }
59}
60
61
62/**
63 * Check if mode indicates property.
64 * @param {number} property Property to check.
65 * @return {boolean} Property matches mode.
66 */
67Stats.prototype._checkModeProperty = function(property) {
68 return ((this.mode & constants.S_IFMT) === property);
69};
70
71
72/**
73 * @return {Boolean} Is a directory.
74 */
75Stats.prototype.isDirectory = function() {
76 return this._checkModeProperty(constants.S_IFDIR);
77};
78
79
80/**
81 * @return {Boolean} Is a regular file.
82 */
83Stats.prototype.isFile = function() {
84 return this._checkModeProperty(constants.S_IFREG);
85};
86
87
88/**
89 * @return {Boolean} Is a block device.
90 */
91Stats.prototype.isBlockDevice = function() {
92 return this._checkModeProperty(constants.S_IFBLK);
93};
94
95
96/**
97 * @return {Boolean} Is a character device.
98 */
99Stats.prototype.isCharacterDevice = function() {
100 return this._checkModeProperty(constants.S_IFCHR);
101};
102
103
104/**
105 * @return {Boolean} Is a symbolic link.
106 */
107Stats.prototype.isSymbolicLink = function() {
108 return this._checkModeProperty(constants.S_IFLNK);
109};
110
111
112/**
113 * @return {Boolean} Is a named pipe.
114 */
115Stats.prototype.isFIFO = function() {
116 return this._checkModeProperty(constants.S_IFIFO);
117};
118
119
120/**
121 * @return {Boolean} Is a socket.
122 */
123Stats.prototype.isSocket = function() {
124 return this._checkModeProperty(constants.S_IFSOCK);
125};
126
127
128
129/**
130 * Create a new binding with the given file system.
131 * @param {FileSystem} system Mock file system.
132 * @constructor
133 */
134var Binding = exports.Binding = function(system) {
135
136 /**
137 * Mock file system.
138 * @type {FileSystem}
139 */
140 this._system = system;
141
142 /**
143 * Stats constructor.
144 * @type {function}
145 */
146 this.Stats = Stats;
147
148 /**
149 * Lookup of open files.
150 * @type {Object.<number, FileDescriptor>}
151 */
152 this._openFiles = {};
153
154 /**
155 * Counter for file descriptors.
156 * @type {number}
157 */
158 this._counter = 0;
159
160};
161
162
163/**
164 * Get the file system underlying this binding.
165 * @return {FileSystem} The underlying file system.
166 */
167Binding.prototype.getSystem = function() {
168 return this._system;
169};
170
171
172/**
173 * Reset the file system underlying this binding.
174 * @param {FileSystem} system The new file system.
175 */
176Binding.prototype.setSystem = function(system) {
177 this._system = system;
178};
179
180
181/**
182 * Get a file descriptor.
183 * @param {number} fd File descriptor identifier.
184 * @return {FileDescriptor} File descriptor.
185 */
186Binding.prototype._getDescriptorById = function(fd) {
187 if (!this._openFiles.hasOwnProperty(fd)) {
188 throw new Error('EBADF, bad file descriptor');
189 }
190 return this._openFiles[fd];
191};
192
193
194/**
195 * Keep track of a file descriptor as open.
196 * @param {FileDescriptor} descriptor The file descriptor.
197 * @return {number} Identifier for file descriptor.
198 */
199Binding.prototype._trackDescriptor = function(descriptor) {
200 var fd = ++this._counter;
201 this._openFiles[fd] = descriptor;
202 return fd;
203};
204
205
206/**
207 * Stop tracking a file descriptor as open.
208 * @param {number} fd Identifier for file descriptor.
209 */
210Binding.prototype._untrackDescriptorById = function(fd) {
211 if (!this._openFiles.hasOwnProperty(fd)) {
212 throw new Error('EBADF, bad file descriptor');
213 }
214 delete this._openFiles[fd];
215};
216
217
218/**
219 * Stat an item.
220 * @param {string} filepath Path.
221 * @param {function(Error, Stats)} callback Callback (optional).
222 * @return {Stats|undefined} Stats or undefined (if sync).
223 */
224Binding.prototype.stat = function(filepath, callback) {
225 return maybeCallback(callback, this, function() {
226 var item = this._system.getItem(filepath);
227 if (!item) {
228 throw noSuchFile(filepath, 'stat');
229 }
230 return new Stats(item.getStats());
231 });
232};
233
234
235/**
236 * Stat an item.
237 * @param {number} fd File descriptor.
238 * @param {function(Error, Stats)} callback Callback (optional).
239 * @return {Stats|undefined} Stats or undefined (if sync).
240 */
241Binding.prototype.fstat = function(fd, callback) {
242 return maybeCallback(callback, this, function() {
243 var descriptor = this._getDescriptorById(fd);
244 var item = descriptor.getItem();
245 return new Stats(item.getStats());
246 });
247};
248
249
250/**
251 * Close a file descriptor.
252 * @param {number} fd File descriptor.
253 * @param {function(Error)} callback Callback (optional).
254 */
255Binding.prototype.close = function(fd, callback) {
256 maybeCallback(callback, this, function() {
257 this._untrackDescriptorById(fd);
258 });
259};
260
261
262/**
263 * Open and possibly create a file.
264 * @param {string} pathname File path.
265 * @param {number} flags Flags.
266 * @param {number} mode Mode.
267 * @param {function(Error, string)} callback Callback (optional).
268 * @return {string} File descriptor (if sync).
269 */
270Binding.prototype.open = function(pathname, flags, mode, callback) {
271 return maybeCallback(callback, this, function() {
272 var descriptor = new FileDescriptor(flags);
273 var item = this._system.getItem(pathname);
274 if (descriptor.isExclusive() && item) {
275 throw new Error('EEXIST, file already exists');
276 }
277 if (descriptor.isCreate() && !item) {
278 var parent = this._system.getItem(path.dirname(pathname));
279 if (!parent) {
280 throw new Error('ENOENT, no such file or directory');
281 }
282 if (!(parent instanceof Directory)) {
283 throw new Error('ENOTDIR, not a directory');
284 }
285 item = new File(path.basename(pathname));
286 item.setMode(mode);
287 parent.addItem(item);
288 }
289 if (descriptor.isRead() && !item) {
290 throw new Error('ENOENT, no such file or directory');
291 }
292 if (descriptor.isTruncate()) {
293 item.setContent('');
294 }
295 if (descriptor.isTruncate() || descriptor.isAppend()) {
296 descriptor.setPosition(item.getContent().length);
297 }
298 descriptor.setItem(item);
299 return this._trackDescriptor(descriptor);
300 });
301};
302
303
304/**
305 * Read from a file descriptor.
306 * @param {string} fd File descriptor.
307 * @param {Buffer} buffer Buffer that the contents will be written to.
308 * @param {number} offset Offset in the buffer to start writing to.
309 * @param {number} length Number of bytes to read.
310 * @param {?number} position Where to begin reading in the file. If null,
311 * data will be read from the current file position.
312 * @param {function(Error, number, Buffer)} callback Callback (optional) called
313 * with any error, number of bytes read, and the buffer.
314 * @return {number} Number of bytes read (if sync).
315 */
316Binding.prototype.read = function(fd, buffer, offset, length, position,
317 callback) {
318 return maybeCallback(callback, this, function() {
319 var descriptor = this._getDescriptorById(fd);
320 if (!descriptor.isRead()) {
321 throw new Error('EBADF, bad file descriptor');
322 }
323 var file = descriptor.getItem();
324 if (!(file instanceof File)) {
325 // deleted or not a regular file
326 throw new Error('EBADF, bad file descriptor');
327 }
328 if (typeof position !== 'number' || position < 0) {
329 position = descriptor.getPosition();
330 }
331 var content = file.getContent();
332 var start = Math.min(position, content.length - 1);
333 var end = Math.min(position + length, content.length);
334 var read = content.copy(buffer, offset, start, end);
335 descriptor.setPosition(descriptor.getPosition() + read);
336 return read;
337 });
338};
339
340
341/**
342 * Write to a file descriptor.
343 * @param {string} fd File descriptor.
344 * @param {Buffer} buffer Buffer with contents to write.
345 * @param {number} offset Offset in the buffer to start writing from.
346 * @param {number} length Number of bytes to write.
347 * @param {?number} position Where to begin writing in the file. If null,
348 * data will be written to the current file position.
349 * @param {function(Error, number, Buffer)} callback Callback (optional) called
350 * with any error, number of bytes written, and the buffer.
351 * @return {number} Number of bytes written (if sync).
352 */
353Binding.prototype.write = function(fd, buffer, offset, length, position,
354 callback) {
355 return maybeCallback(callback, this, function() {
356 var descriptor = this._getDescriptorById(fd);
357 if (!descriptor.isWrite()) {
358 throw new Error('EBADF, bad file descriptor');
359 }
360 var file = descriptor.getItem();
361 if (!(file instanceof File)) {
362 // not a regular file
363 throw new Error('EBADF, bad file descriptor');
364 }
365 if (typeof position !== 'number' || position < 0) {
366 position = descriptor.getPosition();
367 }
368 var content = file.getContent();
369 var newLength = position + length;
370 if (content.length < newLength) {
371 var newContent = new Buffer(newLength);
372 content.copy(newContent);
373 content = newContent;
374 }
375 var sourceEnd = Math.min(offset + length, buffer.length);
376 var written = buffer.copy(content, position, offset, sourceEnd);
377 file.setContent(content);
378 descriptor.setPosition(newLength);
379 return written;
380 });
381};
382
383
384/**
385 * Rename a file.
386 * @param {string} oldPath Old pathname.
387 * @param {string} newPath New pathname.
388 * @param {function(Error)} callback Callback (optional).
389 * @return {undefined}
390 */
391Binding.prototype.rename = function(oldPath, newPath, callback) {
392 return maybeCallback(callback, this, function() {
393 var oldItem = this._system.getItem(oldPath);
394 if (!oldItem) {
395 throw noSuchFile(oldPath, 'rename');
396 }
397 var oldName = oldItem.getName();
398 var newItem = this._system.getItem(newPath);
399 var newParent, newName;
400 if (newItem) {
401 // make sure they are the same type
402 if (oldItem instanceof File) {
403 if (newItem instanceof Directory) {
404 // TODO: error factories
405 throw new Error('EISDIR, illegal operation on a directory');
406 }
407 } else if (oldItem instanceof Directory) {
408 if (!(newItem instanceof Directory)) {
409 throw new Error('ENOTDIR, not a directory');
410 }
411 if (newItem.list().length > 0) {
412 throw new Error('ENOTEMPTY, directory not empty');
413 }
414 }
415 newParent = newItem.getParent();
416 newName = newItem.getName();
417 newParent.removeItem(newName);
418 } else {
419 newParent = this._system.getItem(path.dirname(newPath));
420 if (!newParent) {
421 throw new Error('ENOENT, no such file or directory');
422 }
423 if (!(newParent instanceof Directory)) {
424 throw new Error('ENOTDIR, not a directory');
425 }
426 newName = path.basename(newPath);
427 }
428 oldItem.getParent().removeItem(oldName);
429 newParent.addItem(oldItem);
430 newParent.renameItem(oldName, newName);
431 });
432};
433
434
435/**
436 * Read a directory.
437 * @param {string} dirpath Path to directory.
438 * @param {function(Error, Array.<string>)} callback Callback (optional) called
439 * with any error or array of items in the directory.
440 * @return {Array.<string>} Array of items in directory (if sync).
441 */
442Binding.prototype.readdir = function(dirpath, callback) {
443 return maybeCallback(callback, this, function() {
444 var dir = this._system.getItem(dirpath);
445 if (!dir) {
446 throw new Error('ENOENT, no such file or directory');
447 }
448 if (!(dir instanceof Directory)) {
449 throw new Error('ENOTDIR, not a directory');
450 }
451 return dir.list();
452 });
453};
454
455
456/**
457 * Create a directory.
458 * @param {string} pathname Path to new directory.
459 * @param {number} mode Permissions.
460 * @param {function(Error)} callback Optional callback.
461 */
462Binding.prototype.mkdir = function(pathname, mode, callback) {
463 maybeCallback(callback, this, function() {
464 var item = this._system.getItem(pathname);
465 if (item) {
466 throw new Error('EEXIST, file already exists');
467 }
468 var parent = this._system.getItem(path.dirname(pathname));
469 if (!parent) {
470 throw new Error('ENOENT, no such file or directory');
471 }
472 var dir = new Directory(path.basename(pathname));
473 dir.setMode(mode);
474 parent.addItem(dir);
475 });
476};
477
478
479/**
480 * Remove a directory.
481 * @param {string} pathname Path to directory.
482 * @param {function(Error)} callback Optional callback.
483 */
484Binding.prototype.rmdir = function(pathname, callback) {
485 maybeCallback(callback, this, function() {
486 var item = this._system.getItem(pathname);
487 if (!item) {
488 throw new Error('ENOENT, no such file or directory');
489 }
490 if (!(item instanceof Directory)) {
491 throw new Error('ENOTDIR, not a directory');
492 }
493 if (item.list().length > 0) {
494 throw new Error('ENOTEMPTY, directory not empty');
495 }
496 item.getParent().removeItem(item.getName());
497 });
498};
499
500
501/**
502 * Truncate a file.
503 * @param {number} fd File descriptor.
504 * @param {number} len Number of bytes.
505 * @param {function(Error)} callback Optional callback.
506 */
507Binding.prototype.ftruncate = function(fd, len, callback) {
508 maybeCallback(callback, this, function() {
509 var descriptor = this._getDescriptorById(fd);
510 if (!descriptor.isWrite()) {
511 throw new Error('EINVAL, invalid argument');
512 }
513 var file = descriptor.getItem();
514 if (!(file instanceof File)) {
515 throw new Error('EINVAL, invalid argument');
516 }
517 var content = file.getContent();
518 var newContent = new Buffer(len);
519 content.copy(newContent);
520 file.setContent(newContent);
521 });
522};
523
524
525/**
526 * Legacy support.
527 * @param {number} fd File descriptor.
528 * @param {number} len Number of bytes.
529 * @param {function(Error)} callback Optional callback.
530 */
531Binding.prototype.truncate = Binding.prototype.ftruncate;
532
533
534/**
535 * Change user and group owner.
536 * @param {string} pathname Path.
537 * @param {number} uid User id.
538 * @param {number} gid Group id.
539 * @param {function(Error)} callback Optional callback.
540 */
541Binding.prototype.chown = function(pathname, uid, gid, callback) {
542 maybeCallback(callback, this, function() {
543 var item = this._system.getItem(pathname);
544 if (!item) {
545 throw new Error('ENOENT, no such file or directory');
546 }
547 item.setUid(uid);
548 item.setGid(gid);
549 });
550};
551
552
553/**
554 * Change user and group owner.
555 * @param {number} fd File descriptor.
556 * @param {number} uid User id.
557 * @param {number} gid Group id.
558 * @param {function(Error)} callback Optional callback.
559 */
560Binding.prototype.fchown = function(fd, uid, gid, callback) {
561 maybeCallback(callback, this, function() {
562 var descriptor = this._getDescriptorById(fd);
563 var item = descriptor.getItem();
564 item.setUid(uid);
565 item.setGid(gid);
566 });
567};
568
569
570/**
571 * Change permissions.
572 * @param {string} pathname Path.
573 * @param {number} mode Mode.
574 * @param {function(Error)} callback Optional callback.
575 */
576Binding.prototype.chmod = function(pathname, mode, callback) {
577 maybeCallback(callback, this, function() {
578 var item = this._system.getItem(pathname);
579 if (!item) {
580 throw new Error('ENOENT, no such file or directory');
581 }
582 item.setMode(mode);
583 });
584};
585
586
587/**
588 * Change permissions.
589 * @param {number} fd File descriptor.
590 * @param {number} mode Mode.
591 * @param {function(Error)} callback Optional callback.
592 */
593Binding.prototype.fchmod = function(fd, mode, callback) {
594 maybeCallback(callback, this, function() {
595 var descriptor = this._getDescriptorById(fd);
596 var item = descriptor.getItem();
597 item.setMode(mode);
598 });
599};
600
601
602/**
603 * Delete a named item.
604 * @param {string} pathname Path to item.
605 * @param {function(Error)} callback Optional callback.
606 */
607Binding.prototype.unlink = function(pathname, callback) {
608 maybeCallback(callback, this, function() {
609 var item = this._system.getItem(pathname);
610 if (!item) {
611 throw new Error('ENOENT, no such file or directory');
612 }
613 if (item instanceof Directory) {
614 throw new Error('EPERM, operation not permitted');
615 }
616 var parent = item.getParent();
617 parent.removeItem(item.getName());
618 });
619};