Permalink
Browse files

Implement copy and move in Orionode fs (fixes #4)

-Clean up some API and file helpers
  • Loading branch information...
mamacdon committed Dec 6, 2012
1 parent 42feb3c commit 74cdd6219cb521357ad50bb89690f12214689a69
Showing with 142 additions and 72 deletions.
  1. +73 −2 lib/api.js
  2. +54 −39 lib/file.js
  3. +14 −0 lib/fileUtil.js
  4. +1 −31 lib/node.js
View
@@ -1,4 +1,5 @@
-/*global exports*/
+/*global exports require*/
+var url = require('url');
/*
* Sadly, the Orion client code expects http://orionserver/file and http://orionserver/file/
@@ -39,6 +40,76 @@ function join(/*varags*/) {
return path.join('/');
}
+/**
+ * Helper for writing a JSON response.
+ * @param {Number} code
+ * @param {HttpResponse} res
+ * @param {Object} [headers]
+ * @param {Object|String} [body] If Object, response will be JSON. If string, response will be text/plain.
+ */
+function write(code, res, headers, body) {
+ res.statusCode = code;
+ if (headers) {
+ Object.keys(headers).forEach(function(header) {
+ res.setHeader(header, headers[header]);
+ });
+ }
+ if (typeof body !== 'undefined') {
+ var contentType = typeof body === 'object' ? 'application/json' : 'text/plain';
+ body = typeof body === 'object' ? body = JSON.stringify(body) : body;
+ res.setHeader('Content-Type', contentType);
+ res.setHeader('Content-Length', body.length);
+ res.end(body);
+ } else {
+ res.end();
+ }
+}
+
+/**
+ * Helper for writing an error JSON response.
+ * @param {Number} code
+ * @param {HttpResponse} res
+ * @param {String|Error} [msg]
+ */
+function writeError(code, res, msg) {
+ res.statusCode = code;
+ msg = msg instanceof Error ? msg.message : msg;
+ if (typeof msg === 'string') {
+ var err = JSON.stringify({Error: msg, Message: msg});
+ res.setHeader('Content-Type', 'application/json');
+ res.setHeader('Content-Length', err.length);
+ res.end(err);
+ } else {
+ res.end();
+ }
+}
+
+/**
+ * Util for stripping host names from URLs on this server. If aUrl indicates a resource on this host (as given by the request's Host header),
+ * returns the URL with this server's host removed. Otherwise, returns aUrl unmodified.
+ * <p>
+ * eg. matchHost({ host: 'foo.com:8081' }, 'http://foo.com/file/fizz.txt') returns '/file/fizz.txt'<br>
+ * eg. matchHost({ host: 'foo.com:8081' }, 'http://bar.com/action?frob=quux') returns 'http://bar.com/action?frob=quux'
+ * </p.
+ * @param {HttpRequest}
+ * @param {String} url
+ * @returns {String} The resulting URL
+ */
+function matchHost(req, aUrl) {
+ var thisServer = {
+ host: req.headers.host
+ };
+ var parsedUrl = url.parse(aUrl);
+ if (parsedUrl.host === thisServer.host) {
+ parsedUrl.host = '';
+ return url.format(parsedUrl);
+ }
+ return aUrl;
+}
+
exports.pathMatch = pathMatch;
+exports.matchHost = matchHost;
exports.rest = rest;
-exports.join = join;
+exports.join = join;
+exports.writeError = writeError;
+exports.write = write;
View
@@ -3,15 +3,13 @@ var compat = require('./compat');
var fs = require('fs');
var path = require('path');
var url = require('url');
-var api = require('./api');
-var fileUtil = require('./fileUtil');
+var api = require('./api'), write = api.write, writeError = api.writeError;
+var fileUtil = require('./fileUtil'), ETag = fileUtil.ETag;
var resource = require('./resource');
var USER_WRITE_FLAG = parseInt('0200', 8);
var USER_EXECUTE_FLAG = parseInt('0100', 8);
-var ETag = fileUtil.ETag;
-
function getPartsParam(req) {
var parsedUrl = url.parse(req.url, true);
return parsedUrl && parsedUrl.query.parts;
@@ -24,16 +22,7 @@ function parseBoolean(strOrBool) {
function writeEmptyFilePathError(res, rest) {
if (rest === '') {
// I think this is an implementation detail, not API, but emulate the Java Orion server's behavior here anyway.
- res.statusCode = 403;
- res.end();
- }
-}
-
-function writeError500(res, error) {
- if (error) {
- res.setHeader('Content-Type', 'application/json');
- res.statusCode = 500;
- res.end(JSON.stringify({Message: error.toString()}));
+ writeError(403, res);
}
}
@@ -204,64 +193,91 @@ module.exports = function(options) {
} else {
// write buffer into file
fs.writeFile(filepath, requestBody, function(error) {
- writeError500(res, error);
+ writeError(500, res, error);
writeFileMetadata(res, rest, filepath, stats, requestBodyETag.getValue() /*the new ETag*/);
});
}
});
});
}
},
- // TODO: support for copy/move
POST: function(req, res, next, rest) {
+ function checkXCreateOptions(opts) {
+ return opts.indexOf('copy') === -1 || opts.indexOf('move') === -1;
+ }
if (writeEmptyFilePathError(res, rest)) {
return;
}
var name = req.headers.slug || (req.body && req.body.Name);
if (!name) {
- res.statusCode = 400;
- res.end();
+ write(400, res, 'Missing Slug header or Name property');
return;
}
- var newFilepath = getSafeFilePath(path.join(rest, name));
- fs.exists(newFilepath, function(exists) {
- function fileCreated(error) {
+ var destFilepath = getSafeFilePath(path.join(rest, name));
+ fs.exists(destFilepath, function(destExists) {
+ function writeCreatedFile(error) {
if (error) {
- writeError500(res, error);
+ writeError(500, res, error);
return;
} else if (req.body) {
// var fileAtts = req.body.Attributes;
// TODO: maybe set ReadOnly and Executable based on fileAtts
}
// serialize the file metadata and we're done
- fileUtil.withStats(newFilepath, function(error, stats) {
+ fileUtil.withStats(destFilepath, function(error, stats) {
if (error) {
- writeError500(res, error);
+ writeError(500, res, error);
return;
}
- writeFileMetadata(res, rest, newFilepath, stats, null);
+ writeFileMetadata(res, rest, destFilepath, stats, null);
});
}
function createFile() {
if (req.body && parseBoolean(req.body.Directory) ) {
- fs.mkdir(newFilepath, fileCreated);
+ fs.mkdir(destFilepath, writeCreatedFile);
} else {
- fs.writeFile(newFilepath, '', fileCreated);
+ fs.writeFile(destFilepath, '', writeCreatedFile);
}
}
- var xCreateOptions = req.headers['x-create-options'] || '';
- if (xCreateOptions.indexOf('no-overwrite') !== -1 && exists) {
- if (exists) {
- res.setHeader('Content-Type', 'application/json');
- res.statusCode = 412;
- res.end(JSON.stringify({Message: 'A file or folder with the same name already exists at this location.'}));
+ function doCopyOrMove(isCopy) {
+ var sourceUrl = req.body.Location;
+ if (!sourceUrl) {
+ writeError(400, res, 'Missing Location property in request body');
return;
}
+ var sourceFilepath = getSafeFilePath(api.rest(fileRoot, api.matchHost(req, sourceUrl)));
+ fs.exists(sourceFilepath, function(sourceExists) {
+ if (!sourceExists) {
+ write(404, res, null, 'File not found: ' + sourceUrl);
+ return;
+ }
+ if (isCopy) {
+ fileUtil.copy(sourceFilepath, destFilepath, writeCreatedFile);
+ } else {
+ fs.rename(sourceFilepath, destFilepath, writeCreatedFile);
+ }
+ });
+ }
+ var xCreateOptions = req.headers['x-create-options'] || [];
+ if (!checkXCreateOptions(xCreateOptions)) {
+ write(400, res, null, 'Illegal combination of X-Create-Options.');
+ return;
}
- if (exists) {
- fs.unlink(newFilepath, createFile);
+ if (xCreateOptions.indexOf('no-overwrite') !== -1 && destExists) {
+ res.setHeader('Content-Type', 'application/json');
+ res.statusCode = 412;
+ res.end(JSON.stringify({Message: 'A file or folder with the same name already exists at this location.'}));
+ return;
+ }
+ var isCopy;
+ if ((isCopy = xCreateOptions.indexOf('copy') !== -1) || (xCreateOptions.indexOf('move') !== -1)) {
+ doCopyOrMove(isCopy);
} else {
- createFile();
+ if (destExists) {
+ fs.unlink(destFilepath, createFile);
+ } else {
+ createFile();
+ }
}
});
},
@@ -276,12 +292,11 @@ module.exports = function(options) {
res.statusCode = 204;
res.end();
} else if (etagHeader && etag !== etag) {
- res.statusCode = 214;
- res.end();
+ write(214, res);
} else {
var callback = function(error) {
if (error) {
- writeError500(res, error);
+ writeError(500, res, error);
return;
}
res.statusCode = 204;
View
@@ -139,6 +139,20 @@ exports.rumRuff = function rumRuff(dirpath, callback) {
});
};
+/**
+ * Copy srcPath to destPath
+ * @param {String} srcPath
+ * @param {String} destPath
+ * @param {Function} callback Invoked as callback(error, destPath)
+ */
+exports.copy = function(srcPath, destPath, callback) {
+ var rs = fs.createReadStream(srcPath);
+ var ws = fs.createWriteStream(destPath);
+ rs.pipe(ws);
+ rs.on('error', callback);
+ rs.on('end', callback.bind(null, null, destPath));
+};
+
/**
* @param {Function} callback Invoked as callback(error, stats)
*/
View
@@ -3,44 +3,14 @@ var child_process = require('child_process');
var fs = require('fs');
var path = require('path');
var url = require('url');
-var api = require('./api');
+var api = require('./api'), write = api.write, writeError = api.writeError;
var fileUtil = require('./fileUtil');
var resource = require('./resource');
var node_apps = require('./node_apps'), App = node_apps.App, AppTable = node_apps.AppTable;
var PATH_TO_NODE = process.execPath;
var INSPECT_PORT = 8900;
-function writeError(code, res, msg) {
- res.statusCode = code;
- if (typeof msg === 'string') {
- var err = JSON.stringify({Error: msg});
- res.setHeader('Content-Type', 'application/json');
- res.setHeader('Content-Length', err.length);
- res.end(err);
- } else {
- res.end();
- }
-}
-
-function write(code, res, headers, body) {
- res.statusCode = code;
- if (headers) {
- Object.keys(headers).forEach(function(header) {
- res.setHeader(header, headers[header]);
- });
- }
- if (typeof body !== 'undefined') {
- var contentType = typeof body === 'object' ? 'application/json' : 'text/plain';
- body = typeof body === 'object' ? body = JSON.stringify(body) : body;
- res.setHeader('Content-Type', contentType);
- res.setHeader('Content-Length', body.length);
- res.end(body);
- } else {
- res.end();
- }
-}
-
function printError(e) {
console.log('err' + e);
}

0 comments on commit 74cdd62

Please sign in to comment.