/**
|
* Created by yang on 2015/8/25.
|
*/
|
'use strict';
|
|
var path = require('path');
|
var fs = require('fs');
|
var net = require('net');
|
var _ = require('lodash');
|
var is = require('is-type-of');
|
var Promise = require('bluebird');
|
var Tracker = require('./tracker.js');
|
var logger = require('./logger.js');
|
var helpers = require('./helpers.js');
|
var protocol = require('./protocol.js');
|
|
|
// exports
|
module.exports = FdfsClient;
|
|
var defaults = {
|
charset: 'utf8',
|
trackers: [],
|
// 默认超时时间10s
|
timeout: 10000,
|
// 默认后缀
|
// 当获取不到文件后缀时使用
|
defaultExt: ''
|
};
|
|
function FdfsClient(config) {
|
// config global logger
|
if (config && config.logger) {
|
logger.setLogger(config.logger);
|
}
|
this.config = _.extend({}, defaults, config);
|
|
this._checkConfig();
|
}
|
|
// private methods
|
/**
|
* 确认配置是否合法
|
* @private
|
*/
|
FdfsClient.prototype._checkConfig = function() {
|
|
// 验证trackers是否合法
|
if (!this.config.trackers) {
|
throw new Error('you must specify "trackers" in config.');
|
}
|
|
if (!Array.isArray(this.config.trackers)) {
|
this.config.trackers = [this.config.trackers];
|
}
|
|
if (this.config.trackers.length === 0) {
|
throw new Error('"trackers" in config is empty.');
|
}
|
|
this.config.trackers.forEach(function(tracker) {
|
if (!tracker.host || !tracker.port) {
|
throw new Error('"trackers" in config is invalid, every tracker must all have "host" and "port".');
|
}
|
});
|
};
|
|
/**
|
* 按顺序获取可用的tracker
|
*/
|
FdfsClient.prototype._getTracker = function() {
|
if (!this._trackerIndex) {
|
this._trackerIndex = 0;
|
} else {
|
this._trackerIndex++;
|
if (this._trackerIndex >= this.config.trackers.length) {
|
this._trackerIndex = 0;
|
}
|
}
|
var _self = this;
|
var _tracker = undefined;
|
var config = this._getTrackerConfig(this._trackerIndex);
|
return _getTrackerConnection(config).then(function(tracker) {
|
if (tracker) {
|
_tracker = tracker;
|
return tracker;
|
}
|
return Promise.reduce(_.range(_self.config.trackers.length), function(total, index) {
|
if (_tracker) {
|
return _tracker;
|
}
|
if (index == _self._trackerIndex) {
|
return;
|
}
|
config = _self._getTrackerConfig(index);
|
return _getTrackerConnection(config).then(function(tr) {
|
_tracker = tr;
|
});
|
}, 0);
|
|
}).then(function() {
|
if (_tracker) {
|
return _tracker;
|
}
|
throw new Error('all trackers connect fail, please check your tracker config or your tracker server.');
|
});
|
};
|
|
FdfsClient.prototype._getTrackerConfig = function(index) {
|
var config = this.config.trackers[index];
|
config.timeout = this.config.timeout;
|
config.charset = this.config.charset;
|
return config;
|
};
|
|
|
function _getTrackerConnection(config) {
|
return new Promise(function(resolve, reject) {
|
var _name = config.host + ':' + config.port;
|
var socket = new net.Socket();
|
logger.debug('connect to tracker server [%s]', _name);
|
socket.setTimeout(config.timeout);
|
socket.connect(config.port, config.host);
|
|
socket.on('error', function(err) {
|
logger.error('connect to tracker server [' + _name + '] err:', err);
|
resolve();
|
});
|
|
socket.on('timeout', function() {
|
socket.destroy();
|
logger.error('connect to tracker server [' + _name + '] timeout.');
|
resolve();
|
});
|
|
socket.on('connect', function() {
|
logger.debug('tracker server [%s] is connected', _name);
|
resolve(new Tracker(config, socket));
|
});
|
});
|
}
|
|
// public methods
|
|
/**
|
* 上传文件
|
* @param file absolute file path or Buffer or ReadableStream
|
* @param options
|
* options.group: 指定要上传的group, 不指定则由tracker server分配
|
* options.size: file size, file参数为ReadableStream时必须指定
|
* options.ext: 上传文件的后缀,不指定则获取file参数的后缀,不含(.)
|
*/
|
FdfsClient.prototype.upload = function(file, options) {
|
var _self = this;
|
options = options || {};
|
return _normalizeUploadParams(file, options).then(function() {
|
if (!options.ext) {
|
options.ext = _self.config.defaultExt;
|
}
|
|
if (!options.group && options.fileId) {
|
var gf = helpers.id2gf(options.fileId);
|
options.group = gf.group;
|
}
|
return _self._getTracker();
|
}).then(function(tracker) {
|
return tracker.getStoreStorage(options.group);
|
}).then(function(storage) {
|
return storage.upload(file, options);
|
});
|
};
|
|
/**
|
* 下载文件
|
* @param fileId
|
* @param options options可以直接传options.target
|
* options.target 下载的文件流将被写入到这里,可以是本地文件名,也可以是WritableStream
|
* options.offset和options.bytes: 当只想下载文件中的某1片段时指定
|
*/
|
FdfsClient.prototype.download = function(fileId, options) {
|
var _self = this;
|
return new Promise(function(resolve, reject) {
|
if (!options || is.function(options)) {
|
reject(new Error('options.target is not specified'));
|
return;
|
}
|
|
// 直接传入target
|
if (!options.target) {
|
var ori = options;
|
options = {};
|
options.target = ori;
|
}
|
|
if (!(is.string(options.target) || is.writableStream(options.target))) {
|
reject(new Error('options.target is invalid, it\'s type must be String or WritableStream'));
|
return;
|
}
|
|
if (is.string(options.target)) {
|
options.target = fs.createWriteStream(options.target);
|
}
|
resolve();
|
}).then(function() {
|
return _self._getTracker();
|
}).then(function(tracker) {
|
return tracker.getFetchStorage(fileId);
|
}).then(function(storage) {
|
return storage.download(fileId, options);
|
});
|
};
|
|
/**
|
* 删除fileId指定的文件
|
* @param fileId
|
*/
|
FdfsClient.prototype.del = function(fileId) {
|
return this._getTracker().then(function(tracker) {
|
return tracker.getUpdateStorage(fileId);
|
}).then(function(storage) {
|
return storage.del(fileId);
|
});
|
};
|
|
FdfsClient.prototype.remove = FdfsClient.prototype.del;
|
|
/**
|
* @param fileId
|
* @param metaData {key1: value1, key2: value2}
|
* @param flag 'O' for overwrite all old metadata (default)
|
'M' for merge, insert when the meta item not exist, otherwise update it
|
*/
|
FdfsClient.prototype.setMetaData = function(fileId, metaData, flag) {
|
return this._getTracker().then(function(tracker) {
|
return tracker.getUpdateStorage(fileId);
|
}).then(function(storage) {
|
return storage.setMetaData(fileId, metaData, flag);
|
});
|
};
|
|
/**
|
* 获取指定fileId的meta data
|
* @param fileId
|
*/
|
FdfsClient.prototype.getMetaData = function(fileId) {
|
return this._getTracker().then(function(tracker) {
|
return tracker.getUpdateStorage(fileId);
|
}).then(function(storage) {
|
return storage.getMetaData(fileId);
|
});
|
};
|
|
/**
|
* 获取指定fileId的信息
|
* fileInfo会传给回调,结构如下
|
* {
|
* // 文件大小
|
* size:
|
* // 文件创建的UTC时间戳,单位为秒
|
* timestamp:
|
* crc32:
|
* // 最初上传到的storage server的ip
|
* addr:
|
* }
|
* @param fileId
|
*/
|
FdfsClient.prototype.getFileInfo = function(fileId) {
|
return this._getTracker().then(function(tracker) {
|
return tracker.getUpdateStorage(fileId);
|
}).then(function(storage) {
|
return storage.getFileInfo(fileId);
|
});
|
};
|
|
FdfsClient.prototype.listGroups = function() {
|
return this._getTracker().then(function(tracker) {
|
return tracker.listGroups();
|
});
|
};
|
|
FdfsClient.prototype.listStorages = function(group) {
|
return this._getTracker().then(function(tracker) {
|
return tracker.listStorages(group);
|
});
|
};
|
|
// helpers
|
/**
|
* 验证file参数是否合法,同时补充一些必要的参数
|
* 若为String,则需验证是否存在
|
* 若为ReadableStream,则需验证options.size是否存在
|
* @param file
|
* @param options
|
*/
|
function _normalizeUploadParams(file, options) {
|
return new Promise(function(resolve, reject) {
|
if (!file) {
|
reject(new Error('The "file" parameter is empty.'));
|
return;
|
}
|
|
if (!(is.string(file) || is.buffer(file) || is.readableStream(file))) {
|
reject(new Error('The "file" parameter is invalid, it must be a String, Buffer, or ReadableStream'));
|
return;
|
}
|
|
|
if (is.readableStream(file) && !options.size) {
|
reject(new Error('when the "file" parameter\'s is ReadableStream, options.size must specified'));
|
return;
|
}
|
|
if (is.buffer(file)) {
|
options.size = file.length;
|
}
|
|
if (is.string(options.size)) {
|
options.size = Number(options.size);
|
}
|
|
// TODO
|
if (options.method === protocol.FDFS_METHOD_UPLOAD_APPENDER_FILE) {
|
|
} else if (options.method === protocol.FDFS_METHOD_APPEND_FILE) {
|
if (!options.fileId) {
|
reject(new Error('options.fileId is missed'));
|
}
|
} else if (options.method === protocol.FDFS_METHOD_MODIFY_FILE) {
|
if (!options.fileId) {
|
reject(new Error('options.fileId is missed'));
|
}
|
|
if (is.string(options.offset)) {
|
options.offset = Number(options.offset);
|
}
|
|
if (!(options.offset || options.offset === 0)) {
|
reject(new Error('options.offset is missed'));
|
}
|
if (!is.number(options.offset)) {
|
reject(new Error('options.offset must be a number'));
|
}
|
}
|
|
if (is.string(file)) {
|
var stats = fs.statSync(file);
|
options.size = stats.size;
|
if (!options.ext) {
|
options.ext = path.extname(file);
|
if (options.ext) {
|
// 去掉.
|
options.ext = options.ext.substring(1);
|
}
|
}
|
resolve();
|
}
|
resolve();
|
});
|
|
}
|