diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..47865ab --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,97 @@ +function extendError(ctor) { + ctor.prototype = Object.create(Error.prototype); + ctor.prototype.isNewsflowError = true; + ctor.prototype.constructor = ctor; +} + +function MissingParameterError(message) { + this.name = "MissingParameterError"; + this.message = String(message); + Error.captureStackTrace(this, MissingParameterError); +} +extendError(MissingParameterError); + +function InvalidParameterError(message) { + this.name = "InvalidParameterError"; + this.message = String(message); + Error.captureStackTrace(this, InvalidParameterError); +} +extendError(InvalidParameterError); + +function RequestError(message) { + this.name = "RequestError"; + this.message = String(message || "Request error"); + Error.captureStackTrace(this, ServerError); +} +extendError(RequestError); + +function BadRequestError(message) { + this.name = "BadRequestError"; + this.status = 400; + this.message = String(message || "Bad request"); + Error.captureStackTrace(this, BadRequestError); +} +extendError(NotFoundError); + +function UnauthorizedError(message) { + this.name = "AuthenticationError"; + this.status = 401; + this.message = message || "Unauthorized"; + Error.captureStackTrace(this, UnauthorizedError); +} +extendError(UnauthorizedError); + +function ForbiddenError(message) { + this.name = "ForbiddenError"; + this.status = 403; + this.message = message || "Forbidden"; + Error.captureStackTrace(this, ForbiddenError); +} +extendError(ForbiddenError); + +function NotFoundError(message) { + this.name = "NotFoundError"; + this.status = 404; + this.message = String(message || "Not found"); + Error.captureStackTrace(this, NotFoundError); +} +extendError(NotFoundError); + +function ServerError(message, code) { + this.name = "ServerError"; + this.status = code || 500; + this.message = String(message || "Server error"); + Error.captureStackTrace(this, ServerError); +} +extendError(ServerError); + +function errorFromStatusCode(code, message) { + switch(code) { + case 400: + return new BadRequestError(message); + + case 401: + return new UnauthorizedError(message); + + case 403: + return new ForbiddenError(message); + + case 404: + return new NotFoundError(message); + + default: + return new ServerError(message, code); + } +} + +module.exports = exports = { + MissingParameterError, + InvalidParameterError, + RequestError, + BadRequestError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + ServerError, + errorFromStatusCode +}; diff --git a/lib/index.js b/lib/index.js index 0743b98..549b353 100644 --- a/lib/index.js +++ b/lib/index.js @@ -14,6 +14,7 @@ var fd_slicer = require('fd-slicer'); var mime = require('mime'); var StreamSink = require('streamsink'); var PassThrough = require('stream').PassThrough; +var errors = require('./errors'); var MAX_PUTOBJECT_SIZE = 5 * 1024 * 1024 * 1024; var MAX_DELETE_COUNT = 1000; @@ -51,16 +52,16 @@ function Client(options) { this.multipartDownloadSize = options.multipartDownloadSize || (15 * 1024 * 1024); if (this.multipartUploadThreshold < MIN_MULTIPART_SIZE) { - throw new Error("Minimum multipartUploadThreshold is 5MB."); + throw new errors.InvalidParameterError("Minimum multipartUploadThreshold is 5MB."); } if (this.multipartUploadThreshold > MAX_PUTOBJECT_SIZE) { - throw new Error("Maximum multipartUploadThreshold is 5GB."); + throw new errors.InvalidParameterError("Maximum multipartUploadThreshold is 5GB."); } if (this.multipartUploadSize < MIN_MULTIPART_SIZE) { - throw new Error("Minimum multipartUploadSize is 5MB."); + throw new errors.InvalidParameterError("Minimum multipartUploadSize is 5MB."); } if (this.multipartUploadSize > MAX_PUTOBJECT_SIZE) { - throw new Error("Maximum multipartUploadSize is 5GB."); + throw new errors.InvalidParameterError("Maximum multipartUploadSize is 5GB."); } } @@ -191,8 +192,9 @@ Client.prototype.uploadFile = function(params) { multipartUploadSize = smallestPartSizeFromFileSize(localFileStat.size); } if (multipartUploadSize > MAX_PUTOBJECT_SIZE) { - var err = new Error("File size exceeds maximum object size: " + localFile); + var err = new errors.BadRequestError("File size exceeds maximum object size (see MAX_PUTOBJECT_SIZE): " + localFile); err.retryable = false; + err.fileSize = multipartUploadSize; handleError(err); return; } @@ -318,7 +320,7 @@ Client.prototype.uploadFile = function(params) { if (!compareMultipartETag(data.ETag, multipartETag)) { errorOccurred = true; uploader.progressAmount -= overallDelta; - cb(new Error("ETag does not match MD5 checksum")); + cb(new errors.RequestError("ETag does not match MD5 checksum")); return; } part.ETag = data.ETag; @@ -401,7 +403,7 @@ Client.prototype.uploadFile = function(params) { uploader.progressAmount = 0; inStream.pipe(multipartETag); s3Params.Body = multipartETag; - + self.s3.putObject(s3Params, function(err, data) { pendCb(); if (fatalError) return; @@ -412,7 +414,7 @@ Client.prototype.uploadFile = function(params) { pend.wait(function() { if (fatalError) return; if (!compareMultipartETag(data.ETag, localFileStat.multipartETag)) { - cb(new Error("ETag does not match MD5 checksum")); + cb(new errors.RequestError("ETag does not match MD5 checksum")); return; } cb(null, data); @@ -463,9 +465,9 @@ Client.prototype.downloadFile = function(params) { request.on('httpHeaders', function(statusCode, headers, resp) { if (statusCode >= 300) { - handleError(new Error("http status code " + statusCode)); + handleError(errors.errorFromStatusCode(statusCode)); return; - } + } if (headers['content-length'] == undefined) { var outStream = fs.createWriteStream(localFile); outStream.on('error', handleError); @@ -475,14 +477,14 @@ Client.prototype.downloadFile = function(params) { downloader.progressTotal += chunk.length; downloader.progressAmount += chunk.length; downloader.emit('progress'); - outStream.write(chunk); + outStream.write(chunk); }) - request.on('httpDone', function() { + request.on('httpDone', function() { if (errorOccurred) return; downloader.progressAmount += 1; downloader.emit('progress'); - outStream.end(); + outStream.end(); cb(); }) } else { @@ -504,11 +506,11 @@ Client.prototype.downloadFile = function(params) { hashCheckPend.go(function(cb) { multipartETag.on('end', function() { if (multipartETag.bytes !== contentLength) { - handleError(new Error("Downloaded size does not match Content-Length")); + handleError(new errors.RequestError("Downloaded size does not match Content-Length")); return; } if (eTagCount === 1 && !multipartETag.anyMatch(eTag)) { - handleError(new Error("ETag does not match MD5 checksum")); + handleError(new errors.RequestError("ETag does not match MD5 checksum")); return; } cb(); @@ -801,7 +803,7 @@ Client.prototype.downloadBuffer = function(s3Params) { var hashCheckPend = new Pend(); request.on('httpHeaders', function(statusCode, headers, resp) { if (statusCode >= 300) { - handleError(new Error("http status code " + statusCode)); + handleError(errors.errorFromStatusCode(statusCode)); return; } var contentLength = parseInt(headers['content-length'], 10); @@ -822,11 +824,11 @@ Client.prototype.downloadBuffer = function(s3Params) { hashCheckPend.go(function(cb) { multipartETag.on('end', function() { if (multipartETag.bytes !== contentLength) { - handleError(new Error("Downloaded size does not match Content-Length")); + handleError(new errors.RequestError("Downloaded size does not match Content-Length")); return; } if (eTagCount === 1 && !multipartETag.anyMatch(eTag)) { - handleError(new Error("ETag does not match MD5 checksum")); + handleError(new errors.RequestError("ETag does not match MD5 checksum")); return; } cb(); @@ -884,7 +886,7 @@ Client.prototype.downloadStream = function(s3Params) { var hashCheckPend = new Pend(); request.on('httpHeaders', function(statusCode, headers, resp) { if (statusCode >= 300) { - handleError(new Error("http status code " + statusCode)); + handleError(errors.errorFromStatusCode(statusCode)); return; } downloadStream.emit('httpHeaders', statusCode, headers, resp);