@yawetse/pkgcloud
Version:
An infrastructure-as-a-service agnostic cloud library for node.js
424 lines (366 loc) • 12.3 kB
JavaScript
/*
* files.js: Instance methods for working with files for Openstack Object Storage
*
* (C) 2013 Rackspace, Ken Perkins
* MIT LICENSE
*
*/
var filed = require('filed'),
mime = require('mime'),
base = require('../../../core/storage'),
through = require('through2'),
_ = require('underscore'),
urlJoin = require('url-join');
/**
* client.removeFile
*
* @description remove a file from a container
*
* @param {String|object} container the container or containerName
* @param {String|object} file the file or fileName to delete
* @param callback
*/
exports.removeFile = function (container, file, callback) {
var containerName = container instanceof this.models.Container ? container.name : container,
fileName = file instanceof this.models.File ? file.name : file;
this._request({
method: 'DELETE',
container: containerName,
path: fileName
}, function(err) {
return err
? callback(err)
: callback(null, true);
}
);
};
/**
* client.bulkDelete
*
* @description remove a list of files from a container
*
* @param {String|object} container the container or containerName
* @param {array} files the files or fileNames to delete
* @param callback
*/
exports.bulkDelete = function(container, files, callback) {
var self = this,
containerName = container instanceof this.models.Container ? container.name : container;
this._request({
method: 'DELETE',
body: files.map(function(file) {
return urlJoin(containerName, (file instanceof self.models.File ? file.name : file));
}).join('\r\n'),
headers: {
'Content-Type': 'text/plain'
},
qs: {
'bulk-delete': true
}
}, function(err, results) {
return err
? callback(err)
: callback(null, results);
});
};
/**
* client.upload
*
* @description upload a new file to a container.
* Returns the pipe interface so you can call:
*
* request('http://some.com/file.txt').pipe(client.upload(options));
*
* @param {object} options
* @param {String|object} options.container the container to store the file in
* @param {String} options.remote the file name for the new file
* @param {String} [options.local] an optional local file path to upload
* @param {Stream} [options.stream] optionally explicitly provide the stream instead of pipe
* @param {object} [options.headers] optionally provide headers for the call
* @param {object} [options.metadata] optionally provide metadata for the object
* @param callback
* @returns {request|*}
*/
exports.upload = function (options) {
var self = this;
// check for deprecated calling with a callback
if (typeof arguments[arguments.length - 1] === 'function') {
self.emit('log::warn', 'storage.upload no longer supports calling with a callback');
}
var container = options.container,
writableStream,
proxyStream = through(),
uploadOptions = {
method: 'PUT',
upload: true,
container: container,
path: options.remote,
headers: options.headers || {}
};
if (options.container instanceof this.models.Container) {
uploadOptions.container = options.container.name;
}
if (options.contentType) {
uploadOptions.headers['content-type'] = options.contentType;
}
else {
uploadOptions.headers['content-type'] = mime.lookup(options.remote);
}
if (options.metadata) {
uploadOptions.headers = _.extend(uploadOptions.headers,
self.serializeMetadata(self.OBJECT_META_PREFIX, options.metadata));
}
writableStream = this._request(uploadOptions);
writableStream.on('complete', function(response) {
var err = self._parseError(response);
if (err) {
proxyStream.emit('error', err);
return;
}
// load the file metadata from the cloud, so we can return a proper model
self.getFile(uploadOptions.container, options.remote, function (err, file) {
if (err) {
proxyStream.emit('error', err);
return;
}
proxyStream.emit('success', file);
});
});
writableStream.on('error', function (err) {
proxyStream.emit('error', err);
});
writableStream.on('data', function (chunk) {
proxyStream.emit('data', chunk);
});
// we need a proxy stream so we can always return a file model
// via the 'success' event
proxyStream.pipe(writableStream);
return proxyStream;
};
/**
* client.download
*
* @description download a file from a container
* Returns the pipe interface so you can call:
*
* client.download(options).pipe(fs.createWriteStream(options2));
*
* @param {object} options
* @param {String|object} options.container the container to store the file in
* @param {String} options.remote the file name for the new file
* @param {String} [options.local] an optional local file path to download to
* @param {Stream} [options.stream] optionally explicitly provide the stream instead of pipe
* @param callback
* @returns {request|*}
*/
exports.download = function (options, callback) {
var self = this,
container = options.container,
inputStream,
apiStream;
var success = !callback ? null : function (err, body, res) {
return err
? callback(err)
: callback(null, new self.models.File(self, _.extend(res.headers, {
container: options.container,
name: options.remote
})));
};
if (container instanceof self.models.Container) {
container = container.name;
}
if (options.local) {
inputStream = filed(options.local);
}
else if (options.stream) {
inputStream = options.stream;
}
apiStream = this._request({
container: container,
path: options.remote,
download: true,
headers: options.headers
}, success);
if (inputStream) {
apiStream.pipe(inputStream);
}
return apiStream;
};
/**
* client.getFile
*
* @description get the details for a specific file
*
* @param {String|object} container the container or containerName
* @param {String|object} file the file or fileName to get details for
* @param callback
*/
exports.getFile = function (container, file, callback) {
var containerName = container instanceof this.models.Container ? container.name : container,
self = this;
this._request({
method: 'HEAD',
container: containerName,
path: file,
qs: {
format: 'json'
}
}, function (err, body, res) {
return err
? callback(err)
: callback(null, new self.models.File(self, _.extend(res.headers, {
container: container,
name: file
})));
});
};
/**
* client.getFiles
*
* @description get the list of files in a container. Returns at most 10,000 files if options.limit is unspecified.
* Abstracts the aggregation of files in the case that options.limit is >10,000.
*
* @param {String|object} container the container or containerName
* @param {object|Function} options
* @param {Number} [options.limit] the number of records to return
* @param {String} [options.marker] the id of the first record to return in the current query
* @param {Function} callback
*/
exports.getFiles = function (container, options, callback) {
var self = this;
if (typeof options === 'function') {
callback = options;
options = {};
}
else if (!options) {
options = {};
}
// If limit is not specified, or it is <=10k, just make a single request
if (!options.limit || options.limit <= 10000) {
return this._getFiles(container, options, callback);
}
// Limit is specified and is >10k. Abstract the aggregation of files (cloudfiles returns max 10k at once)
var files = [];
// Keep track of how many files are left to collect
var remainingLimit = options.limit;
delete options.limit;
var getFilesCallback = function(err, someFiles) {
if (err) {
return callback(err);
}
files = files.concat(someFiles);
remainingLimit -= someFiles.length;
// Check if we should attempt to retrieve more results
if (remainingLimit > 0 && someFiles.length === 10000) {
options.marker = someFiles.pop().name;
// Once the remainingLimit value becomes < 10000, we must pass it
if (remainingLimit < 10000) {
options.limit = remainingLimit;
}
self._getFiles(container, options, getFilesCallback);
} else {
callback(null, files);
}
};
this._getFiles(container, options, getFilesCallback);
};
exports._getFiles = function (container, options, callback) {
var containerName = container instanceof this.models.Container ? container.name : container,
self = this;
var getFilesOpts = {
path: containerName,
qs: _.extend({
format: 'json'
}, _.pick(options, ['limit', 'marker', 'prefix', 'path', 'delimiter']))
};
if (options.endMarker) {
getFilesOpts.qs.end_marker = options.endMarker;
}
if (options.end_marker) {
getFilesOpts.qs.end_marker = options.end_marker;
}
if (options.prefix) {
getFilesOpts.qs.prefix = options.prefix;
}
if (options.path) {
getFilesOpts.qs.path = options.path;
}
if (options.delimiter) {
getFilesOpts.qs.delimiter = options.delimiter;
}
this._request(getFilesOpts, function (err, body) {
if (err) {
return callback(err);
}
else if (!body || !(body instanceof Array)) {
return new Error('Malformed API Response');
}
return callback(null, body.map(function (file) {
file.container = container;
return new self.models.File(self, file);
}));
});
};
/**
* client.updateFileMetadata
*
* @description Updates the specified `file` with the provided metadata `headers`
* in the Openstack account associated with this instance.
*
* @param {String|object} container the container or containerName
* @param {String|object} file the file or fileName to update
* @param callback
*/
exports.updateFileMetadata = function (container, file, callback) {
var self = this,
containerName = container instanceof self.models.Container ? container.name : container;
if (!(file instanceof base.File)) {
throw new Error('Must update an existing file instance');
}
var updateFileOpts = {
method: 'POST',
container: containerName,
path: file.name,
headers: self.serializeMetadata(self.OBJECT_META_PREFIX, file.metadata)
};
this._request(updateFileOpts, function (err) {
return err
? callback(err)
: callback(null, file);
});
};
/**
* client.copy
*
* @description copies a file to another location in the same or different container.
*
* @param {object} options
* @param {String|object} options.sourceContainer the source container
* @param {String|object} options.destinationContainer the destination container
* @param {String|object} options.sourceFile the file to copy
* @param {String|object} [options.destinationFile] the destination to copy to
* @param {String|object} [options.headers] headers to send with the request
* @param callback
*/
exports.copy = function (options, callback) {
var self = this,
containerName = options.sourceContainer instanceof self.models.Container ? options.sourceContainer.name : options.sourceContainer,
destContainerName = options.destinationContainer instanceof self.models.Container ? options.destinationContainer.name : options.destinationContainer,
destinationFile = options.destinationFile || options.sourceFile;
var copyOptions = {
method: 'COPY',
uri: options.sourceFile instanceof self.models.File ? options.sourceFile.fullPath : this._getUrl({
container: containerName,
path: options.sourceFile
}),
headers: _.extend(options.headers || {}, {
destination: urlJoin('/', destContainerName,
destinationFile instanceof self.models.File ? destinationFile.name : destinationFile)
})
};
this._request(copyOptions, function (err) {
return err
? callback(err)
: callback(null, true);
});
};