UNPKG

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