Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Consolidate Q-FS and Q-HTTP

  • Loading branch information...
commit 82f1a155f15b13c02c5861c8d4b34d4fa233a283 1 parent c582bcd
@kriskowal authored
View
5 .travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+ - 0.6
+ - 0.7
+ - 0.8
View
295 README.md
@@ -1,4 +1,299 @@
+[![Build Status](https://secure.travis-ci.org/kriskowal/q-io.png)](http://travis-ci.org/kriskowal/q-io)
+
+# Q-IO
+
+Interfaces for IO that make use of promises.
+
+Q-IO now subsumes all of [Q-HTTP][] and [Q-FS][].
+
+[Q-HTTP]: https://github.com/kriskowal/q-http
+[Q-FS]: https://github.com/kriskowal/q-fs
+
+## Filesystem
+
+File system API for Q promises with method signatures patterned after
+[CommonJS/Fileystem/A](http://wiki.commonjs.org/wiki/Filesystem/A) but
+returning promises and promise streams.
+
+### open(path, options)
+
+Options is an optional object.
+
+- ``flags``: ``r``, ``w``, ``a``, ``b``, default of `r`, not binary
+- ``charset``: default of ``utf-8``
+- ``bufferSize``: in bytes
+- ``mode``: UNIX permissions
+- ``begin`` first byte to read (defaults to zero)
+- ``end`` one past the last byte to read. ``end - begin == length``
+
+Open returns a promise for either a Reader or a Writer depending on the
+given flags.
+
+### read(path, options)
+
+### write(path, content, options)
+
+### append(path, content, options)
+
+### copy(source, target)
+
+### copyTree(source, target)
+
+### list(path)
+
+### listTree(path, guard(path, stat))
+
+### listDirectoryTree(path)
+
+### makeDirectory(path)
+
+### makeTree(path)
+
+### remove(path)
+
+### removeTree(path)
+
+### link(source, taget)
+
+### symbolicCopy(source, target)
+
+### symbolicLink(target, relative, type)
+
+### chown(path, uid, gid)
+
+### chmod(path, mode)
+
+### stat(path)
+
+### statLink(path)
+
+### statFd(fd)
+
+### exists(path)
+
+### isFile(path)
+
+### isDirectory(path)
+
+### lastModified(path)
+
+### split(path)
+
+### join(paths)
+
+### join(...paths)
+
+### resolve(...paths)
+
+### normal(...paths)
+
+### absolute(path)
+
+### canonical(path)
+
+### readLink(path)
+
+### contains(parent, child)
+
+### relative(source, target)
+
+### relativeFromFile(source, target)
+
+### relativeFromDirectory(source, target)
+
+### isAbsolute(path)
+
+### isRelative(path)
+
+### isRoot(path)
+
+### root(path)
+
+### directory(path)
+
+### base(path, extension)
+
+### extension(path)
+
+### reroot(path)
+
+### toObject(path)
+
+### glob(pattern)
+
+Not yet implemented
+
+### match(pattern, path)
+
+Not yet implemented
+
+## HTTP
+
+The HTTP module resembles [CommonJS/JSGI][].
+
+[CommonJS/JSGI]: http://wiki.commonjs.org/wiki/JSGI
+
+### Server(app)
+
+The `http` module exports a `Server` constructor.
+
+- accepts an application, returns a server.
+- calls the application function when requests are received.
+ - if the application returns a response object, sends that
+ response.
+- ``listen(port)``
+ - accepts a port number.
+ - returns a promise for undefined when the server has begun
+ listening.
+- ``stop()``
+ - returns a promise for undefined when the server has stopped.
+
+### request(request object or url)
+
+The `http` module exports a `request` function that returns a promise
+for a response.
+
+- accepts a request or a URL string.
+- returns a promise for a response.
+
+### read(request object or url)
+
+The `http` module exports a `read` function, analogous to
+`Fs.read(path)`, but returning a promise for the contento of an OK HTTP
+response.
+
+- accepts a request or a URL string.
+- returns a promise for the response body as a string provided
+ that the request is successful with a 200 status.
+ - rejects the promise with the response as the reason for
+ failure if the request fails.
+
+### normalizeRequest(request object or url)
+
+- coerces URLs into request objects.
+- completes an incomplete request object based on its `url`.
+
+### normalizeResponse(response)
+
+- coerces strings, arrays, and other objects supporting
+ ``forEach`` into proper response objects.
+- if it receives `undefined`, it returns `undefined`. This is used as
+ a singal to the requester that the responder has taken control of
+ the response stream.
+
+### request
+
+A complete request object has the following properties.
+
+- ``url`` the full URL of the request as a string
+- ``path`` the full path as a string
+- ``scriptName`` the routed portion of the path, like ``""`` for
+ ``http://example.com/`` if no routing has occurred.
+- ``pathInfo`` the part of the path that remains to be routed,
+ like ``/`` for ``http://example.com`` or ``http://example.com/``
+ if no routing has occurred.
+- ``version`` the requested HTTP version as an array of strings.
+- ``method`` like ``"GET"``
+- ``scheme`` like ``"http:"``
+- ``host`` like ``"example.com"``
+- ``port`` the port number, like ``80``
+- ``remoteHost``
+- ``remotePort``
+- ``headers``
+ corresponding values, possibly an array for multiple headers
+ of the same name.
+- ``body``
+- ``node`` the wrapped Node request object
+
+### response
+
+A complete response object has the following properties.
+
+- ``status`` the HTTP status code as a number, like ``200``.
+- ``headers``
+- ``body`` an IO reader
+- ``onclose`` is an optional function that this library will call
+ when a response concludes.
+- ``node`` the wrapped Node response object.
+
+### headers
+
+Headers are an object mapping lower-case header-names to corresponding
+values, possibly an array for multiple headers of the same name, for
+both requests and responses.
+
+### body
+
+body is a representation of a readable stream, either for the content of
+a request or a response. It is implemented as a Q-IO reader.
+
+- ``forEach(callback)``
+ - accepts a ``callback(chunk)`` function
+ - accepts a chunk as either a string or a ``Buffer``
+ - returns undefined or a promise for undefined when the
+ chunk has been flushed.
+ - returns undefined or a promise for undefined when the stream
+ is finished writing.
+ - the ``forEach`` function for arrays of strings or buffers is
+ sufficient for user-provided bodies
+- the ``forEach`` function is the only necessary function for
+ bodies provided to this library.
+- in addition to ``forEach``, bodies provided by this library
+ support the entire readable stream interface provided by
+ ``q-io``.
+- ``read()``
+ - returns a promise for the entire body as a string or a
+ buffer.
+
+### application
+
+An HTTP application is a function that accepts a request and returns a
+response. The `request` function itself is an application.
+Applications can be chained and combined to make advanced servers and
+clients.
+
+- accepts a request
+- returns a response, a promise for a response, or nothing if no
+ response should be sent.
+
+### Reader
+
+Reader instances have the following methods:
+
+- `read()`
+- `forEach(callback)`
+- `close()`
+- `node` the underlying node reader
+
+Additionally, the `Reader` constructor has the following methods:
+
+- `read(tream, charset)` accepts any foreachable and returns either a
+ buffer or a string if given a charset.
+- `join(buffers)` consolidates an array of buffers into a single
+ buffer. The buffers array is collapsed in place and the new first
+ and only buffer is returned.
+
+The `reader` module exports a function that accepts a Node reader and
+returns a Q reader.
+
+### Writer
+
+Writer instances have the following methods:
+
+- `write(content)` writes a chunk of content, either from a string or
+ a buffer.
+- `flush()` returns a promise to drain the outbound content all the
+ way to its destination.
+- `close()`
+- `destroy()`
+- `node` the underlying node writer
+
+The `writer` module exports a function that accepts a Node writer and
+returns a Q writer.
+
+---
+
Copyright 2009–2012 Kristopher Michael Kowal
MIT License (enclosed)
View
307 fs-boot.js
@@ -0,0 +1,307 @@
+(function (exports) {
+
+// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License
+// -- tlrobinson Tom Robinson TODO
+
+/**
+ * Pure JavaScript implementations of file system path
+ * manipulation.
+ */
+
+// NOTE: this file may be used is the engine bootstrapping
+// process, so any "requires" must be accounted for in
+// narwhal.js
+
+/*whatsupdoc*/
+/*markup markdown*/
+
+var regExpEscape = function (str) {
+ return str.replace(/[-[\]{}()*+?.\\^$|,#\s]/g, "\\$&");
+};
+
+var path = require("path");
+
+/**
+ * @name ROOT
+ * * `/` on Unix
+ * * `\` on Windows
+ */
+
+/**
+ * @name SEPARATOR
+ * * `/` on Unix
+ * * `\` on Windows
+ */
+
+/**
+ * @name ALT_SEPARATOR
+ * * undefined on Unix
+ * * `/` on Windows
+ */
+
+exports.ROOT = exports.SEPARATOR = path.sep;
+if (path.sep === "\\") {
+ exports.ALT_SEPARATOR = "/";
+} else {
+ exports.ALT_SEPARATOR = undefined;
+}
+
+// we need to make sure the separator regex is always in sync with the separators.
+// this caches the generated regex and rebuild if either separator changes.
+var separatorCached, altSeparatorCached, separatorReCached;
+/**
+ * @function
+ */
+exports.SEPARATORS_RE = function () {
+ if (
+ separatorCached !== exports.SEPARATOR ||
+ altSeparatorCached !== exports.ALT_SEPARATOR
+ ) {
+ separatorCached = exports.SEPARATOR;
+ altSeparatorCached = exports.ALT_SEPARATOR;
+ separatorReCached = new RegExp("[" +
+ (separatorCached || "").replace(/[-[\]{}()*+?.\\^$|,#\s]/g, "\\$&") +
+ (altSeparatorCached || "").replace(/[-[\]{}()*+?.\\^$|,#\s]/g, "\\$&") +
+ "]", "g");
+ }
+ return separatorReCached;
+}
+
+/**
+ * separates a path into components. If the path is
+ * absolute, the first path component is the root of the
+ * file system, indicated by an empty string on Unix, and a
+ * drive letter followed by a colon on Windows.
+ * @returns {Array * String}
+ */
+exports.split = function (path) {
+ var parts;
+ try {
+ parts = String(path).split(exports.SEPARATORS_RE());
+ } catch (exception) {
+ throw new Error("Cannot split " + (typeof path) + ", " + JSON.stringify(path));
+ }
+ // this special case helps isAbsolute
+ // distinguish an empty path from an absolute path
+ // "" -> [] NOT [""]
+ if (parts.length === 1 && parts[0] === "")
+ return [];
+ // "a" -> ["a"]
+ // "/a" -> ["", "a"]
+ return parts;
+};
+
+/**
+ * Takes file system paths as variadic arguments and treats
+ * each as a file or directory path and returns the path
+ * arrived by traversing into the those paths. All
+ * arguments except for the last must be paths to
+ * directories for the result to be meaningful.
+ * @returns {String} path
+ */
+exports.join = function () {
+ if (arguments.length === 1 && typeof arguments[0] === "object")
+ return exports.normal.apply(exports, arguments[0]);
+ return exports.normal.apply(exports, arguments);
+};
+
+/**
+ * Takes file system paths as variadic arguments and treats
+ * each path as a location, in the URL sense, resolving each
+ * new location based on the previous. For example, if the
+ * first argument is the absolute path of a JSON file, and
+ * the second argument is a path mentioned in that JSON
+ * file, `resolve` returns the absolute path of the
+ * mentioned file.
+ * @returns {String} path
+ */
+exports.resolve = function () {
+ var root = "";
+ var parents = [];
+ var children = [];
+ var leaf = "";
+ for (var i = 0; i < arguments.length; i++) {
+ var path = String(arguments[i]);
+ if (path == "")
+ continue;
+ var parts = path.split(exports.SEPARATORS_RE());
+ if (exports.isAbsolute(path)) {
+ root = parts.shift() + exports.SEPARATOR;
+ parents = [];
+ children = [];
+ }
+ leaf = parts.pop();
+ if (leaf == "." || leaf == "..") {
+ parts.push(leaf);
+ leaf = "";
+ }
+ for (var j = 0; j < parts.length; j++) {
+ var part = parts[j];
+ if (part == "." || part == "") {
+ } else if (part == "..") {
+ if (children.length) {
+ children.pop();
+ } else {
+ if (root) {
+ } else {
+ parents.push("..");
+ }
+ }
+ } else {
+ children.push(part);
+ }
+ };
+ }
+ path = parents.concat(children).join(exports.SEPARATOR);
+ if (path) leaf = exports.SEPARATOR + leaf;
+ return root + path + leaf;
+};
+
+/**
+ * Takes paths as any number of arguments and reduces them
+ * into a single path in normal form, removing all "." path
+ * components, and reducing ".." path components by removing
+ * the previous path component if possible.
+ * @returns {String} path
+ */
+exports.normal = function () {
+ var root = "";
+ var parents = [];
+ var children = [];
+ for (var i = 0, ii = arguments.length; i < ii; i++) {
+ var path = String(arguments[i]);
+ // empty paths have no affect
+ if (path === "")
+ continue;
+ var parts = path.split(exports.SEPARATORS_RE());
+ if (exports.isAbsolute(path)) {
+ root = parts.shift() + exports.SEPARATOR;
+ parents = [];
+ children = [];
+ }
+ for (var j = 0, jj = parts.length; j < jj; j++) {
+ var part = parts[j];
+ if (part == "." || part == "") {
+ } else if (part == "..") {
+ if (children.length) {
+ children.pop();
+ } else {
+ if (root) {
+ } else {
+ parents.push("..");
+ }
+ }
+ } else {
+ children.push(part);
+ }
+ }
+ }
+ path = parents.concat(children).join(exports.SEPARATOR);
+ return root + path;
+};
+
+/***
+ * @returns {Boolean} whether the given path begins at the
+ * root of the file system or a drive letter.
+ */
+exports.isAbsolute = function (path) {
+ // for absolute paths on any operating system,
+ // the first path component always determines
+ // whether it is relative or absolute. On Unix,
+ // it is empty, so ["", "foo"].join("/") == "/foo",
+ // "/foo".split("/") == ["", "foo"].
+ var parts = exports.split(path);
+ // split("") == []. "" is not absolute.
+ // split("/") == ["", ""] is absolute.
+ // split(?) == [""] does not occur.
+ if (parts.length == 0)
+ return false;
+ return exports.isRoot(parts[0]);
+};
+
+/**
+ * @returns {Boolean} whether the given path does not begin
+ * at the root of the file system or a drive letter.
+ */
+exports.isRelative = function (path) {
+ return !exports.isAbsolute(path);
+};
+
+/**
+ * @returns {Boolean} whether the given path component
+ * corresponds to the root of the file system or a drive
+ * letter, as applicable.
+ */
+exports.isRoot = function (first) {
+ if (exports.SEPARATOR === "\\") {
+ return /[a-zA-Z]:$/.test(first);
+ } else {
+ return first == "";
+ }
+};
+
+/**
+ * @returns {String} the Unix root path or corresponding
+ * Windows drive for a given path.
+ */
+exports.root = function (path) {
+ if (!exports.isAbsolute(path))
+ path = require("./fs").absolute(path);
+ var parts = exports.split(path);
+ return exports.join(parts[0], "");
+};
+
+/**
+ * @returns {String} the parent directory of the given path.
+ */
+exports.directory = function (path) {
+ path = exports.normal(path);
+ var absolute = exports.isAbsolute(path);
+ var parts = exports.split(path);
+ // XXX needs to be sensitive to the root for
+ // Windows compatibility
+ if (parts.length) {
+ if (parts[parts.length - 1] == "..") {
+ parts.push("..");
+ } else {
+ parts.pop();
+ }
+ } else {
+ parts.unshift("..");
+ }
+ return parts.join(exports.SEPARATOR) || (
+ exports.isRelative(path) ?
+ "" : exports.ROOT
+ );
+};
+
+/**
+ * @returns {String} the last component of a path, without
+ * the given extension if the extension is provided and
+ * matches the given file.
+ * @param {String} path
+ * @param {String} extention an optional extention to detect
+ * and remove if it exists.
+ */
+exports.base = function (path, extension) {
+ var base = path.split(exports.SEPARATORS_RE()).pop();
+ if (extension)
+ base = base.replace(
+ new RegExp(regExpEscape(extension) + "$"),
+ ""
+ );
+ return base;
+};
+
+/**
+ * @returns {String} the extension (e.g., `txt`) of the file
+ * at the given path.
+ */
+exports.extension = function (path) {
+ path = exports.base(path);
+ path = path.replace(/^\.*/, "");
+ var index = path.lastIndexOf(".");
+ return index <= 0 ? "" : path.substring(index);
+};
+
+})(typeof exports !== "undefined" ? exports : FS_BOOT = {});
View
414 fs-common.js
@@ -0,0 +1,414 @@
+
+var Q = require("q");
+var Boot = require("./fs-boot");
+var RootFs = require("./fs-root");
+var MockFs = require("./fs-mock");
+
+// TODO patternToRegExp
+// TODO glob
+// TODO match
+// TODO copyTree
+
+var concat = function (arrays) {
+ return Array.prototype.concat.apply([], arrays);
+};
+
+exports.update = function (exports, workingDirectory) {
+
+ for (var name in Boot) {
+ exports[name] = Boot[name];
+ }
+
+ /**
+ * @param {String} path
+ * @param {Object} options
+ * @returns {Promise * (String || Buffer)}
+ */
+ exports.read = function (path, flags, charset, options) {
+ if (typeof flags == "object") {
+ options = flags;
+ } else {
+ options = options || {};
+ options.flags = flags;
+ options.carset = charset;
+ }
+ options.flags = "r" + (options.flags || "").replace(/r/g, "");
+ return Q.when(this.open(path, options), function (stream) {
+ return stream.read();
+ }, function (reason) {
+ var error = new Error("Can't read " + path + " because " + reason.message);
+ error.path = path;
+ error.flags = flags;
+ error.charset = charset;
+ error.cause = reason;
+ throw error;
+ });
+ };
+
+ /**
+ * @param {String} path
+ * @param {String || Buffer} content
+ * @param {Object} options
+ * @returns {Promise * Undefined} a promise that resolves
+ * when the writing is complete.
+ */
+ exports.write = function (path, content, flags, charset, options) {
+ var self = this;
+ if (typeof flags == "object") {
+ options = flags;
+ } else {
+ options = options || {};
+ options.flags = flags;
+ options.carset = charset;
+ }
+ options.flags = "w" + (options.flags || "").replace(/w/g, "");
+ return Q.when(self.open(path, options), function (stream) {
+ return Q.when(stream.write(content), function () {
+ return stream.close();
+ });
+ });
+ };
+
+ exports.append = function (path, content, flags, charset, options) {
+ var self = this;
+ if (typeof flags == "object") {
+ options = flags;
+ } else {
+ options = options || {};
+ options.flags = flags;
+ options.carset = charset;
+ }
+ options.flags = "w+" + (options.flags || "").replace(/[w\+]/g, "");
+ return Q.when(self.open(path, options), function (stream) {
+ return Q.when(stream.write(content), function () {
+ return stream.close();
+ });
+ });
+ };
+
+ exports.copy = function (source, target) {
+ var self = this;
+ return Q.spread([
+ self.open(source, {flags: "rb"}),
+ self.open(target, {flags: "wb"})
+ ], function (reader, writer) {
+ return Q.when(reader.forEach(function (block) {
+ return writer.write(block);
+ }), function () {
+ return Q.all([
+ reader.close && reader.close(), // TODO fix q-io
+ writer.close()
+ ]);
+ });
+ });
+ };
+
+ exports.copyTree = function (source, target) {
+ var self = this;
+ return Q.when(exports.stat(source), function (stat) {
+ if (stat.isFile()) {
+ return exports.copy(source, target);
+ } else if (stat.isDirectory()) {
+ return Q.when(exports.makeDirectory(target), function () {
+ return Q.when(exports.list(source), function (list) {
+ return Q.all(list.map(function (child) {
+ return exports.copyTree(
+ exports.join(source, child),
+ exports.join(target, child)
+ );
+ }));
+ });
+ });
+ } else if (stat.isSymbolicLink()) {
+ return exports.symbolicCopy(source, target);
+ }
+ });
+ };
+
+ exports.listTree = function (basePath, guard) {
+ var self = this;
+ basePath = String(basePath || '');
+ if (!basePath)
+ basePath = ".";
+ guard = guard || function () {
+ return true;
+ };
+ var stat = self.stat(basePath);
+ return Q.when(stat, function (stat) {
+ var paths = [];
+ var mode; // true:include, false:exclude, null:no-recur
+ try {
+ var include = guard(basePath, stat);
+ } catch (exception) {
+ return Q.reject(exception);
+ }
+ return Q.when(include, function (include) {
+ if (include) {
+ paths.push([basePath]);
+ }
+ if (include !== null && stat.isDirectory()) {
+ return Q.when(self.list(basePath), function (children) {
+ paths.push.apply(paths, children.map(function (child) {
+ var path = self.join(basePath, child);
+ return self.listTree(path, guard);
+ }));
+ return paths;
+ });
+ } else {
+ return paths;
+ }
+ });
+ }, function noSuchFile(reason) {
+ return [];
+ }).then(Q.all).then(concat);
+ };
+
+ exports.listDirectoryTree = function (path) {
+ return this.listTree(path, function (path, stat) {
+ return stat.isDirectory();
+ });
+ };
+
+ exports.makeTree = function (path, mode) {
+ var self = this;
+ var parts = self.split(path);
+ var at = [];
+ if (self.isAbsolute(path))
+ at.push(self.ROOT);
+ return parts.reduce(function (parent, part) {
+ return Q.when(parent, function () {
+ at.push(part);
+ var parts = self.join(at);
+ var made = self.makeDirectory(parts, mode);
+ return Q.when(made, null, function rejected(reason) {
+ // throw away errors for already made directories
+ if (reason.code == "EEXIST" || reason.code == "EISDIR") {
+ return;
+ } else {
+ return Q.reject(reason);
+ }
+ });
+ });
+ }, undefined);
+ };
+
+ exports.removeTree = function (path) {
+ var self = this;
+ return Q.when(self.stat(path), function (stat) {
+ if (stat.isLink()) {
+ return self.remove(path);
+ } else if (stat.isDirectory()) {
+ var list = self.list(path);
+ return Q.when(list, function (list) {
+ // asynchronously remove every subtree
+ var done = list.reduce(function (prev, name) {
+ var child = self.join(path, name);
+ var next = self.removeTree(child);
+ // join next and prev
+ return Q.when(prev, function () {
+ return next;
+ });
+ });
+ return Q.when(done, function () {
+ self.removeDirectory(path);
+ });
+ });
+ } else {
+ return self.remove(path);
+ }
+ });
+ };
+
+ exports.exists = function (path) {
+ return Q.when(this.stat(path), function () {
+ return true;
+ }, function () {
+ return false;
+ });
+ };
+
+ exports.isFile = function (path) {
+ return Q.when(this.stat(path), function (stat) {
+ return stat.isFile();
+ }, function (reason) {
+ return false;
+ });
+ };
+
+ exports.isDirectory = function (path) {
+ return Q.when(this.stat(path), function (stat) {
+ return stat.isDirectory();
+ }, function (reason) {
+ return false;
+ });
+ };
+
+ exports.absolute = function (path) {
+ if (this.isAbsolute(path))
+ return path;
+ return this.join(workingDirectory(), path);
+ };
+
+ exports.relative = function (source, target) {
+ var self = this;
+ return Q.when(this.isDirectory(source), function (isDirectory) {
+ if (isDirectory) {
+ return self.relativeFromDirectory(source, target);
+ } else {
+ return self.relativeFromFile(source, target);
+ }
+ });
+ };
+
+ exports.relativeFromFile = function (source, target) {
+ var self = this;
+ source = self.absolute(source);
+ target = self.absolute(target);
+ source = source.split(self.SEPARATORS_RE());
+ target = target.split(self.SEPARATORS_RE());
+ source.pop();
+ while (
+ source.length &&
+ target.length &&
+ target[0] == source[0]
+ ) {
+ source.shift();
+ target.shift();
+ }
+ while (source.length) {
+ source.shift();
+ target.unshift("..");
+ }
+ return target.join(self.SEPARATOR);
+ };
+
+ exports.relativeFromDirectory = function (source, target) {
+ if (!target) {
+ target = source;
+ source = workingDirectory();
+ }
+ source = this.absolute(source);
+ target = this.absolute(target);
+ source = source.split(this.SEPARATORS_RE());
+ target = target.split(this.SEPARATORS_RE());
+ if (source.length === 2 && source[1] === "")
+ source.pop();
+ while (
+ source.length &&
+ target.length &&
+ target[0] == source[0]
+ ) {
+ source.shift();
+ target.shift();
+ }
+ while (source.length) {
+ source.shift();
+ target.unshift("..");
+ }
+ return target.join(this.SEPARATOR);
+ };
+
+ exports.contains = function (parent, child) {
+ var i, ii;
+ parent = this.absolute(parent);
+ child = this.absolute(child);
+ parent = parent.split(this.SEPARATORS_RE());
+ child = child.split(this.SEPARATORS_RE());
+ if (parent.length === 2 && parent[1] === "")
+ parent.pop();
+ if (parent.length > child.length)
+ return false;
+ for (i = 0, ii = parent.length; i < ii; i++) {
+ if (parent[i] !== child[i])
+ break;
+ }
+ return i == ii;
+ };
+
+ exports.reroot = reroot;
+ function reroot(path) {
+ var self = this;
+ path = path || this.ROOT;
+ return Q.when(this.list(path), function (list) {
+ if (list.length !== 1)
+ return RootFs(self, path);
+ var nextPath = self.join(path, list[0]);
+ return Q.when(self.stat(nextPath), function (stat) {
+ if (stat.isDirectory()) {
+ return reroot(nextPath);
+ } else {
+ return RootFs(self, path);
+ }
+ });
+ });
+ }
+
+ exports.toObject = function (path) {
+ var self = this;
+ var list = self.listTree(path || "", function (path, stat) {
+ return stat.isFile();
+ });
+ return Q.when(list, function (list) {
+ var tree = {};
+ return Q.all(list.map(function (path) {
+ return Q.when(self.read(path, "rb"), function (content) {
+ tree[path] = content;
+ });
+ })).then(function () {
+ return tree;
+ });
+ });
+ };
+
+ exports.merge = function (fss) {
+ var tree = {};
+ var done;
+ fss.forEach(function (fs) {
+ done = Q.when(done, function () {
+ return fs.listTree("", function (path, stat) {
+ return stat.isFile();
+ })
+ .then(function (list) {
+ return Q.all(list.map(function (path) {
+ return Q.when(fs.read(path, "rb"), function (content) {
+ tree[path] = content;
+ });
+ }));
+ });
+ });
+ })
+ return Q.when(done, function () {
+ return MockFs(tree);
+ });
+ };
+
+ var Stats = exports.Stats = function (nodeStat) {
+ this.node = nodeStat;
+ };
+
+ var stats = [
+ "isDirectory",
+ "isFile",
+ "isBlockDevice",
+ "isCharacterDevice",
+ "isSymbolicLink",
+ "isFIFO",
+ "isSocket"
+ ];
+
+ stats.forEach(function (name) {
+ Stats.prototype[name] = function () {
+ return this.node[name]();
+ };
+ });
+
+ Stats.prototype.lastModified = function () {
+ return Date.parse(this.node.mtime);
+ };
+
+ Stats.prototype.lastAccessed = function () {
+ return Date.parse(this.node.atime);
+ };
+
+}
+
View
203 fs-mock.js
@@ -0,0 +1,203 @@
+(function (require, exports) {
+
+var Q = require("q");
+var BOOT = require("./fs-boot");
+var FS = require("./fs");
+var COMMON = require("./fs-common");
+
+module.exports = MockFs;
+
+function MockFs(files) {
+ var fs = Object.create(BOOT);
+ var root = {};
+ var now = new Date();
+
+ function init() {
+ // construct a file tree
+ Object.keys(files).forEach(function (path) {
+ var content = files[path];
+ find(root, path).set(content);
+ });
+ }
+
+ function find(at, path) {
+ path = fs.absolute(path);
+ if (path === "" || path === FS.ROOT) {
+ return Node(function get() {
+ return root;
+ }, function set(content) {
+ root = content;
+ });
+ }
+ var parts = FS.split(path);
+ var empty = parts.shift();
+ if (empty !== "")
+ throw new Error("assertion: first component of root should be empty");
+ var i, ii;
+ var manifest = function () {};
+ for (i = 0, ii = parts.length - 1; i < ii; i++) {
+ var part = parts[i];
+ if (part === ".") {
+ continue;
+ } if (typeof at[part] !== "object") {
+ manifest = (function (on, part, manifest) {
+ var created = {};
+ at = created;
+ return function () {
+ on[part] = created;
+ manifest();
+ };
+ })(at, part, manifest);
+ } else {
+ at = at[part];
+ }
+ }
+ var leaf = parts[i];
+ return Node(function get() {
+ return at[leaf];
+ }, function set(content) {
+ manifest();
+ at[leaf] = content;
+ });
+ }
+
+ fs.list = function (path) {
+ path = String(path);
+ return Q.when(fs.stat(path), function (stat) {
+ if (!stat.isDirectory())
+ throw new Error("Can't list non-directory " + path);
+ var node = find(root, path).get();
+ return Object.keys(node);
+ });
+ };
+
+ fs.open = function (path, flags, charset, options) {
+ if (typeof flags == "object") {
+ options = flags;
+ flags = options.flags;
+ charset = options.charset;
+ } else {
+ options = options || {};
+ }
+ var node = find(root, path);
+ // TODO create an actual open file object, rather
+ // than this rather primitive duck
+ flags = flags || "r";
+ var binary = flags.indexOf("b") >= 0;
+ charset = charset || "utf-8";
+ if (flags.indexOf("w") === -1) {
+ return fs.stat(path).post("isFile")
+ .then(function (isFile) {
+ if (!isFile) {
+ throw new Error("Can't open non-file " + path);
+ }
+ return {
+ "read": function () {
+ var content = node.get();
+ if (!binary)
+ content = content.toString(charset);
+ return content;
+ }
+ };
+ });
+ } else {
+ throw new Error("Can't open files for writing in read-only mock file system");
+ }
+ };
+
+ fs.stat = function (path) {
+ var stat = find(root, path);
+ if (stat.get() === undefined)
+ return Q.reject(new Error("No such file: " + path));
+ return Q.resolve(stat);
+ };
+
+ fs.getNode = function (path) {
+ path = path || "";
+ return find(root, path).get();
+ };
+
+ fs.canonical = function (path) {
+ return fs.normal(path);
+ };
+
+ var Node = function (get, set) {
+ var self = Object.create(Node.prototype);
+ self.get = get;
+ self.set = set;
+ return self;
+ };
+
+ Node.prototype = Object.create(GenericNode.prototype);
+
+ Node.prototype.constructor = Node;
+
+ Node.prototype.lastModified = function () {
+ return now;
+ };
+
+ COMMON.update(fs, function () {
+ return fs.ROOT;
+ });
+
+ init();
+
+ return fs;
+}
+
+MockFs.mock = mock;
+function mock(fs, root) {
+ return Q.when(fs.listTree(root), function (list) {
+ var tree = {};
+ return Q.all(list.map(function (path) {
+ var actual = fs.join(root, path);
+ var relative = fs.relativeFromDirectory(root, actual);
+ return Q.when(fs.stat(actual), function (stat) {
+ if (stat.isFile()) {
+ return Q.when(fs.read(path, "rb"), function (content) {
+ tree[relative] = content;
+ });
+ }
+ });
+ })).then(function () {
+ return MockFs(tree);
+ });
+ });
+}
+
+var GenericNode = function () {};
+
+GenericNode.prototype.exists = function () {
+ var node = this.get();
+ return typeof node !== "undefined";
+};
+
+GenericNode.prototype.isFile = function () {
+ var node = this.get();
+ return typeof node !== "undefined" && (
+ typeof node !== "object" ||
+ node.constructor !== Object
+ );
+};
+
+GenericNode.prototype.isDirectory = function () {
+ var node = this.get();
+ return (
+ typeof node === "object" &&
+ node.constructor === Object
+ )
+};
+
+}).apply(null, typeof exports !== "undefined" ?
+ [require, exports] :
+ [
+ function (id) {
+ id = id.toUpperCase()
+ .replace(".", "Q_FS")
+ .replace("/", "$")
+ .replace("-", "_");
+ return window[id];
+ },
+ Q_FS$MOCK
+ ]
+)
View
93 fs-root.js
@@ -0,0 +1,93 @@
+
+var Q = require("q");
+var BOOT = require("./fs-boot");
+var COMMON = require("./fs-common");
+
+module.exports = RootFs;
+
+function RootFs(outer, root) {
+ var inner = Object.create(BOOT);
+
+ function attenuate(path) {
+
+ // the machinations of projecting a path inside a
+ // subroot
+ var actual;
+ // if it's absolute, we want the path relative to
+ // the root of the inner file system
+ if (outer.isAbsolute(path)) {
+ actual = outer.relativeFromDirectory(outer.ROOT, path);
+ } else {
+ actual = path;
+ }
+ // we join the path onto the root of the inner file
+ // system so that parent references from the root
+ // return to the root, emulating standard unix
+ // behavior
+ actual = outer.join(outer.ROOT, actual);
+ // then we reconstruct the path relative to the
+ // inner root
+ actual = outer.relativeFromDirectory(outer.ROOT, actual);
+ // and rejoin it on the outer root
+ actual = outer.join(root, actual);
+ // and find the corresponding real path
+ actual = outer.canonical(actual);
+ return Q.when(actual, function (actual) {
+ // and verify that the outer canonical path is
+ // actually inside the inner canonical path, to
+ // prevent break-outs
+ if (outer.contains(root, actual)) {
+ return {
+ "inner": outer.join(outer.ROOT, outer.relativeFromDirectory(root, actual)),
+ "outer": actual
+ };
+ } else {
+ return Q.reject("No such file: " + JSON.stringify(path));
+ }
+ });
+ }
+
+ function workingDirectory() {
+ return outer.ROOT;
+ }
+
+ COMMON.update(inner, workingDirectory);
+
+ inner.list = function (path) {
+ return Q.when(attenuate(path), function (path) {
+ return outer.list(path.outer);
+ }).then(null, function (reason) {
+ return Q.reject("Can't list " + JSON.stringify(path));
+ });
+ };
+
+ inner.open = function (path, flags, charset) {
+ return Q.when(attenuate(path), function (path) {
+ return outer.open(path.outer, flags, charset);
+ }).then(null, function (reason) {
+ return Q.reject("Can't open " + JSON.stringify(path));
+ });
+ };
+
+ inner.stat = function (path) {
+ return Q.when(attenuate(path), function (path) {
+ return outer.stat(path.outer);
+ }).then(null, function (reason) {
+ return Q.reject("Can't stat " + JSON.stringify(path));
+ });
+ };
+
+ inner.canonical = function (path) {
+ return Q.when(attenuate(path), function (path) {
+ return path.inner;
+ }).then(null, function (reason) {
+ return Q.reject("Can't find canonical of " + JSON.stringify(path));
+ });
+ };
+
+ return Q.when(outer.canonical(root), function (_root) {
+ root = _root;
+ return inner;
+ });
+}
+
View
324 fs.js
@@ -0,0 +1,324 @@
+/**
+ * An asynchronous local file system API, based on a subset
+ * of the `narwhal/fs` API and the `narwhal/promise` API,
+ * such that the method names are the same but some return
+ * values are promises instead of fully resolved values.
+ * @module
+ */
+
+/*whatsupdoc*/
+
+var FS = require("fs"); // node
+var Q = require("q");
+var Reader = require("./reader");
+var Writer = require("./writer");
+var Common = require("./fs-common");
+var Mock = require("./fs-mock");
+var Root = require("./fs-root");
+
+Common.update(exports, process.cwd);
+exports.Mock = Mock;
+exports.mock = Mock.mock;
+exports.Root = Root;
+
+// facilitates AIMD (additive increase, multiplicative decrease) for backing off
+var backOffDelay = 0;
+var backOffFactor = 1.0001;
+function dampen(wrapped, thisp) {
+ var retry = function () {
+ var args = arguments;
+ var ready = backOffDelay ? Q.delay(backOffDelay) : Q.resolve();
+ return ready.then(function () {
+ return Q.when(wrapped.apply(thisp, args), function (stream) {
+ backOffDelay = Math.max(0, backOffDelay - 1);
+ return stream;
+ }, function (error) {
+ if (error.code === "EMFILE") {
+ backOffDelay = (backOffDelay + 1) * backOffFactor;
+ return retry.apply(null, args);
+ } else {
+ throw error;
+ }
+ });
+ });
+ };
+ return retry;
+}
+
+/**
+ * @param {String} path
+ * @param {Object} options (flags, mode, bufferSize, charset, begin, end)
+ * @returns {Promise * Stream} a stream from the `q-io` module.
+ */
+exports.open = dampen(function (path, flags, charset, options) {
+ var self = this;
+ if (typeof flags == "object") {
+ options = flags;
+ flags = options.flags;
+ charset = options.charset;
+ }
+ options = options || {};
+ flags = flags || "r";
+ var nodeOptions = {
+ "flags": flags.replace(/b/g, "")
+ };
+ if ("bufferSize" in options) {
+ nodeOptions.bufferSize = options.bufferSize;
+ }
+ if ("mode" in options) {
+ nodeOptions.mode = options.mode;
+ }
+ if ("begin" in options) {
+ nodeOptions.start = options.begin;
+ nodeOptions.end = options.end - 1;
+ }
+ if (flags.indexOf("b") >= 0) {
+ if (charset) {
+ throw new Error("Can't open a binary file with a charset: " + charset);
+ }
+ }
+ if (flags.indexOf("w") >= 0) {
+ var stream = FS.createWriteStream(String(path), nodeOptions);
+ return Writer(stream, charset);
+ } else {
+ var stream = FS.createReadStream(String(path), nodeOptions);
+ return Reader(stream, charset);
+ }
+});
+
+exports.remove = function (path) {
+ path = String(path);
+ var done = Q.defer();
+ FS.unlink(path, function (error) {
+ if (error) {
+ error.message = "Can't remove " + JSON.stringify(path) + ": " + error.message;
+ done.reject(error);
+ } else {
+ done.resolve();
+ }
+ });
+ return done.promise;
+};
+
+exports.makeDirectory = function (path, mode) {
+ path = String(path);
+ var done = Q.defer();
+ mode = mode === undefined ? parseInt('755', 8) : mode;
+ FS.mkdir(path, mode, function (error) {
+ if (error) {
+ error.message = "Can't makeDirectory " + JSON.stringify(path) + " with mode " + mode + ": " + error.message;
+ done.reject(error);
+ } else {
+ done.resolve();
+ }
+ });
+ return done.promise;
+};
+
+exports.removeDirectory = function (path) {
+ path = String(path);
+ var done = Q.defer();
+ FS.rmdir(path, function (error) {
+ if (error) {
+ error.message = "Can't removeDirectory " + JSON.stringify(path) + ": " + error.message;
+ done.reject(error);
+ } else {
+ done.resolve();
+ }
+ });
+ return done.promise;
+};
+
+/**
+ */
+exports.list = dampen(function (path) {
+ path = String(path);
+ var result = Q.defer();
+ FS.readdir(path, function (error, list) {
+ if (error) {
+ error.message = "Can't list " + JSON.stringify(path) + ": " + error.message;
+ return result.reject(error);
+ } else {
+ result.resolve(list);
+ }
+ });
+ return result.promise;
+});
+
+/**
+ * @param {String} path
+ * @returns {Promise * Stat}
+ */
+exports.stat = function (path) {
+ var self = this;
+ path = String(path);
+ var done = Q.defer();
+ try {
+ FS.stat(path, function (error, stat) {
+ if (error) {
+ error.message = "Can't stat " + JSON.stringify(path) + ": " + error;
+ done.reject(error);
+ } else {
+ done.resolve(new self.Stats(stat));
+ }
+ });
+ } catch (error) {
+ done.reject(error);
+ }
+ return done.promise;
+};
+
+exports.statLink = function (path) {
+ path = String(path);
+ var done = Q.defer();
+ try {
+ FS.lstat(path, function (error, stat) {
+ if (error) {
+ error.message = "Can't statLink " + JSON.stringify(path) + ": " + error.message;
+ done.reject(error);
+ } else {
+ done.resolve(stat);
+ }
+ });
+ } catch (error) {
+ done.reject(error);
+ }
+ return done.promise;
+};
+
+exports.statFd = function (fd) {
+ fd = Number(fd);
+ var done = Q.defer();
+ try {
+ FS.fstat(fd, function (error, stat) {
+ if (error) {
+ error.message = "Can't statFd file descriptor " + JSON.stringify(fd) + ": " + error.message;
+ done.reject(error);
+ } else {
+ done.resolve(stat);
+ }
+ });
+ } catch (error) {
+ done.reject(error);
+ }
+ return done.promise;
+};
+
+exports.link = function (source, target) {
+ source = String(source);
+ target = String(target);
+ var done = Q.defer();
+ try {
+ FS.link(source, target, function (error) {
+ if (error) {
+ error.message = "Can't link " + JSON.stringify(source) + " to " + JSON.stringify(target) + ": " + error.message;
+ done.reject(error);
+ } else {
+ done.resolve();
+ }
+ });
+ } catch (error) {
+ done.reject(error);
+ }
+ return done.promise;
+};
+
+exports.symbolicLink = function (target, relative, type) {
+ if (!type) {
+ console.warn(new Error("For Windows compatibility, symbolicLink must be called with a type argument 'file', 'directory', or 'junction'"));
+ }
+ target = String(target);
+ relative = String(relative);
+ var done = Q.defer();
+ try {
+ FS.symlink(relative, target, type || 'file', function (error) {
+ if (error) {
+ error.message = "Can't create symbolicLink " + JSON.stringify(target) + " to relative location " + JSON.stringify(relative) + ": " + error.message;
+ done.reject(error);
+ } else {
+ done.resolve();
+ }
+ });
+ } catch (error) {
+ done.reject(error);
+ }
+ return done.promise;
+};
+
+exports.symbolicCopy = function (source, target) {
+ return Q.when(exports.relative(target, source), function (relative) {
+ return exports.symbolicLink(target, relative, "file");
+ });
+};
+
+exports.chown = function (path, uid, gid) {
+ path = String(path);
+ var done = Q.defer();
+ try {
+ FS.chown(path, uid, gid, function (error) {
+ if (error) {
+ error.message = "Can't chown (change owner) of " + JSON.stringify(path) + " to user " + JSON.stringify(uid) + " and group " + JSON.stringify(gid) + ": " + error.message;
+ done.reject(error);
+ } else {
+ done.resolve();
+ }
+ });
+ } catch (error) {
+ done.reject(error);
+ }
+ return done.promise;
+};
+
+exports.chmod = function (path, mode) {
+ path = String(path);
+ mode = String(mode);
+ var done = Q.defer();
+ try {
+ FS.chmod(path, mode, function (error) {
+ if (error) {
+ error.message = "Can't chmod (change permissions mode) of " + JSON.stringify(path) + " to (octal number) " + mode.toString(8) + ": " + error.message;
+ done.reject(error);
+ } else {
+ done.resolve();
+ }
+ });
+ } catch (error) {
+ done.reject(error);
+ }
+ return done.promise;
+};
+
+exports.lastModified = function (path) {
+ return exports.stat(path).get('mtime').then(Date.parse);
+};
+
+exports.lastAccessed = function (path) {
+ return exports.stat(path).get('atime').then(Date.parse);
+};
+
+exports.canonical = function (path) {
+ var result = Q.defer();
+ FS.realpath(path, function (error, canonicalPath) {
+ if (error) {
+ error.message = "Can't get canonical path of " + JSON.stringify(path) + " by way of C realpath: " + error.message;
+ result.reject(error);
+ } else {
+ result.resolve(canonicalPath);
+ }
+ });
+ return result.promise;
+};
+
+exports.readLink = function (path) {
+ var result = Q.defer();
+ FS.readlink(path, function (error, path) {
+ if (error) {
+ error.message = "Can't get link from " + JSON.stringify(path) + " by way of C readlink: " + error.message;
+ result.reject(error);
+ } else {
+ result.resolve(path);
+ }
+ });
+ return result.promise;
+};
+
View
367 http.js
@@ -0,0 +1,367 @@
+/**
+ * A promise-based Q-JSGI server and client API.
+ * @module
+ */
+
+/*whatsupdoc*/
+
+var HTTP = require("http"); // node
+var HTTPS = require("https"); // node
+var URL = require("url"); // node
+var Q = require("q");
+var Reader = require("./reader");
+
+/**
+ * @param {respond(request Request)} respond a JSGI responder function that
+ * receives a Request object as its argument. The JSGI responder promises to
+ * return an object of the form `{status, headers, body}`. The status and
+ * headers must be fully resolved, but the body may be a promise for an object
+ * with a `forEach(write(chunk String))` method, albeit an array of strings.
+ * The `forEach` method may promise to resolve when all chunks have been
+ * written.
+ * @returns a Node Server object.
+ */
+exports.Server = function (respond) {
+ var self = Object.create(exports.Server.prototype);
+
+ var server = HTTP.createServer(function (_request, _response) {
+ var request = exports.ServerRequest(_request);
+ var response = exports.ServerResponse(_response);
+
+ var closed = Q.defer();
+ _request.on("end", function (error, value) {
+ if (error) {
+ closed.reject(error);
+ } else {
+ closed.resolve(value);
+ }
+ });
+
+ Q.when(request, function (request) {
+ return Q.when(respond(request, response), function (response) {
+ if (!response)
+ return;
+
+ _response.writeHead(response.status, response.headers);
+
+ if (response.onclose || response.onClose)
+ Q.when(closed, response.onclose || response.onClose);
+
+ return Q.when(response.body, function (body) {
+ var length;
+ if (
+ Array.isArray(body) &&
+ (length = body.length) &&
+ body.every(function (chunk) {
+ return typeof chunk === "string"
+ })
+ ) {
+ body.forEach(function (chunk, i) {
+ if (i < length - 1) {
+ _response.write(chunk, response.charset);
+ } else {
+ _response.end(chunk, response.charset);
+ }
+ });
+ } else if (body) {
+ var end;
+ var done = body.forEach(function (chunk) {
+ end = Q.when(end, function () {
+ return Q.when(chunk, function (chunk) {
+ _response.write(chunk, response.charset);
+ });
+ });
+ });
+ return Q.when(done, function () {
+ return Q.when(end, function () {
+ _response.end();
+ });
+ });
+ } else {
+ _response.end();
+ }
+ });
+
+ })
+ })
+ .done(); // should be .fail(self.emitter("error"))
+
+ });
+
+ var stopped = Q.defer();
+ server.on("close", function (err) {
+ if (err) {
+ stopped.reject(err);
+ } else {
+ stopped.resolve();
+ }
+ });
+
+ /***
+ * Stops the server.
+ * @returns {Promise * Undefined} a promise that will
+ * resolve when the server is stopped.
+ */
+ self.stop = function () {
+ server.close();
+ listening = undefined;
+ return stopped.promise;
+ };
+
+ var listening = Q.defer();
+ server.on("listening", function (err) {
+ if (err) {
+ listening.reject(err);
+ } else {
+ listening.resolve(self);
+ }
+ });
+
+ /***
+ * Starts the server, listening on the given port
+ * @param {Number} port
+ * @returns {Promise * Undefined} a promise that will
+ * resolve when the server is ready to receive
+ * connections
+ */
+ self.listen = function (/*...args*/) {
+ if (typeof server.port !== "undefined")
+ return Q.reject(new Error("A server cannot be restarted or " +
+ "started on a new port"));
+ server.listen.apply(server, arguments);
+ return listening.promise;
+ };
+
+ self.stopped = stopped.promise;
+
+ self.node = server;
+ self.nodeServer = server; // Deprecated
+
+ return self;
+};
+
+Object.defineProperties(exports.Server, {
+
+ port: {
+ get: function () {
+ return this.node.port;
+ }
+ },
+
+ host: {
+ get: function () {
+ return this.node.host;
+ }
+ }
+
+});
+
+/**
+ * A wrapper for a Node HTTP Request, as received by
+ * the Q HTTP Server, suitable for use by the Q HTTP Client.
+ */
+exports.ServerRequest = function (_request) {
+ var request = Object.create(_request);
+ /*** {Array} HTTP version. (JSGI) */
+ request.version = _request.httpVersion.split(".").map(Math.floor);
+ /*** {String} HTTP method, e.g., `"GET"` (JSGI) */
+ request.method = _request.method;
+ /*** {String} path, starting with `"/"` */
+ request.path = _request.url;
+ /*** {String} pathInfo, starting with `"/"`, the
+ * portion of the path that has not yet
+ * been routed (JSGI) */
+ request.pathInfo = URL.parse(_request.url).pathname;
+ /*** {String} scriptName, the portion of the path that
+ * has already been routed (JSGI) */
+ request.scriptName = "";
+ /*** {String} (JSGI) */
+ request.scheme = "http";
+
+ if (_request.headers.host) {
+ var hostPort = _request.headers.host.split(":");
+ /*** {String} */
+ request.host = hostPort[0];
+ /*** {Number} */
+ request.port = +hostPort[1] || 80;
+ }
+
+ var socket = _request.socket;
+ /*** {String} */
+ request.remoteHost = socket.remoteAddress;
+ /*** {Number} */
+ request.remotePort = socket.remotePort;
+
+ /*** {String} url */
+ request.url = request.scheme + "://" + _request.headers.host + request.path;
+ /*** A Q IO asynchronous text reader */
+ request.body = Reader(_request);
+ /*** {Object} HTTP headers (JSGI)*/
+ request.headers = _request.headers;
+ /*** The underlying Node request */
+ request.node = _request;
+ request.nodeRequest = _request; // Deprecated
+ /*** The underlying Node TCP connection */
+ request.nodeConnection = _request.connection;
+
+ return Q.when(request.body, function (body) {
+ request.body = body;
+ return request;
+ });
+};
+
+exports.ServerResponse = function (_response) {
+ var response = Object.create(_response);
+ response.node = _response;
+ response.nodeResponse = _response; // Deprecated
+ return response;
+};
+
+exports.normalizeRequest = function (request) {
+ if (typeof request === "string") {
+ request = {
+ url: request
+ };
+ }
+ if (request.url) {
+ var url = URL.parse(request.url);
+ request.host = url.hostname;
+ request.port = url.port;
+ request.ssl = url.protocol === "https:";
+ request.method = request.method || "GET";
+ request.path = (url.pathname || "") + (url.search || "");
+ request.headers = request.headers || {};
+ request.headers.host = url.hostname; // FIXME name consistency
+ }
+ return request;
+};
+
+exports.normalizeResponse = function (response) {
+ if (response === void 0) {
+ return;
+ }
+ if (typeof response == "string") {
+ response = [response];
+ }
+ if (response.forEach) {
+ response = {
+ status: 200,
+ headers: {},
+ body: response
+ }
+ }
+ return response;
+};
+
+/**
+ * Issues an HTTP request.
+ *
+ * @param {Request {host, port, method, path, headers,
+ * body}} request (may be a promise)
+ * @returns {Promise * Response} promise for a response
+ */
+exports.request = function (request) {
+ return Q.when(request, function (request) {
+
+ request = exports.normalizeRequest(request);
+
+ var deferred = Q.defer();
+ var ssl = request.ssl;
+ var http = ssl ? HTTPS : HTTP;
+
+ var headers = request.headers || {};
+
+ headers.host = headers.host || request.host;
+
+ var _request = http.request({
+ "host": request.host,
+ "port": request.port || (ssl ? 443 : 80),
+ "path": request.path || "/",
+ "method": request.method || "GET",
+ "headers": headers
+ }, function (_response) {
+ deferred.resolve(exports.ClientResponse(_response));
+ _response.on("error", function (error) {
+ // TODO find a better way to channel
+ // this into the response
+ console.warn(error && error.stack || error);
+ deferred.reject(error);
+ });
+ });
+
+ _request.on("error", function (error) {
+ deferred.reject(error);
+ });
+
+ Q.when(request.body, function (body) {
+ var end, done;
+ if (body) {
+ done = body.forEach(function (chunk) {
+ end = Q.when(end, function () {
+ return Q.when(chunk, function (chunk) {
+ _request.write(chunk, request.charset);
+ });
+ });
+ });
+ }
+ return Q.when(end, function () {
+ return Q.when(done, function () {
+ _request.end();
+ });
+ });
+ });
+
+ return deferred.promise;
+ });
+};
+
+/**
+ * Issues a GET request to the given URL and returns
+ * a promise for a `String` containing the entirety
+ * of the response.
+ *
+ * @param {String} url
+ * @returns {Promise * String} or a rejection if the
+ * status code is not exactly 200. The reason for the
+ * rejection is the full response object.
+ */
+exports.read = function (request, qualifier) {
+ qualifier = qualifier || function (response) {
+ return response.status === 200;
+ };
+ return Q.when(exports.request(request), function (response) {
+ if (!qualifier(response)){
+ var error = new Error("HTTP request failed with code " + response.status);
+ error.response = response;
+ throw error;
+ }
+ return Q.post(response.body, 'read', []);
+ });
+};
+
+
+/**
+ * A wrapper for the Node HTTP Response as provided
+ * by the Q HTTP Client API, suitable for use by the
+ * Q HTTP Server API.
+ */
+exports.ClientResponse = function (_response) {
+ var response = Object.create(exports.ClientResponse.prototype);
+ /*** {Number} HTTP status code */
+ response.status = _response.statusCode;
+ /*** HTTP version */
+ response.version = _response.httpVersion;
+ /*** {Object} HTTP headers */
+ response.headers = _response.headers;
+ /***
+ * A Q IO asynchronous text reader.
+ */
+ response.node = _response;
+ response.nodeResponse = _response; // Deprecated
+ response.nodeConnection = _response.connection; // Deprecated
+ return Q.when(Reader(_response), function (body) {
+ response.body = body;
+ return response;
+ });
+};
+
View
19 package.json
@@ -1,7 +1,7 @@
{
"name": "q-io",
- "version": "0.0.18",
- "description": "Q Promise wrappers for Node's IO.",
+ "version": "1.0.0",
+ "description": "IO using Q promises",
"homepage": "http://github.com/kriskowal/q-io/",
"author": "Kris Kowal <kris@cixar.com> (http://github.com/kriskowal/)",
"bugs": {
@@ -14,15 +14,20 @@
"url": "http://github.com/kriskowal/q-io/raw/master/LICENSE"
}
],
- "main": "q-io.js",
- "dependencies": {
- "q": "0.8.x"
- },
"repository": {
"type": "git",
"url": "http://github.com/kriskowal/q-io.git"
},
+ "dependencies": {
+ "q": "0.8.x && >=0.8.11"
+ },
+ "devDependencies": {
+ "test": "*"
+ },
+ "scripts": {
+ "test": "node test/all.js"
+ },
"engines": {
- "node": ">=0.2.0"
+ "node": ">=0.6.0"
}
}
View
133 q-io.js → reader.js
@@ -1,12 +1,5 @@
-/**
- * Q Promise IO streams for Node
- * @module
- */
-
-var Q = require("q"); // q package
-
-/*whatsupdoc*/
+var Q = require("q");
/**
* Wraps a Node readable stream, providing an API similar
@@ -17,8 +10,9 @@ var Q = require("q"); // q package
* the text stream reader.
* @constructor
*/
-exports.Reader = function (_stream, charset) {
- var self = Object.create(exports.Reader.prototype);
+module.exports = Reader;
+function Reader(_stream, charset) {
+ var self = Object.create(Reader.prototype);
if (charset && _stream.setEncoding) // TODO complain about inconsistency
_stream.setEncoding(charset);
@@ -55,7 +49,7 @@ exports.Reader = function (_stream, charset) {
if (charset) {
result = chunks.join("");
} else {
- result = join(chunks);
+ result = self.constructor.join(chunks);
}
chunks.splice(0, chunks.length);
return result;
@@ -99,94 +93,25 @@ exports.Reader = function (_stream, charset) {
};
return begin.promise;
-};
-
-/**
- * Wraps a Node writable stream, providing an API similar to
- * Narwhal's synchronous `io` streams, except returning and
- * accepting promises for long-latency operations.
- *
- * @param stream any Node writable stream
- * @returns {Promise * Writer} a promise for the
- * text writer.
- */
-exports.Writer = function (_stream, charset) {
- var self = Object.create(exports.Writer.prototype);
-
- if (charset && _stream.setEncoding) // TODO complain about inconsistency
- _stream.setEncoding(charset);
-
- var begin = Q.defer();
- var drained = Q.defer();
-
- _stream.on("error", function (reason) {
- begin.reject(reason);
- });
+}
- _stream.on("drain", function () {
- begin.resolve(self);
- drained.resolve();
- drained = Q.defer();
+/*
+ Reads an entire forEachable stream of buffers and returns a single buffer.
+*/
+Reader.read = read;
+function read(stream, charset) {
+ var chunks = [];
+ stream.forEach(function (chunk) {
+ chunks.push(chunk);
});
+ if (charset) {
+ return chunks.join("");
+ } else {
+ return join(chunks);
+ }
+}
- /***
- * Writes content to the stream.
- * @param {String} content
- * @returns {Promise * Undefined} a promise that will
- * be resolved when the buffer is empty, meaning
- * that all of the content has been sent.
- */
- self.write = function (content) {
- if (!_stream.writeable && !_stream.writable)
- return Q.reject(new Error("Can't write to non-writable (possibly closed) stream"));
- if (!_stream.write(content)) {
- return drained.promise;
- } else {
- return Q.resolve();
- }
- };
-
- /***
- * Waits for all data to flush on the stream.
- *
- * @returns {Promise * Undefined} a promise that will
- * be resolved when the buffer is empty
- */
- self.flush = function () {
- return drained.promise;
- };
-
- /***
- * Closes the stream, waiting for the internal buffer
- * to flush.
- *
- * @returns {Promise * Undefined} a promise that will
- * be resolved when the stream has finished writing,
- * flushing, and closed.
- */
- self.close = function () {
- _stream.end();
- drained.resolve(); // we will get no further drain events
- return Q.resolve(); // closing not explicitly observable
- };
-
- /***
- * Terminates writing on a stream, closing before
- * the internal buffer drains.
- *
- * @returns {Promise * Undefined} a promise that will
- * be resolved when the stream has finished closing.
- */
- self.destroy = function () {
- _stream.destroy();
- drained.resolve(); // we will get no further drain events
- return Q.resolve(); // destruction not explicitly observable
- };
-
- return Q.resolve(self); // todo returns the begin.promise
-};
-
-exports.join = join;
+Reader.join = join;
function join(buffers) {
var length = 0;
var at;
@@ -209,19 +134,3 @@ function join(buffers) {
return result;
}
-/*
- Reads an entire forEachable stream of buffers and returns a single buffer.
-*/
-exports.read = read;
-function read(stream, charset) {
- var chunks = [];
- stream.forEach(function (chunk) {
- chunks.push(chunk);
- });
- if (charset) {
- return chunks.join("");
- } else {
- return join(chunks);
- }
-}
-
View
9 test/all.js
@@ -0,0 +1,9 @@
+
+exports["test http"] = require("./http/all");
+exports["test fs"] = require("./fs/all");
+exports["test issue 1"] = require("./issues/1");
+
+if (require.main === module) {
+ require("test").run(exports);
+}
+
View
11 test/fs/all.js
@@ -0,0 +1,11 @@
+'use strict'
+
+exports['test mock/merge'] = require('./mock/merge');
+exports['test mock/read'] = require('./mock/read');
+exports['test mock/subtree'] = require('./mock/subtree');
+exports['test root mock'] = require('./root');
+exports['test partial'] = require("./partial");
+
+if (module == require.main)
+ require('test').run(exports)
+
View
45 test/fs/boot-directory.js
@@ -0,0 +1,45 @@
+
+var FS = require("../../fs-boot");
+
+[
+ {
+ "from": "foo",
+ "to": ""
+ },
+ {
+ "from": "",
+ "to": ".."
+ },
+ {
+ "from": ".",
+ "to": ".."
+ },
+ {
+ "from": "..",
+ "to": "../.."
+ },
+ {
+ "from": "../foo",
+ "to": ".."
+ },
+ {
+ "from": "/foo/bar",
+ "to": "/foo"
+ },
+ {
+ "from": "/foo",
+ "to": "/"
+ },
+ {
+ "from": "/",
+ "to": "/"
+ }
+].forEach(function (test) {
+ exports['test ' + test.from] = function (assert) {
+ assert.equal(FS.directory(test.from), test.to, 'ok');
+ };
+});
+
+if (require.main == module)
+ require("test").run(exports);
+
View
1  test/fs/fixtures/1234.txt
@@ -0,0 +1 @@
+1234
View
25 test/fs/issues/1.js
@@ -0,0 +1,25 @@
+
+var Q = require("q");
+var FS = require("../../../fs");
+
+exports["test write/remove"] = function (assert, done) {
+ var fileName = FS.join(module.directory || __dirname, "fixture.txt");
+
+ FS.write(fileName, "1234")
+ .then(function (data) {
+ assert.equal(data, undefined, 'written');
+ return FS.remove(fileName).then(function (data) {
+ assert.equal(data, undefined, 'removed');
+ return FS.isFile(fileName).then(function (isFile) {
+ assert.equal(isFile, false, 'confirmed removal');
+ });
+ });
+ })
+ .fin(done)
+ .done()
+
+};
+
+if (require.main === module)
+ require("test").run(exports);
+
View
32 test/fs/lazy.js
@@ -0,0 +1,32 @@
+
+var Q = require("q");
+var FS = require("../q-fs");
+
+exports['test lazy list mock'] = function (ASSERT, done) {
+ var fs = FS.Mock({
+ "a": 1,
+ "a/b": 2,
+ "a/b/c": 3
+ });
+ var tree = fs.listTree("");
+ ASSERT.ok(Q.isPromise(tree), 'tree is promise');
+ var results = [".", "a", "a/b", "a/b/c"];
+ fs.listTree("").forEach(function (name, i) {
+ ASSERT.equal(name, results.shift(), 'tree is lazy array: ' + i);
+ })
+ .fin(done)