diff --git a/docs/unifile-dropbox.js.html b/docs/unifile-dropbox.js.html
index 897a00b..9f7eae4 100644
--- a/docs/unifile-dropbox.js.html
+++ b/docs/unifile-dropbox.js.html
@@ -22,7 +22,7 @@
@@ -50,6 +50,8 @@
unifile-dropbox.js
const NAME = 'dropbox';
const DB_OAUTH_URL = 'https://www.dropbox.com/oauth2';
+const charsToEncode = /[\u007f-\uffff]/g;
+
/**
* Make a call to the Dropbox API
* @param {Object} session - Dropbox session storage
@@ -101,12 +103,18 @@
unifile-dropbox.js
errorMessage = (isJson ? body : JSON.parse(body)).error_summary;
}
// Dropbox only uses 409 for endpoints specific errors
+ let filename = null;
+ try {
+ filename = res.request.headers.hasOwnProperty('Dropbox-API-Arg') ?
+ JSON.parse(res.request.headers['Dropbox-API-Arg']).path
+ : JSON.parse(res.request.body).path;
+ } catch (e) {}
if(errorMessage.includes('/not_found/')) {
- reject(new UnifileError(UnifileError.ENOENT, 'Not Found'));
+ reject(new UnifileError(UnifileError.ENOENT, `Not Found: ${filename}`));
} else if(errorMessage.startsWith('path/conflict/')) {
- reject(new UnifileError(UnifileError.EINVAL, 'Creation failed due to conflict'));
+ reject(new UnifileError(UnifileError.EINVAL, `Creation failed due to conflict: ${filename}`));
} else if(errorMessage.startsWith('path/not_file/')) {
- reject(new UnifileError(UnifileError.EINVAL, 'Path is a directory'));
+ reject(new UnifileError(UnifileError.EINVAL, `Path is a directory: ${filename}`));
} else if(res.statusCode === 401) {
reject(new UnifileError(UnifileError.EACCES, errorMessage));
} else {
@@ -162,6 +170,18 @@
unifile-dropbox.js
})
.then((result) => checkBatchEnd(session, result, checkRoute, newId));
case 'complete':
+ const failed = result.entries.reduce((memo, entry, index) => {
+ if(entry['.tag'] === 'failure') memo.push({entry, index});
+ return memo;
+ }, []);
+ if(failed.length > 0) {
+ const errors = failed.map(({entry, index}) => {
+ const failureTag = entry.failure['.tag'];
+ return `Could not complete action ${index}: ${failureTag + '/' + entry.failure[failureTag]['.tag']}`;
+ });
+ return Promise.reject(new UnifileError(
+ UnifileError.EIO, errors.join(', ')));
+ }
return Promise.resolve();
}
}
@@ -170,6 +190,23 @@
unifile-dropbox.js
return path === '' ? path : '/' + path.split('/').filter((token) => token != '').join('/');
}
+/**
+ * Stringifies a JSON object and make it header-safe by encoding
+ * non-ASCII characters.
+ *
+ * @param {Object} v - JSON object to stringify
+ * @returns {String} the stringified object with special chars encoded
+ *
+ * @see https://www.dropboxforum.com/t5/API-support/HTTP-header-quot-Dropbox-API-Arg-quot-could-not-decode-input-as/td-p/173822
+ */
+function safeStringify(v) {
+ return JSON.stringify(v).replace(charsToEncode,
+ function(c) {
+ return '\\u' + ('000' + c.charCodeAt(0).toString(16)).slice(-4);
+ }
+ );
+}
+
/**
* Service connector for {@link https://dropbox.com|Dropbox} plateform.
*
@@ -346,7 +383,7 @@
unifile-dropbox.js
// TODO Handle file conflict and write mode
return callAPI(session, '/files/upload', data, 'content', false, {
'Content-Type': 'application/octet-stream',
- 'Dropbox-API-Arg': JSON.stringify({
+ 'Dropbox-API-Arg': safeStringify({
path: makePathAbsolute(path),
mode: this.writeMode
})
@@ -379,7 +416,7 @@
unifile-dropbox.js
readFile(session, path) {
return callAPI(session, '/files/download', {}, 'content', false, {
- 'Dropbox-API-Arg': JSON.stringify({
+ 'Dropbox-API-Arg': safeStringify({
path: makePathAbsolute(path)
})
});
@@ -393,7 +430,7 @@
unifile-dropbox.js
headers: {
'Authorization': 'Bearer ' + session.token,
'User-Agent': 'Unifile',
- 'Dropbox-API-Arg': JSON.stringify({
+ 'Dropbox-API-Arg': safeStringify({
path: makePathAbsolute(path)
})
}
@@ -447,6 +484,7 @@
unifile-dropbox.js
}
batch(session, actions, message) {
+ const writeMode = this.writeMode;
let actionsChain = Promise.resolve();
let uploadEntries = [];
@@ -483,10 +521,20 @@
unifile-dropbox.js
function deleteBatch() {
if(deleteEntries.length === 0) return Promise.resolve();
- const toDelete = deleteEntries.slice();
+ /*
+ Dropbox executes all the deletion at the same time,
+ so we remove all the descendant of a deleted folder beforehand
+ */
+ const toDelete = deleteEntries.slice().sort((a, b) => a.path.length - b.path.length);
+ const deduplicated = [];
+ while(toDelete.length !== 0) {
+ if(!deduplicated.some(({path}) => toDelete[0].path.includes(path + '/'))) {
+ deduplicated.push(toDelete.shift());
+ } else toDelete.shift();
+ }
actionsChain = actionsChain.then(() => {
return callAPI(session, '/files/delete_batch', {
- entries: toDelete
+ entries: deduplicated
})
.then((result) => checkBatchEnd(session, result, '/files/delete_batch/check'));
});
@@ -499,16 +547,17 @@
unifile-dropbox.js
const toUpload = uploadEntries.slice();
actionsChain = actionsChain.then(() => {
return Promise.map(toUpload, (action) => {
- return openUploadSession(session, action.content, true)
+ const bitContent = new Buffer(action.content);
+ return openUploadSession(session, bitContent, true)
.then((result) => {
return {
cursor: {
session_id: result.session_id,
- offset: action.content.length
+ offset: bitContent.length
},
commit: {
path: makePathAbsolute(action.path),
- mode: this.writeMode
+ mode: writeMode
}
};
});
@@ -585,7 +634,7 @@
unifile-dropbox.js
- Documentation generated by JSDoc 3.5.5 on Sat Nov 25 2017 00:43:59 GMT+0100 (Romance Standard Time) using the docdash theme.
+ Documentation generated by JSDoc 3.5.5 on Mon Dec 11 2017 00:19:56 GMT+0100 (Romance Standard Time) using the docdash theme.
diff --git a/docs/unifile-fs.js.html b/docs/unifile-fs.js.html
index d8ed211..3470153 100644
--- a/docs/unifile-fs.js.html
+++ b/docs/unifile-fs.js.html
@@ -22,7 +22,7 @@
- Classes Externals Global
+ Classes Externals Global
@@ -242,7 +242,7 @@
unifile-fs.js
- Documentation generated by JSDoc 3.5.5 on Sat Nov 25 2017 00:43:59 GMT+0100 (Romance Standard Time) using the docdash theme.
+ Documentation generated by JSDoc 3.5.5 on Mon Dec 11 2017 00:19:56 GMT+0100 (Romance Standard Time) using the docdash theme.
diff --git a/docs/unifile-ftp.js.html b/docs/unifile-ftp.js.html
index 128aa0d..c8325b0 100644
--- a/docs/unifile-ftp.js.html
+++ b/docs/unifile-ftp.js.html
@@ -22,7 +22,7 @@
- Classes Externals Global
+ Classes Externals Global
@@ -38,15 +38,15 @@
unifile-ftp.js
'use strict';
-const Path = require('path');
const PassThrough = require('stream').PassThrough;
const Promise = require('bluebird');
-const Ftp = require('ftp');
+const Ftp = require('jsftp');
const Mime = require('mime');
const Tools = require('unifile-common-tools');
+const {UnifileError} = require('./error');
const NAME = 'ftp';
@@ -57,56 +57,82 @@ unifile-ftp.js
*/
function getClient(credentials) {
return new Promise((resolve, reject) => {
- const ftp = new Ftp();
- ftp.on('ready', () => {
+ const ftp = new Ftp(credentials);
+ ftp.once('connect', () => {
resolve(ftp);
});
- ftp.on('error', (err) => {
- reject(err);
- ftp.end();
- });
-
- ftp.connect(credentials);
});
}
function callAPI(session, action, client, ...params) {
- function execute(resolve, reject, ftpClient) {
- ftpClient[action](...params, (err, res) => {
- if(err) reject(err);
- else resolve(res);
+ function execute(ftpClient) {
+ return new Promise((resolve, reject) => {
+ const handler = (err, res) => {
+ if(err) reject(err);
+ else resolve(res);
+ };
+ // Makes paths in params absolute
+ const absParams = params.map((p) => {
+ if(p.constructor === String) return '/' + p;
+ return p;
+ });
+ switch (action) {
+ case 'delete':
+ ftpClient.raw('DELE', ...absParams, handler);
+ break;
+ case 'rmdir':
+ ftpClient.raw('RMD', ...absParams, handler);
+ break;
+ case 'mkdir':
+ ftpClient.raw('MKD', ...absParams, handler);
+ break;
+ default:
+ ftpClient[action](...absParams, handler);
+ }
});
}
let ftp = client;
- return new Promise((resolve, reject) => {
- if(client) {
- execute(resolve, reject, client);
- } else {
- getClient(session)
- .then((client) => {
- ftp = client;
- execute(resolve, reject, ftp);
- })
- .catch((err) => {
- reject(err);
- });
+ let promise = null;
+ if(client) {
+ promise = execute(client);
+ } else {
+ promise = getClient(session)
+ .then((client) => {
+ ftp = client;
+ // Adds a error handler on the client
+ return Promise.race([
+ new Promise((resolve, reject) => {
+ ftp.on('error', (err) => {
+ ftp.destroy();
+ reject(err);
+ });
+ }),
+ execute(ftp)
+ ]);
+ });
+ }
+
+ return promise.catch((err) => {
+ if(err.code === 530) {
+ throw new UnifileError(UnifileError.EACCES, 'Invalid credentials');
}
+ throw new UnifileError(UnifileError.EIO, err.message);
})
.then((result) => {
// Client was not provided, we can close it
if(!client && result && !result.readable) {
- ftp.end();
+ ftp.destroy();
}
return result;
});
}
function toFileInfos(entry) {
- const isDir = entry.type === 'd';
+ const isDir = entry.type === 1;
return {
- size: entry.size,
- modified: entry.date,
+ size: parseInt(entry.size, 10),
+ modified: new Date(entry.time).toISOString(),
name: entry.name,
isDir: isDir,
mime: isDir ? 'application/directory' : Mime.getType(entry.name)
@@ -165,18 +191,23 @@ unifile-ftp.js
const ftpConf = {};
try {
Object.assign(ftpConf, Tools.parseBasicAuth(loginInfos));
+ ftpConf.pass = ftpConf.password;
} catch (e) {
return Promise.reject(e);
}
return new Promise((resolve, reject) => {
- const client = new Ftp();
+ const client = new Ftp(ftpConf);
// Successful connection
- client.on('ready', resolve);
- // Error
- client.on('error', reject);
-
- client.connect(ftpConf);
+ client.once('connect', () => {
+ client.auth(ftpConf.user, ftpConf.password, (err) => {
+ if(err) reject(err);
+ else resolve();
+ });
+ });
+ })
+ .catch((err) => {
+ throw new UnifileError(UnifileError.EACCES, 'Invalid credentials');
})
.then(() => {
Object.assign(session, ftpConf);
@@ -187,7 +218,7 @@ unifile-ftp.js
//Filesystem commands
readdir(session, path, ftpSession) {
- return callAPI(session, 'list', ftpSession, path)
+ return callAPI(session, 'ls', ftpSession, path)
.then((list) => {
return list.reduce((memo, entry) => {
if(this.showHiddenFile || entry.name.charAt(0) != '.')
@@ -198,14 +229,21 @@ unifile-ftp.js
}
stat(session, path, ftpSession) {
- return callAPI(session, 'list', ftpSession, path)
+ return callAPI(session, 'ls', ftpSession, path)
.then((entries) => {
// It's a file
if(entries.length === 1) return toFileInfos(entries[0]);
// It's a folder
- const folder = entries.find((stat) => stat.name === '.');
- folder.name = Path.basename(path);
- return toFileInfos(folder);
+ const lastTime = entries.reduce((memo, stat) => {
+ // eslint-disable-next-line no-param-reassign
+ if(stat.time > memo) memo = stat.time;
+ return memo;
+ }, 0);
+ return toFileInfos({
+ name: path.split('/').pop(),
+ type: 1,
+ time: lastTime
+ });
});
}
@@ -214,7 +252,7 @@ unifile-ftp.js
}
writeFile(session, path, data, ftpSession) {
- return callAPI(session, 'put', ftpSession, data, path);
+ return callAPI(session, 'put', ftpSession, new Buffer(data), path);
}
createWriteStream(session, path, ftpSession) {
@@ -232,11 +270,11 @@ unifile-ftp.js
const chunks = [];
fileStream.on('data', (chunk) => chunks.push(chunk));
fileStream.on('end', () => resolve(Buffer.concat(chunks)));
- fileStream.once('close', () => client.end());
fileStream.on('error', (err) => {
client.end();
reject(err);
});
+ fileStream.resume();
});
});
});
@@ -245,7 +283,10 @@ unifile-ftp.js
createReadStream(session, path, ftpSession) {
var through = new PassThrough();
callAPI(session, 'get', ftpSession, path)
- .then((fileStream) => fileStream.pipe(through))
+ .then((fileStream) => {
+ fileStream.pipe(through);
+ fileStream.resume();
+ })
.catch((err) => through.emit('error', err));
return through;
@@ -282,8 +323,7 @@ unifile-ftp.js
default:
console.warn(`Unsupported batch action: ${action.name}`);
}
- })
- .finally(() => ftpClient.end());
+ });
});
}
}
@@ -301,7 +341,7 @@ unifile-ftp.js
- Documentation generated by JSDoc 3.5.5 on Sat Nov 25 2017 00:43:59 GMT+0100 (Romance Standard Time) using the docdash theme.
+ Documentation generated by JSDoc 3.5.5 on Mon Dec 11 2017 00:19:56 GMT+0100 (Romance Standard Time) using the docdash theme.
diff --git a/docs/unifile-github.js.html b/docs/unifile-github.js.html
index 2fbb525..1f3ebb7 100644
--- a/docs/unifile-github.js.html
+++ b/docs/unifile-github.js.html
@@ -22,7 +22,7 @@
- Classes Externals Global
+ Classes Externals Global
@@ -975,7 +975,7 @@
unifile-github.js
- Documentation generated by JSDoc 3.5.5 on Sat Nov 25 2017 00:43:59 GMT+0100 (Romance Standard Time) using the docdash theme.
+ Documentation generated by JSDoc 3.5.5 on Mon Dec 11 2017 00:19:56 GMT+0100 (Romance Standard Time) using the docdash theme.
diff --git a/docs/unifile-remoteStorage.js.html b/docs/unifile-remoteStorage.js.html
index 8993a9c..ea9b1aa 100644
--- a/docs/unifile-remoteStorage.js.html
+++ b/docs/unifile-remoteStorage.js.html
@@ -22,7 +22,7 @@
- Classes Externals Global
+ Classes Externals Global
@@ -341,7 +341,7 @@
unifile-remoteStorage.js
- Documentation generated by JSDoc 3.5.5 on Sat Nov 25 2017 00:43:59 GMT+0100 (Romance Standard Time) using the docdash theme.
+ Documentation generated by JSDoc 3.5.5 on Mon Dec 11 2017 00:19:56 GMT+0100 (Romance Standard Time) using the docdash theme.
diff --git a/docs/unifile-sftp.js.html b/docs/unifile-sftp.js.html
index dfa0e0b..de07880 100644
--- a/docs/unifile-sftp.js.html
+++ b/docs/unifile-sftp.js.html
@@ -22,7 +22,7 @@
- Classes Externals Global
+ Classes Externals Global
@@ -327,7 +327,7 @@
unifile-sftp.js
- Documentation generated by JSDoc 3.5.5 on Sat Nov 25 2017 00:43:59 GMT+0100 (Romance Standard Time) using the docdash theme.
+ Documentation generated by JSDoc 3.5.5 on Mon Dec 11 2017 00:19:56 GMT+0100 (Romance Standard Time) using the docdash theme.
diff --git a/package.json b/package.json
index ea535ff..35cefc7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "unifile",
"description": "Library to provide a unified API, mirroring Fs, for cloud storage services. ",
- "version": "2.0.0",
+ "version": "2.0.1",
"author": "Jean-Baptiste Richardet
(https://github.com/JbIPS)",
"contributors": [
"Alex Hoyau (https://lexoyo.me/)"