'use strict';
const PassThrough = require('stream').PassThrough;
const Promise = require('bluebird');
const Ftp = require('basic-ftp');
const Mime = require('mime');
const Tools = require('unifile-common-tools');
const {UnifileError} = require('./error');
const NAME = 'ftp';
/**
* Initialize a new FTP client
* @param {Credentials} credentials - Access info for the FTP server
* @return {Promise<Ftp>} a promise for a FTP client
*/
function getClient(credentials) {
const ftp = new Ftp.Client();
return ftp.access(credentials)
.then(() => ftp);
}
function callAPI(session, action, client, ...params) {
function execute(ftpClient) {
// Makes paths in params absolute
const absParams = params.map((p) => {
if(p.constructor === String) return '/' + p;
return p;
});
switch (action) {
case 'ls':
case 'stat':
return ftpClient.list(...absParams);
case 'put':
return ftpClient.upload(...absParams);
case 'get':
return ftpClient.download(...absParams);
case 'rename':
return ftpClient.rename(...absParams);
case 'delete':
return ftpClient.remove(...absParams);
case 'rmdir':
return ftpClient.removeDir(...absParams);
case 'mkdir':
return ftpClient.send(`MKD ${absParams[0]}`);
default:
throw new UnifileError(UnifileError.ENOTSUP, `Unsupported FTP command ${action}`);
}
}
let ftp = client;
let promise = null;
if(ftp) {
promise = execute(ftp);
} else {
promise = getClient(session)
.then((ftpClient) => {
ftp = ftpClient;
return execute(ftp);
});
}
return promise.catch((err) => {
if(err.code === 530) {
throw new UnifileError(UnifileError.EACCES, 'Invalid credentials');
} else if(err.code >= 400 && err.code < 500) {
throw new UnifileError(UnifileError.ENOENT, 'Not found');
}
throw new UnifileError(UnifileError.EIO, err.message);
})
.then((result) => {
// Client was not provided, we can close it
if(!client && result && !result.readable) {
ftp.close();
}
return result;
});
}
function toFileInfos(entry) {
return {
size: entry.size,
modified: new Date(entry.date).toISOString(),
name: entry.name.split('/').pop(),
isDir: entry.isDirectory,
mime: entry.isDirectory ? 'application/directory' : Mime.getType(entry.name)
};
}
/**
* Service connector for {@link https://en.wikipedia.org/wiki/File_Transfer_Protocol|FTP} server
*/
class FtpConnector {
/**
* @constructor
* @param {Object} config - Configuration object
* @param {string} config.redirectUri - URI of the login page
* @param {boolean} [config.showHiddenFiles=false] - Flag to show hidden files.
* @param {ConnectorStaticInfos} [config.infos] - Connector infos to override
*/
constructor(config) {
if(!config || !config.redirectUri)
throw new Error('You should at least set a redirectUri for this connector');
this.redirectUri = config.redirectUri;
this.showHiddenFile = config.showHiddenFile || false;
this.infos = Tools.mergeInfos(config.infos || {}, {
name: NAME,
displayName: 'FTP',
icon: '../assets/ftp.png',
description: 'Edit files on a web FTP server.'
});
this.name = this.infos.name;
}
getInfos(session) {
return Object.assign({
isLoggedIn: (session && 'token' in session),
isOAuth: false,
username: session.user
}, this.infos);
}
getAuthorizeURL(session) {
return Promise.resolve(this.redirectUri);
}
setAccessToken(session, token) {
session.token = token;
return Promise.resolve(token);
}
clearAccessToken(session) {
Tools.clearSession(session);
return Promise.resolve();
}
login(session, loginInfos) {
const ftpConf = {};
try {
Object.assign(ftpConf, Tools.parseBasicAuth(loginInfos));
ftpConf.pass = ftpConf.password;
} catch (e) {
return Promise.reject(e);
}
const client = new Ftp.Client();
return client.access(ftpConf)
.catch((err) => {
if(err.code === 'ETIMEDOUT')
throw new UnifileError(UnifileError.EIO, 'Unable to reach server');
else
throw new UnifileError(UnifileError.EACCES, 'Invalid credentials');
})
.then(() => {
Object.assign(session, ftpConf);
this.setAccessToken(session, ftpConf.user);
});
}
//Filesystem commands
readdir(session, path, ftpSession) {
return callAPI(session, 'ls', ftpSession, path)
.then((list) => {
return list.reduce((memo, entry) => {
if(this.showHiddenFile || entry.name.charAt(0) != '.')
memo.push(toFileInfos(entry));
return memo;
}, []);
});
}
stat(session, path, ftpSession) {
return callAPI(session, 'stat', ftpSession, path)
.then((result) => {
if(result.length > 1)
// It's a folder
return {
size: 4096,
modified: new Date(Math.min(...result.map((entry) => new Date(entry.date).getTime()))).toISOString(),
name: path,
isDir: true,
mime: 'application/directory'
};
else return toFileInfos(result[0]);
});
}
mkdir(session, path, ftpSession) {
return callAPI(session, 'mkdir', ftpSession, path);
}
writeFile(session, path, data, ftpSession) {
const stream = new PassThrough();
stream.end(data);
return callAPI(session, 'put', ftpSession, stream, path);
}
createWriteStream(session, path, ftpSession) {
var through = new PassThrough();
callAPI(session, 'put', ftpSession, through, path);
return through;
}
readFile(session, path, ftpSession) {
var fileStream = new PassThrough();
return callAPI(session, 'get', ftpSession, fileStream, path)
.then(() => {
return new Promise((resolve, reject) => {
const chunks = [];
fileStream.on('data', (chunk) => chunks.push(chunk));
fileStream.on('end', () => resolve(Buffer.concat(chunks)));
fileStream.on('error', (err) => {
reject(err);
});
});
});
}
createReadStream(session, path, ftpSession) {
var through = new PassThrough();
callAPI(session, 'get', ftpSession, through, path)
.catch((err) => through.emit('error', err));
return through;
}
rename(session, src, dest, ftpSession) {
return callAPI(session, 'rename', ftpSession, src, dest);
}
unlink(session, path, ftpSession) {
return callAPI(session, 'delete', ftpSession, path);
}
rmdir(session, path, ftpSession) {
return callAPI(session, 'rmdir', ftpSession, path);
}
batch(session, actions, message) {
let ftpClient;
return getClient(session)
.then((ftp) => {
ftpClient = ftp;
return Promise.each(actions, (action) => {
const act = action.name.toLowerCase();
switch (act) {
case 'unlink':
case 'rmdir':
case 'mkdir':
return this[act](session, action.path, ftpClient);
case 'rename':
return this[act](session, action.path, action.destination, ftpClient);
case 'writefile':
return this.writeFile(session, action.path, action.content, ftpClient);
default:
console.warn(`Unsupported batch action: ${action.name}`);
}
});
});
}
}
module.exports = FtpConnector;