UNPKG

6.92 kBJavaScriptView Raw
1'use strict';
2
3const PassThrough = require('stream').PassThrough;
4
5const Promise = require('bluebird');
6const Ftp = require('basic-ftp');
7const Mime = require('mime');
8
9const Tools = require('unifile-common-tools');
10const {UnifileError} = require('./error');
11
12const NAME = 'ftp';
13
14/**
15 * Initialize a new FTP client
16 * @param {Credentials} credentials - Access info for the FTP server
17 * @return {Promise<Ftp>} a promise for a FTP client
18 */
19function getClient(credentials) {
20 const ftp = new Ftp.Client();
21 return ftp.access(credentials)
22 .then(() => ftp);
23}
24
25function callAPI(session, action, client, ...params) {
26 function execute(ftpClient) {
27 // Makes paths in params absolute
28 const absParams = params.map((p) => {
29 if(p.constructor === String) return '/' + p;
30 return p;
31 });
32 switch (action) {
33 case 'ls':
34 case 'stat':
35 return ftpClient.list(...absParams);
36 case 'put':
37 return ftpClient.upload(...absParams);
38 case 'get':
39 return ftpClient.download(...absParams);
40 case 'rename':
41 return ftpClient.rename(...absParams);
42 case 'delete':
43 return ftpClient.remove(...absParams);
44 case 'rmdir':
45 return ftpClient.removeDir(...absParams);
46 case 'mkdir':
47 return ftpClient.send(`MKD ${absParams[0]}`);
48 default:
49 throw new UnifileError(UnifileError.ENOTSUP, `Unsupported FTP command ${action}`);
50 }
51 }
52
53 let ftp = client;
54 let promise = null;
55 if(ftp) {
56 promise = execute(ftp);
57 } else {
58 promise = getClient(session)
59 .then((ftpClient) => {
60 ftp = ftpClient;
61 return execute(ftp);
62 });
63 }
64
65 return promise.catch((err) => {
66 if(err.code === 530) {
67 throw new UnifileError(UnifileError.EACCES, 'Invalid credentials');
68 } else if(err.code >= 400 && err.code < 500) {
69 throw new UnifileError(UnifileError.ENOENT, 'Not found');
70 }
71 throw new UnifileError(UnifileError.EIO, err.message);
72 })
73 .then((result) => {
74 // Client was not provided, we can close it
75 if(!client && result && !result.readable) {
76 ftp.close();
77 }
78 return result;
79 });
80}
81
82function toFileInfos(entry) {
83 return {
84 size: entry.size,
85 modified: new Date(entry.date).toISOString(),
86 name: entry.name.split('/').pop(),
87 isDir: entry.isDirectory,
88 mime: entry.isDirectory ? 'application/directory' : Mime.getType(entry.name)
89 };
90}
91
92/**
93 * Service connector for {@link https://en.wikipedia.org/wiki/File_Transfer_Protocol|FTP} server
94 */
95class FtpConnector {
96 /**
97 * @constructor
98 * @param {Object} config - Configuration object
99 * @param {string} config.redirectUri - URI of the login page
100 * @param {boolean} [config.showHiddenFiles=false] - Flag to show hidden files.
101 * @param {ConnectorStaticInfos} [config.infos] - Connector infos to override
102 */
103 constructor(config) {
104 if(!config || !config.redirectUri)
105 throw new Error('You should at least set a redirectUri for this connector');
106
107 this.redirectUri = config.redirectUri;
108 this.showHiddenFile = config.showHiddenFile || false;
109 this.infos = Tools.mergeInfos(config.infos || {}, {
110 name: NAME,
111 displayName: 'FTP',
112 icon: '../assets/ftp.png',
113 description: 'Edit files on a web FTP server.'
114 });
115 this.name = this.infos.name;
116 }
117
118 getInfos(session) {
119 return Object.assign({
120 isLoggedIn: (session && 'token' in session),
121 isOAuth: false,
122 username: session.user
123 }, this.infos);
124 }
125
126 getAuthorizeURL(session) {
127 return Promise.resolve(this.redirectUri);
128 }
129
130 setAccessToken(session, token) {
131 session.token = token;
132 return Promise.resolve(token);
133 }
134
135 clearAccessToken(session) {
136 Tools.clearSession(session);
137 return Promise.resolve();
138 }
139
140 login(session, loginInfos) {
141 const ftpConf = {};
142 try {
143 Object.assign(ftpConf, Tools.parseBasicAuth(loginInfos));
144 ftpConf.pass = ftpConf.password;
145 } catch (e) {
146 return Promise.reject(e);
147 }
148
149 const client = new Ftp.Client();
150 return client.access(ftpConf)
151 .catch((err) => {
152 if(err.code === 'ETIMEDOUT')
153 throw new UnifileError(UnifileError.EIO, 'Unable to reach server');
154 else
155 throw new UnifileError(UnifileError.EACCES, 'Invalid credentials');
156 })
157 .then(() => {
158 Object.assign(session, ftpConf);
159 this.setAccessToken(session, ftpConf.user);
160 });
161 }
162
163 //Filesystem commands
164
165 readdir(session, path, ftpSession) {
166 return callAPI(session, 'ls', ftpSession, path)
167 .then((list) => {
168 return list.reduce((memo, entry) => {
169 if(this.showHiddenFile || entry.name.charAt(0) != '.')
170 memo.push(toFileInfos(entry));
171 return memo;
172 }, []);
173 });
174 }
175
176 stat(session, path, ftpSession) {
177 return callAPI(session, 'stat', ftpSession, path)
178 .then((result) => {
179 if(result.length > 1)
180 // It's a folder
181 return {
182 size: 4096,
183 modified: new Date(Math.min(...result.map((entry) => new Date(entry.date).getTime()))).toISOString(),
184 name: path,
185 isDir: true,
186 mime: 'application/directory'
187 };
188 else return toFileInfos(result[0]);
189 });
190 }
191
192 mkdir(session, path, ftpSession) {
193 return callAPI(session, 'mkdir', ftpSession, path);
194 }
195
196 writeFile(session, path, data, ftpSession) {
197 const stream = new PassThrough();
198 stream.end(data);
199 return callAPI(session, 'put', ftpSession, stream, path);
200 }
201
202 createWriteStream(session, path, ftpSession) {
203 var through = new PassThrough();
204 callAPI(session, 'put', ftpSession, through, path);
205 return through;
206 }
207
208 readFile(session, path, ftpSession) {
209 var fileStream = new PassThrough();
210 return callAPI(session, 'get', ftpSession, fileStream, path)
211 .then(() => {
212 return new Promise((resolve, reject) => {
213 const chunks = [];
214 fileStream.on('data', (chunk) => chunks.push(chunk));
215 fileStream.on('end', () => resolve(Buffer.concat(chunks)));
216 fileStream.on('error', (err) => {
217 reject(err);
218 });
219 });
220 });
221 }
222
223 createReadStream(session, path, ftpSession) {
224 var through = new PassThrough();
225 callAPI(session, 'get', ftpSession, through, path)
226 .catch((err) => through.emit('error', err));
227 return through;
228 }
229
230 rename(session, src, dest, ftpSession) {
231 return callAPI(session, 'rename', ftpSession, src, dest);
232 }
233
234 unlink(session, path, ftpSession) {
235 return callAPI(session, 'delete', ftpSession, path);
236 }
237
238 rmdir(session, path, ftpSession) {
239 return callAPI(session, 'rmdir', ftpSession, path);
240 }
241
242 batch(session, actions, message) {
243 let ftpClient;
244 return getClient(session)
245 .then((ftp) => {
246 ftpClient = ftp;
247 return Promise.each(actions, (action) => {
248 const act = action.name.toLowerCase();
249 switch (act) {
250 case 'unlink':
251 case 'rmdir':
252 case 'mkdir':
253 return this[act](session, action.path, ftpClient);
254 case 'rename':
255 return this[act](session, action.path, action.destination, ftpClient);
256 case 'writefile':
257 return this.writeFile(session, action.path, action.content, ftpClient);
258 default:
259 console.warn(`Unsupported batch action: ${action.name}`);
260 }
261 });
262 });
263 }
264}
265
266module.exports = FtpConnector;