var authConfig = {
"siteName": "Achirou's Cloud", // 网站名称
"version" : "1.1.0", // 程序版本
"theme" : "acrou",
/*"client_id": "202264815644.apps.googleusercontent.com",
"client_secret": "X4Z3ca8xfWDb1Voo-F9a7ZxJ",*/
// 强烈推荐使用自己的 client_id 和 client_secret
"client_id": "",
"client_secret": "",
"refresh_token": "", // 授权 token
/**
* 设置要显示的多个云端硬盘;按格式添加多个
* [id]: 可以是 团队盘id、子文件夹id、或者"root"(代表个人盘根目录);
* [name]: 显示的名称
* [user]: Basic Auth 的用户名
* [pass]: Basic Auth 的密码
* [protect_file_link]: Basic Auth 是否用于保护文件链接,默认值(不设置时)为 false,即不保护文件链接(方便 直链下载/外部播放 等)
* 每个盘的 Basic Auth 都可以单独设置。Basic Auth 默认保护该盘下所有文件夹/子文件夹路径
* 【注意】默认不保护文件链接,这样可以方便 直链下载/外部播放;
* 如果要保护文件链接,需要将 protect_file_link 设置为 true,此时如果要进行外部播放等操作,需要将 host 替换为 user:pass@host 的 形式
* 不需要 Basic Auth 的盘,保持 user 和 pass 同时为空即可。(直接不设置也可以)
* 【注意】对于id设置为为子文件夹id的盘将不支持搜索功能(不影响其他盘)。
*/
"roots": [
{
id: "",
name: "TeamDrive",
pass: "",
},
{
id: "root",
name: "PrivateDrive",
user: '',
pass: "",
protect_file_link: true
},
{
id: "",
name: "folder1",
pass: "",
}
],
"default_gd": 0,
/**
* 文件列表页面每页显示的数量。【推荐设置值为 100 到 1000 之间】;
* 如果设置大于1000,会导致请求 drive api 时出错;
* 如果设置的值过小,会导致文件列表页面滚动条增量加载(分页加载)失效;
* 此值的另一个作用是,如果目录内文件数大于此设置值(即需要多页展示的),将会对首次列目录结果进行缓存。
*/
"files_list_page_size": 50,
/**
* 搜索结果页面每页显示的数量。【推荐设置值为 50 到 1000 之间】;
* 如果设置大于1000,会导致请求 drive api 时出错;
* 如果设置的值过小,会导致搜索结果页面滚动条增量加载(分页加载)失效;
* 此值的大小影响搜索操作的响应速度。
*/
"search_result_list_page_size": 50,
// 确认有 cors 用途的可以开启
"enable_cors_file_down": false,
/**
* 上面的 basic auth 已经包含了盘内全局保护的功能。所以默认不再去认证 .password 文件内的密码;
* 如果在全局认证的基础上,仍需要给某些目录单独进行 .password 文件内的密码验证的话,将此选项设置为 true;
* 【注意】如果开启了 .password 文件密码验证,每次列目录都会额外增加查询目录内 .password 文件是否存在的开销。
*/
"enable_password_file_verify": true
};
var themeOptions = {
//可选默认系统语言:en/zh-chs/zh-cht
languages: 'en'
}
/**
* global functions
*/
const FUNCS = {
/**
* 转换成针对谷歌搜索词法相对安全的搜索关键词
*/
formatSearchKeyword: function (keyword) {
let nothing = "";
let space = " ";
if (!keyword) return nothing;
return keyword.replace(/(!=)|['"=<>/\\:]/g, nothing)
.replace(/[,,|(){}]/g, space)
.trim()
}
};
/**
* global consts
* @type {{folder_mime_type: string, default_file_fields: string, gd_root_type: {share_drive: number, user_drive: number, sub_folder: number}}}
*/
const CONSTS = new (class {
default_file_fields = 'parents,id,name,mimeType,modifiedTime,createdTime,fileExtension,size';
gd_root_type = {
user_drive: 0,
share_drive: 1,
sub_folder: 2
};
folder_mime_type = 'application/vnd.google-apps.folder';
})();
// gd instances
var gds = [];
function html(current_drive_order = 0, model = {}) {
return `
${authConfig.siteName}
`;
};
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
/**
* Fetch and log a request
* @param {Request} request
*/
async function handleRequest(request) {
if (gds.length === 0) {
for (let i = 0; i < authConfig.roots.length; i++) {
const gd = new googleDrive(authConfig, i);
await gd.init();
gds.push(gd)
}
// 这个操作并行,提高效率
let tasks = [];
gds.forEach(gd => {
tasks.push(gd.initRootType());
});
for (let task of tasks) {
await task;
}
}
// 从 path 中提取 drive order
// 并根据 drive order 获取对应的 gd instance
let gd;
let url = new URL(request.url);
let path = url.pathname;
/**
* 重定向至起始页
* @returns {Response}
*/
function redirectToIndexPage() {
return new Response('', {status: 301, headers: {'Location': `/${authConfig.default_gd}:/`}});
}
if (path == '/') return redirectToIndexPage();
if (path.toLowerCase() == '/favicon.ico') {
// 后面可以找一个 favicon
return new Response('', {status: 404})
}
// 特殊命令格式
const command_reg = /^\/(?\d+):(?[a-zA-Z0-9]+)(\/.*)?$/g;
const match = command_reg.exec(path);
let command;
if (match) {
const num = match.groups.num;
const order = Number(num);
if (order >= 0 && order < gds.length) {
gd = gds[order];
} else {
return redirectToIndexPage()
}
// basic auth
for (const r = gd.basicAuthResponse(request); r;) return r;
command = match.groups.command;
// 搜索
if (command === 'search') {
if (request.method === 'POST') {
// 搜索结果
return handleSearch(request, gd);
} else {
const params = url.searchParams;
// 搜索页面
return new Response(html(gd.order, {
q: params.get("q") || '',
is_search_page: true,
root_type: gd.root_type
}),
{
status: 200,
headers: {'Content-Type': 'text/html; charset=utf-8'}
});
}
} else if (command === 'id2path' && request.method === 'POST') {
return handleId2Path(request, gd)
} else if (command === 'view'){
const params = url.searchParams;
return gd.view(params.get("url"), request.headers.get('Range'));
} else if (command!=='down' && request.method === 'GET'){
return new Response(html(gd.order, {root_type: gd.root_type}), {
status: 200,
headers: {'Content-Type': 'text/html; charset=utf-8'}
});
}
}
const reg = new RegExp(`^(/\\d+:)${command}/`, 'g');
path = path.replace(reg,(p1,p2)=>{
return p2+'/'
})
// 期望的 path 格式
const common_reg = /^\/\d+:\/.*$/g;
try {
if (!path.match(common_reg)) {
return redirectToIndexPage();
}
let split = path.split("/");
let order = Number(split[1].slice(0, -1));
if (order >= 0 && order < gds.length) {
gd = gds[order];
} else {
return redirectToIndexPage()
}
} catch (e) {
return redirectToIndexPage()
}
// basic auth
// for (const r = gd.basicAuthResponse(request); r;) return r;
const basic_auth_res = gd.basicAuthResponse(request);
path = path.replace(gd.url_path_prefix, '') || '/';
if (request.method == 'POST') {
return basic_auth_res || apiRequest(request, gd);
}
let action = url.searchParams.get('a');
if (path.substr(-1) == '/' || action != null) {
return basic_auth_res || new Response(html(gd.order, {root_type: gd.root_type}), {
status: 200,
headers: {'Content-Type': 'text/html; charset=utf-8'}
});
} else {
if (path.split('/').pop().toLowerCase() == ".password") {
return basic_auth_res || new Response("", {status: 404});
}
let file = await gd.file(path);
let range = request.headers.get('Range');
if (gd.root.protect_file_link && basic_auth_res) return basic_auth_res;
const is_down = !(command && command=='down');
return gd.down(file.id, range, is_down);
}
}
async function apiRequest(request, gd) {
let url = new URL(request.url);
let path = url.pathname;
path = path.replace(gd.url_path_prefix, '') || '/';
let option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}}
if (path.substr(-1) == '/') {
let deferred_pass = gd.password(path);
let body = await request.text();
body = JSON.parse(body);
// 这样可以提升首次列目录时的速度。缺点是,如果password验证失败,也依然会产生列目录的开销
let deferred_list_result = gd.list(path, body.page_token, Number(body.page_index));
// check .password file, if `enable_password_file_verify` is true
if (authConfig['enable_password_file_verify']) {
let password = await gd.password(path);
// console.log("dir password", password);
if (password && password.replace("\n", "") !== body.password) {
let html = `{"error": {"code": 401,"message": "password error."}}`;
return new Response(html, option);
}
}
let list_result = await deferred_list_result;
return new Response(JSON.stringify(list_result), option);
} else {
let file = await gd.file(path);
let range = request.headers.get('Range');
return new Response(JSON.stringify(file));
}
}
// 处理 search
async function handleSearch(request, gd) {
const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}};
let body = await request.text();
body = JSON.parse(body);
let search_result = await
gd.search(body.q || '', body.page_token, Number(body.page_index));
return new Response(JSON.stringify(search_result), option);
}
/**
* 处理 id2path
* @param request 需要 id 参数
* @param gd
* @returns {Promise} 【注意】如果从前台接收的id代表的项目不在目标gd盘下,那么response会返回给前台一个空字符串""
*/
async function handleId2Path(request, gd) {
const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}};
let body = await request.text();
body = JSON.parse(body);
let path = await gd.findPathById(body.id);
return new Response(path || '', option);
}
class googleDrive {
constructor(authConfig, order) {
// 每个盘对应一个order,对应一个gd实例
this.order = order;
this.root = authConfig.roots[order];
this.root.protect_file_link = this.root.protect_file_link || false;
this.url_path_prefix = `/${order}:`;
this.authConfig = authConfig;
// TODO: 这些缓存的失效刷新策略,后期可以制定一下
// path id
this.paths = [];
// path file
this.files = [];
// path pass
this.passwords = [];
// id <-> path
this.id_path_cache = {};
this.id_path_cache[this.root['id']] = '/';
this.paths["/"] = this.root['id'];
/*if (this.root['pass'] != "") {
this.passwords['/'] = this.root['pass'];
}*/
// this.init();
}
/**
* 初次授权;然后获取 user_drive_real_root_id
* @returns {Promise}
*/
async init() {
await this.accessToken();
/*await (async () => {
// 只获取1次
if (authConfig.user_drive_real_root_id) return;
const root_obj = await (gds[0] || this).findItemById('root');
if (root_obj && root_obj.id) {
authConfig.user_drive_real_root_id = root_obj.id
}
})();*/
// 等待 user_drive_real_root_id ,只获取1次
if (authConfig.user_drive_real_root_id) return;
const root_obj = await (gds[0] || this).findItemById('root');
if (root_obj && root_obj.id) {
authConfig.user_drive_real_root_id = root_obj.id
}
}
/**
* 获取根目录类型,设置到 root_type
* @returns {Promise}
*/
async initRootType() {
const root_id = this.root['id'];
const types = CONSTS.gd_root_type;
if (root_id === 'root' || root_id === authConfig.user_drive_real_root_id) {
this.root_type = types.user_drive;
} else {
const obj = await this.getShareDriveObjById(root_id);
this.root_type = obj ? types.share_drive : types.sub_folder;
}
}
/**
* Returns a response that requires authorization, or null
* @param request
* @returns {Response|null}
*/
basicAuthResponse(request) {
const user = this.root.user || '',
pass = this.root.pass || '',
_401 = new Response('Unauthorized', {
headers: {'WWW-Authenticate': `Basic realm="goindex:drive:${this.order}"`},
status: 401
});
if (user || pass) {
const auth = request.headers.get('Authorization')
if (auth) {
try {
const [received_user, received_pass] = atob(auth.split(' ').pop()).split(':');
return (received_user === user && received_pass === pass) ? null : _401;
} catch (e) {}
}
} else return null;
return _401;
}
async view(url, range = '', inline = true) {
let requestOption = await this.requestOption();
requestOption.headers['Range'] = range;
let res = await fetch(url, requestOption);
const {headers} = res = new Response(res.body, res)
this.authConfig.enable_cors_file_down && headers.append('Access-Control-Allow-Origin', '*');
inline === true && headers.set('Content-Disposition', 'inline');
return res;
}
async down(id, range = '', inline = false) {
let url = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`;
let requestOption = await this.requestOption();
requestOption.headers['Range'] = range;
let res = await fetch(url, requestOption);
const {headers} = res = new Response(res.body, res)
this.authConfig.enable_cors_file_down && headers.append('Access-Control-Allow-Origin', '*');
inline === true && headers.set('Content-Disposition', 'inline');
return res;
}
async file(path) {
if (typeof this.files[path] == 'undefined') {
this.files[path] = await this._file(path);
}
return this.files[path];
}
async _file(path) {
let arr = path.split('/');
let name = arr.pop();
name = decodeURIComponent(name).replace(/\'/g, "\\'");
let dir = arr.join('/') + '/';
// console.log(name, dir);
let parent = await this.findPathId(dir);
// console.log(parent);
let url = 'https://www.googleapis.com/drive/v3/files';
let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true};
params.q = `'${parent}' in parents and name = '${name}' and trashed = false`;
params.fields = "files(id, name, mimeType, size ,createdTime, modifiedTime, iconLink, thumbnailLink)";
url += '?' + this.enQuery(params);
let requestOption = await this.requestOption();
let response = await fetch(url, requestOption);
let obj = await response.json();
// console.log(obj);
return obj.files[0];
}
// 通过reqeust cache 来缓存
async list(path, page_token = null, page_index = 0) {
if (this.path_children_cache == undefined) {
// { :[ {nextPageToken:'',data:{}}, {nextPageToken:'',data:{}} ...], ...}
this.path_children_cache = {};
}
if (this.path_children_cache[path]
&& this.path_children_cache[path][page_index]
&& this.path_children_cache[path][page_index].data
) {
let child_obj = this.path_children_cache[path][page_index];
return {
nextPageToken: child_obj.nextPageToken || null,
curPageIndex: page_index,
data: child_obj.data
};
}
let id = await this.findPathId(path);
let result = await this._ls(id, page_token, page_index);
let data = result.data;
// 对有多页的,进行缓存
if (result.nextPageToken && data.files) {
if (!Array.isArray(this.path_children_cache[path])) {
this.path_children_cache[path] = []
}
this.path_children_cache[path][Number(result.curPageIndex)] = {
nextPageToken: result.nextPageToken,
data: data
};
}
return result
}
async _ls(parent, page_token = null, page_index = 0) {
// console.log("_ls", parent);
if (parent == undefined) {
return null;
}
let obj;
let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true};
params.q = `'${parent}' in parents and trashed = false AND name !='.password'`;
params.orderBy = 'folder,name,modifiedTime desc';
params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime, thumbnailLink, description)";
params.pageSize = this.authConfig.files_list_page_size;
if (page_token) {
params.pageToken = page_token;
}
let url = 'https://www.googleapis.com/drive/v3/files';
url += '?' + this.enQuery(params);
let requestOption = await this.requestOption();
let response = await fetch(url, requestOption);
obj = await response.json();
return {
nextPageToken: obj.nextPageToken || null,
curPageIndex: page_index,
data: obj
};
/*do {
if (pageToken) {
params.pageToken = pageToken;
}
let url = 'https://www.googleapis.com/drive/v3/files';
url += '?' + this.enQuery(params);
let requestOption = await this.requestOption();
let response = await fetch(url, requestOption);
obj = await response.json();
files.push(...obj.files);
pageToken = obj.nextPageToken;
} while (pageToken);*/
}
async password(path) {
if (this.passwords[path] !== undefined) {
return this.passwords[path];
}
// console.log("load", path, ".password", this.passwords[path]);
let file = await this.file(path + '.password');
if (file == undefined) {
this.passwords[path] = null;
} else {
let url = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`;
let requestOption = await this.requestOption();
let response = await this.fetch200(url, requestOption);
this.passwords[path] = await response.text();
}
return this.passwords[path];
}
/**
* 通过 id 获取 share drive 信息
* @param any_id
* @returns {Promise} 任何非正常情况都返回 null
*/
async getShareDriveObjById(any_id) {
if (!any_id) return null;
if ('string' !== typeof any_id) return null;
let url = `https://www.googleapis.com/drive/v3/drives/${any_id}`;
let requestOption = await this.requestOption();
let res = await fetch(url, requestOption);
let obj = await res.json();
if (obj && obj.id) return obj;
return null
}
/**
* 搜索
* @returns {Promise<{data: null, nextPageToken: null, curPageIndex: number}>}
*/
async search(origin_keyword, page_token = null, page_index = 0) {
const types = CONSTS.gd_root_type;
const is_user_drive = this.root_type === types.user_drive;
const is_share_drive = this.root_type === types.share_drive;
const empty_result = {
nextPageToken: null,
curPageIndex: page_index,
data: null
};
if (!is_user_drive && !is_share_drive) {
return empty_result;
}
let keyword = FUNCS.formatSearchKeyword(origin_keyword);
if (!keyword) {
// 关键词为空,返回
return empty_result;
}
let words = keyword.split(/\s+/);
let name_search_str = `name contains '${words.join("' AND name contains '")}'`;
// corpora 为 user 是个人盘 ,为 drive 是团队盘。配合 driveId
let params = {};
if (is_user_drive) {
params.corpora = 'user'
}
if (is_share_drive) {
params.corpora = 'drive';
params.driveId = this.root.id;
// This parameter will only be effective until June 1, 2020. Afterwards shared drive items will be included in the results.
params.includeItemsFromAllDrives = true;
params.supportsAllDrives = true;
}
if (page_token) {
params.pageToken = page_token;
}
params.q = `trashed = false AND name !='.password' AND (${name_search_str})`;
params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime, thumbnailLink, description)";
params.pageSize = this.authConfig.search_result_list_page_size;
// params.orderBy = 'folder,name,modifiedTime desc';
let url = 'https://www.googleapis.com/drive/v3/files';
url += '?' + this.enQuery(params);
// console.log(params)
let requestOption = await this.requestOption();
let response = await fetch(url, requestOption);
let res_obj = await response.json();
return {
nextPageToken: res_obj.nextPageToken || null,
curPageIndex: page_index,
data: res_obj
};
}
/**
* 一层一层的向上获取这个文件或文件夹的上级文件夹的 file 对象。注意:会很慢!!!
* 最多向上寻找到当前 gd 对象的根目录 (root id)
* 只考虑一条单独的向上链。
* 【注意】如果此id代表的项目不在目标gd盘下,那么此函数会返回null
*
* @param child_id
* @param contain_myself
* @returns {Promise<[]>}
*/
async findParentFilesRecursion(child_id, contain_myself = true) {
const gd = this;
const gd_root_id = gd.root.id;
const user_drive_real_root_id = authConfig.user_drive_real_root_id;
const is_user_drive = gd.root_type === CONSTS.gd_root_type.user_drive;
// 自下向上查询的终点目标id
const target_top_id = is_user_drive ? user_drive_real_root_id : gd_root_id;
const fields = CONSTS.default_file_fields;
// [{},{},...]
const parent_files = [];
let meet_top = false;
async function addItsFirstParent(file_obj) {
if (!file_obj) return;
if (!file_obj.parents) return;
if (file_obj.parents.length < 1) return;
// ['','',...]
let p_ids = file_obj.parents;
if (p_ids && p_ids.length > 0) {
// its first parent
const first_p_id = p_ids[0];
if (first_p_id === target_top_id) {
meet_top = true;
return;
}
const p_file_obj = await gd.findItemById(first_p_id);
if (p_file_obj && p_file_obj.id) {
parent_files.push(p_file_obj);
await addItsFirstParent(p_file_obj);
}
}
}
const child_obj = await gd.findItemById(child_id);
if (contain_myself) {
parent_files.push(child_obj);
}
await addItsFirstParent(child_obj);
return meet_top ? parent_files : null
}
/**
* 获取相对于本盘根目录的path
* @param child_id
* @returns {Promise} 【注意】如果此id代表的项目不在目标gd盘下,那么此方法会返回空字符串""
*/
async findPathById(child_id) {
if (this.id_path_cache[child_id]) {
return this.id_path_cache[child_id];
}
const p_files = await this.findParentFilesRecursion(child_id);
if (!p_files || p_files.length < 1) return '';
let cache = [];
// 把查出来的每一级的path和id都缓存一下
p_files.forEach((value, idx) => {
const is_folder = idx === 0 ? (p_files[idx].mimeType === CONSTS.folder_mime_type) : true;
let path = '/' + p_files.slice(idx).map(it => it.name).reverse().join('/');
if (is_folder) path += '/';
cache.push({id: p_files[idx].id, path: path})
});
cache.forEach((obj) => {
this.id_path_cache[obj.id] = obj.path;
this.paths[obj.path] = obj.id
});
/*const is_folder = p_files[0].mimeType === CONSTS.folder_mime_type;
let path = '/' + p_files.map(it => it.name).reverse().join('/');
if (is_folder) path += '/';*/
return cache[0].path;
}
// 根据id获取file item
async findItemById(id) {
const is_user_drive = this.root_type === CONSTS.gd_root_type.user_drive;
let url = `https://www.googleapis.com/drive/v3/files/${id}?fields=${CONSTS.default_file_fields}${is_user_drive ? '' : '&supportsAllDrives=true'}`;
let requestOption = await this.requestOption();
let res = await fetch(url, requestOption);
return await res.json()
}
async findPathId(path) {
let c_path = '/';
let c_id = this.paths[c_path];
let arr = path.trim('/').split('/');
for (let name of arr) {
c_path += name + '/';
if (typeof this.paths[c_path] == 'undefined') {
let id = await this._findDirId(c_id, name);
this.paths[c_path] = id;
}
c_id = this.paths[c_path];
if (c_id == undefined || c_id == null) {
break;
}
}
// console.log(this.paths);
return this.paths[path];
}
async _findDirId(parent, name) {
name = decodeURIComponent(name).replace(/\'/g, "\\'");
// console.log("_findDirId", parent, name);
if (parent == undefined) {
return null;
}
let url = 'https://www.googleapis.com/drive/v3/files';
let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true};
params.q = `'${parent}' in parents and mimeType = 'application/vnd.google-apps.folder' and name = '${name}' and trashed = false`;
params.fields = "nextPageToken, files(id, name, mimeType)";
url += '?' + this.enQuery(params);
let requestOption = await this.requestOption();
let response = await fetch(url, requestOption);
let obj = await response.json();
if (obj.files[0] == undefined) {
return null;
}
return obj.files[0].id;
}
async accessToken() {
console.log("accessToken");
if (this.authConfig.expires == undefined || this.authConfig.expires < Date.now()) {
const obj = await this.fetchAccessToken();
if (obj.access_token != undefined) {
this.authConfig.accessToken = obj.access_token;
this.authConfig.expires = Date.now() + 3500 * 1000;
}
}
return this.authConfig.accessToken;
}
async fetchAccessToken() {
console.log("fetchAccessToken");
const url = "https://www.googleapis.com/oauth2/v4/token";
const headers = {
'Content-Type': 'application/x-www-form-urlencoded'
};
const post_data = {
'client_id': this.authConfig.client_id,
'client_secret': this.authConfig.client_secret,
'refresh_token': this.authConfig.refresh_token,
'grant_type': 'refresh_token'
}
let requestOption = {
'method': 'POST',
'headers': headers,
'body': this.enQuery(post_data)
};
const response = await fetch(url, requestOption);
return await response.json();
}
async fetch200(url, requestOption) {
let response;
for (let i = 0; i < 3; i++) {
response = await fetch(url, requestOption);
console.log(response.status);
if (response.status != 403) {
break;
}
await this.sleep(800 * (i + 1));
}
return response;
}
async requestOption(headers = {}, method = 'GET') {
const accessToken = await this.accessToken();
headers['authorization'] = 'Bearer ' + accessToken;
return {'method': method, 'headers': headers};
}
enQuery(data) {
const ret = [];
for (let d in data) {
ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
}
return ret.join('&');
}
sleep(ms) {
return new Promise(function (resolve, reject) {
let i = 0;
setTimeout(function () {
console.log('sleep' + ms);
i++;
if (i >= 2) reject(new Error('i>=2'));
else resolve(i);
}, ms);
})
}
}
String.prototype.trim = function (char) {
if (char) {
return this.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), '');
}
return this.replace(/^\s+|\s+$/g, '');
};