UNPKG

22.7 kBJavaScriptView Raw
1var path = require('path');
2
3var File = require('./file');
4var FileDescriptor = require('./descriptor');
5var Directory = require('./directory');
6var SymbolicLink = require('./symlink');
7var FSError = require('./error');
8
9var constants = process.binding('constants');
10
11
12/**
13 * Call the provided function and either return the result or call the callback
14 * with it (depending on if a callback is provided).
15 * @param {function()} callback Optional callback.
16 * @param {Object} thisArg This argument for the following function.
17 * @param {function()} func Function to call.
18 * @return {*} Return (if callback is not provided).
19 */
20function maybeCallback(callback, thisArg, func) {
21 if (callback) {
22 var err = null;
23 var val;
24 try {
25 val = func.call(thisArg);
26 } catch (e) {
27 err = e;
28 }
29 process.nextTick(function() {
30 callback(err, val);
31 });
32 } else {
33 return func.call(thisArg);
34 }
35}
36
37function notImplemented() {
38 throw new Error('Method not implemented');
39}
40
41
42
43/**
44 * Create a new stats object.
45 * @param {Object} config Stats properties.
46 * @constructor
47 */
48function Stats(config) {
49 for (var key in config) {
50 this[key] = config[key];
51 }
52}
53
54
55/**
56 * Check if mode indicates property.
57 * @param {number} property Property to check.
58 * @return {boolean} Property matches mode.
59 */
60Stats.prototype._checkModeProperty = function(property) {
61 return ((this.mode & constants.S_IFMT) === property);
62};
63
64
65/**
66 * @return {Boolean} Is a directory.
67 */
68Stats.prototype.isDirectory = function() {
69 return this._checkModeProperty(constants.S_IFDIR);
70};
71
72
73/**
74 * @return {Boolean} Is a regular file.
75 */
76Stats.prototype.isFile = function() {
77 return this._checkModeProperty(constants.S_IFREG);
78};
79
80
81/**
82 * @return {Boolean} Is a block device.
83 */
84Stats.prototype.isBlockDevice = function() {
85 return this._checkModeProperty(constants.S_IFBLK);
86};
87
88
89/**
90 * @return {Boolean} Is a character device.
91 */
92Stats.prototype.isCharacterDevice = function() {
93 return this._checkModeProperty(constants.S_IFCHR);
94};
95
96
97/**
98 * @return {Boolean} Is a symbolic link.
99 */
100Stats.prototype.isSymbolicLink = function() {
101 return this._checkModeProperty(constants.S_IFLNK);
102};
103
104
105/**
106 * @return {Boolean} Is a named pipe.
107 */
108Stats.prototype.isFIFO = function() {
109 return this._checkModeProperty(constants.S_IFIFO);
110};
111
112
113/**
114 * @return {Boolean} Is a socket.
115 */
116Stats.prototype.isSocket = function() {
117 return this._checkModeProperty(constants.S_IFSOCK);
118};
119
120
121
122/**
123 * Create a new binding with the given file system.
124 * @param {FileSystem} system Mock file system.
125 * @constructor
126 */
127function Binding(system) {
128
129 /**
130 * Mock file system.
131 * @type {FileSystem}
132 */
133 this._system = system;
134
135 /**
136 * Stats constructor.
137 * @type {function}
138 */
139 this.Stats = Stats;
140
141 /**
142 * Lookup of open files.
143 * @type {Object.<number, FileDescriptor>}
144 */
145 this._openFiles = {};
146
147 /**
148 * Counter for file descriptors.
149 * @type {number}
150 */
151 this._counter = 0;
152
153}
154
155
156/**
157 * Get the file system underlying this binding.
158 * @return {FileSystem} The underlying file system.
159 */
160Binding.prototype.getSystem = function() {
161 return this._system;
162};
163
164
165/**
166 * Reset the file system underlying this binding.
167 * @param {FileSystem} system The new file system.
168 */
169Binding.prototype.setSystem = function(system) {
170 this._system = system;
171};
172
173
174/**
175 * Get a file descriptor.
176 * @param {number} fd File descriptor identifier.
177 * @return {FileDescriptor} File descriptor.
178 */
179Binding.prototype._getDescriptorById = function(fd) {
180 if (!this._openFiles.hasOwnProperty(fd)) {
181 throw new FSError('EBADF');
182 }
183 return this._openFiles[fd];
184};
185
186
187/**
188 * Keep track of a file descriptor as open.
189 * @param {FileDescriptor} descriptor The file descriptor.
190 * @return {number} Identifier for file descriptor.
191 */
192Binding.prototype._trackDescriptor = function(descriptor) {
193 var fd = ++this._counter;
194 this._openFiles[fd] = descriptor;
195 return fd;
196};
197
198
199/**
200 * Stop tracking a file descriptor as open.
201 * @param {number} fd Identifier for file descriptor.
202 */
203Binding.prototype._untrackDescriptorById = function(fd) {
204 if (!this._openFiles.hasOwnProperty(fd)) {
205 throw new FSError('EBADF');
206 }
207 delete this._openFiles[fd];
208};
209
210
211/**
212 * Stat an item.
213 * @param {string} filepath Path.
214 * @param {function(Error, Stats)} callback Callback (optional).
215 * @return {Stats|undefined} Stats or undefined (if sync).
216 */
217Binding.prototype.stat = function(filepath, callback) {
218 return maybeCallback(callback, this, function() {
219 var item = this._system.getItem(filepath);
220 if (item instanceof SymbolicLink) {
221 item = this._system.getItem(
222 path.resolve(path.dirname(filepath), item.getPath()));
223 }
224 if (!item) {
225 throw new FSError('ENOENT', filepath);
226 }
227 return new Stats(item.getStats());
228 });
229};
230
231
232/**
233 * Stat an item.
234 * @param {number} fd File descriptor.
235 * @param {function(Error, Stats)} callback Callback (optional).
236 * @return {Stats|undefined} Stats or undefined (if sync).
237 */
238Binding.prototype.fstat = function(fd, callback) {
239 return maybeCallback(callback, this, function() {
240 var descriptor = this._getDescriptorById(fd);
241 var item = descriptor.getItem();
242 return new Stats(item.getStats());
243 });
244};
245
246
247/**
248 * Close a file descriptor.
249 * @param {number} fd File descriptor.
250 * @param {function(Error)} callback Callback (optional).
251 */
252Binding.prototype.close = function(fd, callback) {
253 maybeCallback(callback, this, function() {
254 this._untrackDescriptorById(fd);
255 });
256};
257
258
259/**
260 * Open and possibly create a file.
261 * @param {string} pathname File path.
262 * @param {number} flags Flags.
263 * @param {number} mode Mode.
264 * @param {function(Error, string)} callback Callback (optional).
265 * @return {string} File descriptor (if sync).
266 */
267Binding.prototype.open = function(pathname, flags, mode, callback) {
268 return maybeCallback(callback, this, function() {
269 var descriptor = new FileDescriptor(flags);
270 var item = this._system.getItem(pathname);
271 if (item instanceof SymbolicLink) {
272 item = this._system.getItem(
273 path.resolve(path.dirname(pathname), item.getPath()));
274 }
275 if (descriptor.isExclusive() && item) {
276 throw new FSError('EEXIST', pathname);
277 }
278 if (descriptor.isCreate() && !item) {
279 var parent = this._system.getItem(path.dirname(pathname));
280 if (!parent) {
281 throw new FSError('ENOENT', pathname);
282 }
283 if (!(parent instanceof Directory)) {
284 throw new FSError('ENOTDIR', pathname);
285 }
286 item = new File();
287 item.setMode(mode);
288 parent.addItem(path.basename(pathname), item);
289 }
290 if (descriptor.isRead() && !item) {
291 throw new FSError('ENOENT', pathname);
292 }
293 if (descriptor.isTruncate()) {
294 item.setContent('');
295 }
296 if (descriptor.isTruncate() || descriptor.isAppend()) {
297 descriptor.setPosition(item.getContent().length);
298 }
299 descriptor.setItem(item);
300 return this._trackDescriptor(descriptor);
301 });
302};
303
304
305/**
306 * Read from a file descriptor.
307 * @param {string} fd File descriptor.
308 * @param {Buffer} buffer Buffer that the contents will be written to.
309 * @param {number} offset Offset in the buffer to start writing to.
310 * @param {number} length Number of bytes to read.
311 * @param {?number} position Where to begin reading in the file. If null,
312 * data will be read from the current file position.
313 * @param {function(Error, number, Buffer)} callback Callback (optional) called
314 * with any error, number of bytes read, and the buffer.
315 * @return {number} Number of bytes read (if sync).
316 */
317Binding.prototype.read = function(fd, buffer, offset, length, position,
318 callback) {
319 return maybeCallback(callback, this, function() {
320 var descriptor = this._getDescriptorById(fd);
321 if (!descriptor.isRead()) {
322 throw new FSError('EBADF');
323 }
324 var file = descriptor.getItem();
325 if (!(file instanceof File)) {
326 // deleted or not a regular file
327 throw new FSError('EBADF');
328 }
329 if (typeof position !== 'number' || position < 0) {
330 position = descriptor.getPosition();
331 }
332 var content = file.getContent();
333 var start = Math.min(position, content.length);
334 var end = Math.min(position + length, content.length);
335 var read = (start < end) ? content.copy(buffer, offset, start, end) : 0;
336 descriptor.setPosition(position + read);
337 return read;
338 });
339};
340
341
342/**
343 * Write to a file descriptor given a buffer.
344 * @param {string} fd File descriptor.
345 * @param {Buffer} buffer Buffer with contents to write.
346 * @param {number} offset Offset in the buffer to start writing from.
347 * @param {number} length Number of bytes to write.
348 * @param {?number} position Where to begin writing in the file. If null,
349 * data will be written to the current file position.
350 * @param {function(Error, number, Buffer)} callback Callback (optional) called
351 * with any error, number of bytes written, and the buffer.
352 * @return {number} Number of bytes written (if sync).
353 */
354Binding.prototype.writeBuffer = function(fd, buffer, offset, length, position,
355 callback) {
356 return maybeCallback(callback, this, function() {
357 var descriptor = this._getDescriptorById(fd);
358 if (!descriptor.isWrite()) {
359 throw new FSError('EBADF');
360 }
361 var file = descriptor.getItem();
362 if (!(file instanceof File)) {
363 // not a regular file
364 throw new FSError('EBADF');
365 }
366 if (typeof position !== 'number' || position < 0) {
367 position = descriptor.getPosition();
368 }
369 var content = file.getContent();
370 var newLength = position + length;
371 if (content.length < newLength) {
372 var newContent = new Buffer(newLength);
373 content.copy(newContent);
374 content = newContent;
375 }
376 var sourceEnd = Math.min(offset + length, buffer.length);
377 var written = buffer.copy(content, position, offset, sourceEnd);
378 file.setContent(content);
379 descriptor.setPosition(newLength);
380 return written;
381 });
382};
383
384
385/**
386 * Alias for writeBuffer (used in Node <= 0.10).
387 * @param {string} fd File descriptor.
388 * @param {Buffer} buffer Buffer with contents to write.
389 * @param {number} offset Offset in the buffer to start writing from.
390 * @param {number} length Number of bytes to write.
391 * @param {?number} position Where to begin writing in the file. If null,
392 * data will be written to the current file position.
393 * @param {function(Error, number, Buffer)} callback Callback (optional) called
394 * with any error, number of bytes written, and the buffer.
395 * @return {number} Number of bytes written (if sync).
396 */
397Binding.prototype.write = Binding.prototype.writeBuffer;
398
399
400/**
401 * Write to a file descriptor given a string.
402 * @param {string} fd File descriptor.
403 * @param {string} string String with contents to write.
404 * @param {number} position Where to begin writing in the file. If null,
405 * data will be written to the current file position.
406 * @param {string} encoding String encoding.
407 * @param {function(Error, number, string)} callback Callback (optional) called
408 * with any error, number of bytes written, and the string.
409 * @return {number} Number of bytes written (if sync).
410 */
411Binding.prototype.writeString = function(fd, string, position, encoding,
412 callback) {
413 var buffer = new Buffer(string, encoding);
414 var wrapper;
415 if (callback) {
416 wrapper = function(err, written, returned) {
417 callback(err, written, returned && string);
418 };
419 }
420 return this.writeBuffer(fd, buffer, 0, string.length, position, wrapper);
421};
422
423
424/**
425 * Rename a file.
426 * @param {string} oldPath Old pathname.
427 * @param {string} newPath New pathname.
428 * @param {function(Error)} callback Callback (optional).
429 * @return {undefined}
430 */
431Binding.prototype.rename = function(oldPath, newPath, callback) {
432 return maybeCallback(callback, this, function() {
433 var oldItem = this._system.getItem(oldPath);
434 if (!oldItem) {
435 throw new FSError('ENOENT', oldPath);
436 }
437 var oldParent = this._system.getItem(path.dirname(oldPath));
438 var oldName = path.basename(oldPath);
439 var newItem = this._system.getItem(newPath);
440 var newParent = this._system.getItem(path.dirname(newPath));
441 var newName = path.basename(newPath);
442 if (newItem) {
443 // make sure they are the same type
444 if (oldItem instanceof File) {
445 if (newItem instanceof Directory) {
446 throw new FSError('EISDIR', newPath);
447 }
448 } else if (oldItem instanceof Directory) {
449 if (!(newItem instanceof Directory)) {
450 throw new FSError('ENOTDIR', newPath);
451 }
452 if (newItem.list().length > 0) {
453 throw new FSError('ENOTEMPTY', newPath);
454 }
455 }
456 newParent.removeItem(newName);
457 } else {
458 if (!newParent) {
459 throw new FSError('ENOENT', newPath);
460 }
461 if (!(newParent instanceof Directory)) {
462 throw new FSError('ENOTDIR', newPath);
463 }
464 }
465 oldParent.removeItem(oldName);
466 newParent.addItem(newName, oldItem);
467 });
468};
469
470
471/**
472 * Read a directory.
473 * @param {string} dirpath Path to directory.
474 * @param {function(Error, Array.<string>)} callback Callback (optional) called
475 * with any error or array of items in the directory.
476 * @return {Array.<string>} Array of items in directory (if sync).
477 */
478Binding.prototype.readdir = function(dirpath, callback) {
479 return maybeCallback(callback, this, function() {
480 var dir = this._system.getItem(dirpath);
481 if (!dir) {
482 throw new FSError('ENOENT', dirpath);
483 }
484 if (!(dir instanceof Directory)) {
485 throw new FSError('ENOTDIR', dirpath);
486 }
487 return dir.list();
488 });
489};
490
491
492/**
493 * Create a directory.
494 * @param {string} pathname Path to new directory.
495 * @param {number} mode Permissions.
496 * @param {function(Error)} callback Optional callback.
497 */
498Binding.prototype.mkdir = function(pathname, mode, callback) {
499 maybeCallback(callback, this, function() {
500 var item = this._system.getItem(pathname);
501 if (item) {
502 throw new FSError('EEXIST', pathname);
503 }
504 var parent = this._system.getItem(path.dirname(pathname));
505 if (!parent) {
506 throw new FSError('ENOENT', pathname);
507 }
508 var dir = new Directory();
509 dir.setMode(mode);
510 parent.addItem(path.basename(pathname), dir);
511 });
512};
513
514
515/**
516 * Remove a directory.
517 * @param {string} pathname Path to directory.
518 * @param {function(Error)} callback Optional callback.
519 */
520Binding.prototype.rmdir = function(pathname, callback) {
521 maybeCallback(callback, this, function() {
522 var item = this._system.getItem(pathname);
523 if (!item) {
524 throw new FSError('ENOENT', pathname);
525 }
526 if (!(item instanceof Directory)) {
527 throw new FSError('ENOTDIR', pathname);
528 }
529 if (item.list().length > 0) {
530 throw new FSError('ENOTEMPTY', pathname);
531 }
532 var parent = this._system.getItem(path.dirname(pathname));
533 parent.removeItem(path.basename(pathname));
534 });
535};
536
537
538/**
539 * Truncate a file.
540 * @param {number} fd File descriptor.
541 * @param {number} len Number of bytes.
542 * @param {function(Error)} callback Optional callback.
543 */
544Binding.prototype.ftruncate = function(fd, len, callback) {
545 maybeCallback(callback, this, function() {
546 var descriptor = this._getDescriptorById(fd);
547 if (!descriptor.isWrite()) {
548 throw new FSError('EINVAL');
549 }
550 var file = descriptor.getItem();
551 if (!(file instanceof File)) {
552 throw new FSError('EINVAL');
553 }
554 var content = file.getContent();
555 var newContent = new Buffer(len);
556 content.copy(newContent);
557 file.setContent(newContent);
558 });
559};
560
561
562/**
563 * Legacy support.
564 * @param {number} fd File descriptor.
565 * @param {number} len Number of bytes.
566 * @param {function(Error)} callback Optional callback.
567 */
568Binding.prototype.truncate = Binding.prototype.ftruncate;
569
570
571/**
572 * Change user and group owner.
573 * @param {string} pathname Path.
574 * @param {number} uid User id.
575 * @param {number} gid Group id.
576 * @param {function(Error)} callback Optional callback.
577 */
578Binding.prototype.chown = function(pathname, uid, gid, callback) {
579 maybeCallback(callback, this, function() {
580 var item = this._system.getItem(pathname);
581 if (!item) {
582 throw new FSError('ENOENT', pathname);
583 }
584 item.setUid(uid);
585 item.setGid(gid);
586 });
587};
588
589
590/**
591 * Change user and group owner.
592 * @param {number} fd File descriptor.
593 * @param {number} uid User id.
594 * @param {number} gid Group id.
595 * @param {function(Error)} callback Optional callback.
596 */
597Binding.prototype.fchown = function(fd, uid, gid, callback) {
598 maybeCallback(callback, this, function() {
599 var descriptor = this._getDescriptorById(fd);
600 var item = descriptor.getItem();
601 item.setUid(uid);
602 item.setGid(gid);
603 });
604};
605
606
607/**
608 * Change permissions.
609 * @param {string} pathname Path.
610 * @param {number} mode Mode.
611 * @param {function(Error)} callback Optional callback.
612 */
613Binding.prototype.chmod = function(pathname, mode, callback) {
614 maybeCallback(callback, this, function() {
615 var item = this._system.getItem(pathname);
616 if (!item) {
617 throw new FSError('ENOENT', pathname);
618 }
619 item.setMode(mode);
620 });
621};
622
623
624/**
625 * Change permissions.
626 * @param {number} fd File descriptor.
627 * @param {number} mode Mode.
628 * @param {function(Error)} callback Optional callback.
629 */
630Binding.prototype.fchmod = function(fd, mode, callback) {
631 maybeCallback(callback, this, function() {
632 var descriptor = this._getDescriptorById(fd);
633 var item = descriptor.getItem();
634 item.setMode(mode);
635 });
636};
637
638
639/**
640 * Delete a named item.
641 * @param {string} pathname Path to item.
642 * @param {function(Error)} callback Optional callback.
643 */
644Binding.prototype.unlink = function(pathname, callback) {
645 maybeCallback(callback, this, function() {
646 var item = this._system.getItem(pathname);
647 if (!item) {
648 throw new FSError('ENOENT', pathname);
649 }
650 if (item instanceof Directory) {
651 throw new FSError('EPERM', pathname);
652 }
653 var parent = this._system.getItem(path.dirname(pathname));
654 parent.removeItem(path.basename(pathname));
655 });
656};
657
658
659/**
660 * Update timestamps.
661 * @param {string} pathname Path to item.
662 * @param {number} atime Access time (in seconds).
663 * @param {number} mtime Modification time (in seconds).
664 * @param {function(Error)} callback Optional callback.
665 */
666Binding.prototype.utimes = function(pathname, atime, mtime, callback) {
667 maybeCallback(callback, this, function() {
668 var item = this._system.getItem(pathname);
669 if (!item) {
670 throw new FSError('ENOENT', pathname);
671 }
672 item.setATime(new Date(atime * 1000));
673 item.setMTime(new Date(mtime * 1000));
674 });
675};
676
677
678/**
679 * Update timestamps.
680 * @param {number} fd File descriptor.
681 * @param {number} atime Access time (in seconds).
682 * @param {number} mtime Modification time (in seconds).
683 * @param {function(Error)} callback Optional callback.
684 */
685Binding.prototype.futimes = function(fd, atime, mtime, callback) {
686 maybeCallback(callback, this, function() {
687 var descriptor = this._getDescriptorById(fd);
688 var item = descriptor.getItem();
689 item.setATime(new Date(atime * 1000));
690 item.setMTime(new Date(mtime * 1000));
691 });
692};
693
694
695/**
696 * Synchronize in-core state with storage device.
697 * @param {number} fd File descriptor.
698 * @param {function(Error)} callback Optional callback.
699 */
700Binding.prototype.fsync = function(fd, callback) {
701 maybeCallback(callback, this, function() {
702 this._getDescriptorById(fd);
703 });
704};
705
706
707/**
708 * Synchronize in-core metadata state with storage device.
709 * @param {number} fd File descriptor.
710 * @param {function(Error)} callback Optional callback.
711 */
712Binding.prototype.fdatasync = function(fd, callback) {
713 maybeCallback(callback, this, function() {
714 this._getDescriptorById(fd);
715 });
716};
717
718
719/**
720 * Create a hard link.
721 * @param {string} srcPath The existing file.
722 * @param {string} destPath The new link to create.
723 * @param {function(Error)} callback Optional callback.
724 */
725Binding.prototype.link = function(srcPath, destPath, callback) {
726 maybeCallback(callback, this, function() {
727 var item = this._system.getItem(srcPath);
728 if (!item) {
729 throw new FSError('ENOENT', srcPath);
730 }
731 if (item instanceof Directory) {
732 throw new FSError('EPERM', srcPath);
733 }
734 if (this._system.getItem(destPath)) {
735 throw new FSError('EEXIST', destPath);
736 }
737 var parent = this._system.getItem(path.dirname(destPath));
738 if (!parent) {
739 throw new FSError('ENOENT', destPath);
740 }
741 if (!(parent instanceof Directory)) {
742 throw new FSError('ENOTDIR', destPath);
743 }
744 parent.addItem(path.basename(destPath), item);
745 });
746};
747
748
749/**
750 * Create a symbolic link.
751 * @param {string} srcPath Path from link to the source file.
752 * @param {string} destPath Path for the generated link.
753 * @param {string} type Ignored (used for Windows only).
754 * @param {function(Error)} callback Optional callback.
755 */
756Binding.prototype.symlink = function(srcPath, destPath, type, callback) {
757 maybeCallback(callback, this, function() {
758 if (this._system.getItem(destPath)) {
759 throw new FSError('EEXIST', destPath);
760 }
761 var parent = this._system.getItem(path.dirname(destPath));
762 if (!parent) {
763 throw new FSError('ENOENT', destPath);
764 }
765 if (!(parent instanceof Directory)) {
766 throw new FSError('ENOTDIR', destPath);
767 }
768 var link = new SymbolicLink();
769 link.setPath(srcPath);
770 parent.addItem(path.basename(destPath), link);
771 });
772};
773
774
775/**
776 * Read the contents of a symbolic link.
777 * @param {string} pathname Path to symbolic link.
778 * @param {function(Error, string)} callback Optional callback.
779 * @return {string} Symbolic link contents (path to source).
780 */
781Binding.prototype.readlink = function(pathname, callback) {
782 return maybeCallback(callback, this, function() {
783 var link = this._system.getItem(pathname);
784 if (!(link instanceof SymbolicLink)) {
785 throw new FSError('EINVAL', pathname);
786 }
787 return link.getPath();
788 });
789};
790
791
792/**
793 * Stat an item.
794 * @param {string} filepath Path.
795 * @param {function(Error, Stats)} callback Callback (optional).
796 * @return {Stats|undefined} Stats or undefined (if sync).
797 */
798Binding.prototype.lstat = function(filepath, callback) {
799 return maybeCallback(callback, this, function() {
800 var item = this._system.getItem(filepath);
801 if (!item) {
802 throw new FSError('ENOENT', filepath);
803 }
804 return new Stats(item.getStats());
805 });
806};
807
808
809/**
810 * Not yet implemented.
811 * @type {function()}
812 */
813Binding.prototype.StatWatcher = notImplemented;
814
815
816/**
817 * Export the binding constructor.
818 * @type {function()}
819 */
820exports = module.exports = Binding;