UNPKG

7.46 kBJavaScriptView Raw
1'use strict';
2
3const Mime = require('mime');
4const Promise = require('bluebird');
5const {PassThrough} = require('stream');
6const SFTPClient = require('sftp-promises');
7const {Client} = require('ssh2');
8
9const Tools = require('unifile-common-tools');
10
11const NAME = 'sftp';
12
13function parseError(err) {
14 let msg = null;
15 switch (err.code) {
16 case 2:
17 msg = 'This path does not exist';
18 break;
19 case 4:
20 msg = 'An error occured: ' + err;
21 break;
22 default:
23 throw err;
24 }
25 const error = new Error(msg);
26 error.code = err.code;
27 throw error;
28}
29
30/**
31 * Service connector for {@link https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol|SFTP}
32 */
33class SftpConnector {
34 /**
35 * @constructor
36 * @param {Object} config - Configuration object.
37 * @param {string} config.redirectUri - URI redirecting to an authantification form.
38 * @param {boolean} [config.showHiddenFiles=false] - Flag to show hidden files.
39 * @param {ConnectorStaticInfos} [config.infos] - Connector infos to override
40 */
41 constructor(config) {
42 if(!config || !config.redirectUri)
43 throw new Error('You should at least set a redirectUri for this connector');
44
45 this.redirectUri = config.redirectUri;
46 this.showHiddenFile = config.showHiddenFile || false;
47 this.infos = Tools.mergeInfos(config.infos || {}, {
48 name: NAME,
49 displayName: 'SFTP',
50 icon: '',
51 description: 'Edit files on a SSH server.'
52 });
53 this.name = this.infos.name;
54 }
55
56 getInfos(session) {
57 return Object.assign({
58 isLoggedIn: 'token' in session,
59 isOAuth: false,
60 username: session.username
61 }, this.infos);
62 }
63
64 // Auth methods are useless here
65
66 getAuthorizeURL(session) {
67 return Promise.resolve(this.redirectUri);
68 }
69
70 setAccessToken(session, token) {
71 session.token = token;
72 return Promise.resolve(token);
73 }
74
75 clearAccessToken(session) {
76 Tools.clearSession(session);
77 return Promise.resolve();
78 }
79
80 login(session, loginInfos) {
81 try {
82 const auth = Tools.parseBasicAuth(loginInfos);
83 session.host = auth.host;
84 session.port = auth.port;
85 session.user = auth.user;
86 // Duplicate because SFTP wait for `username` but we want to keep compatibility
87 session.username = auth.user;
88 session.password = auth.password;
89 } catch (e) {
90 return Promise.reject(e);
91 }
92 // Check credentials by stating root
93 return this.stat(session, '/')
94 .catch((err) => {
95 throw new Error('Cannot access server. Please check your credentials. ' + err);
96 })
97 .then(() => Promise.resolve(this.setAccessToken(session, session.username)));
98 }
99
100 //Filesystem commands
101 // An additional sftpSession is added to signature to support batch actions
102
103 readdir(session, path, sftpSession) {
104 const sftp = sftpSession ? new SFTPClient() : new SFTPClient(session);
105 return sftp.ls(path, sftpSession)
106 .catch(parseError)
107 .then((directory) => {
108 if(!directory.entries) return Promise.reject('Target is not a directory');
109 return directory.entries.reduce((memo, entry) => {
110 if(this.showHiddenFile || !entry.filename.startsWith('.')) {
111 const isDir = entry.longname.startsWith('d');
112 memo.push({
113 size: entry.attrs.size,
114 modified: entry.attrs.mtime,
115 name: entry.filename,
116 isDir: isDir,
117 mime: isDir ? 'application/directory' : Mime.getType(entry.filename)
118 });
119 }
120 return memo;
121 }, []);
122 });
123 }
124
125 stat(session, path, sftpSession) {
126 const sftp = sftpSession ? new SFTPClient() : new SFTPClient(session);
127 return sftp.stat(path, sftpSession)
128 .catch(parseError)
129 .then((entry) => {
130 const filename = entry.path.split('/').pop();
131 const isDir = entry.type === 'directory';
132 return {
133 size: entry.size,
134 modified: entry.mtime,
135 name: filename,
136 isDir: isDir,
137 mime: isDir ? 'application/directory' : Mime.getType(filename)
138 };
139 });
140 }
141
142 mkdir(session, path, sftpSession) {
143 const sftp = sftpSession ? new SFTPClient() : new SFTPClient(session);
144 return sftp.mkdir(path, sftpSession)
145 .catch(parseError)
146 .catch((err) => {
147 if(err.code === 4) throw new Error('Unable to create remote dir. Does it already exist?');
148 else throw err;
149 });
150 }
151
152 writeFile(session, path, data, sftpSession) {
153 const sftp = sftpSession ? new SFTPClient() : new SFTPClient(session);
154 return sftp.putBuffer(new Buffer(data), path, sftpSession)
155 .catch(parseError)
156 .catch((err) => {
157 if(err.code === 4) throw new Error('Unable to create remote file. Does its parent exist?');
158 else throw err;
159 });
160 }
161
162 createWriteStream(session, path, sftpSession) {
163 const stream = new PassThrough();
164 // Get stream for ssh2 directly
165 if(sftpSession) {
166 sftpSession.sftp((err, sftp) => {
167 const sStream = sftp.createWriteStream(path)
168 .on('close', () => {
169 stream.emit('close');
170 });
171
172 stream.pipe(sStream);
173 });
174 } else {
175 const connection = new Client();
176 connection.on('ready', function() {
177 connection.sftp((err, sftp) => {
178 const sStream = sftp.createWriteStream(path)
179 .on('close', () => {
180 stream.emit('close');
181 connection.end();
182 connection.destroy();
183 });
184
185 stream.pipe(sStream);
186 });
187 });
188 connection.on('error', function(err) {
189 stream.emit('error', err);
190 });
191
192 connection.connect(session);
193 }
194
195 return stream;
196 }
197
198 readFile(session, path, sftpSession) {
199 const sftp = sftpSession ? new SFTPClient() : new SFTPClient(session);
200 return sftp.getBuffer(path, sftpSession)
201 .catch(parseError);
202 }
203
204 createReadStream(session, path, sftpSession) {
205 const stream = new PassThrough();
206 // Get stream for ssh2 directly
207 if(sftpSession) {
208 sftpSession.sftp((err, sftp) => sftp.createReadStream(path).pipe(stream));
209 } else {
210 const connection = new Client();
211 connection.on('ready', function() {
212 connection.sftp((err, sftp) => {
213 const sStream = sftp.createReadStream(path)
214 .on('close', () => {
215 stream.emit('close');
216 connection.end();
217 connection.destroy();
218 })
219 .on('error', (err) => stream.emit('error', err));
220
221 sStream.pipe(stream);
222 });
223 });
224 connection.on('error', function(err) {
225 stream.emit('error', err);
226 });
227
228 connection.connect(session);
229 }
230
231 return stream;
232 }
233
234 rename(session, src, dest, sftpSession) {
235 const sftp = sftpSession ? new SFTPClient() : new SFTPClient(session);
236 return sftp.mv(src, dest, sftpSession)
237 .catch(parseError);
238 }
239
240 unlink(session, path, sftpSession) {
241 const sftp = sftpSession ? new SFTPClient() : new SFTPClient(session);
242 return sftp.rm(path, sftpSession)
243 .catch(parseError);
244 }
245
246 rmdir(session, path, sftpSession) {
247 const sftp = sftpSession ? new SFTPClient() : new SFTPClient(session);
248 return sftp.rmdir(path, sftpSession)
249 .catch(parseError);
250 }
251
252 batch(session, actions, message) {
253 const sftp = new SFTPClient();
254 return sftp.session(session)
255 .catch(parseError)
256 .then((sftpSession) => {
257 return Promise.each(actions, (action) => {
258 const act = action.name.toLowerCase();
259 switch (act) {
260 case 'unlink':
261 case 'rmdir':
262 case 'mkdir':
263 return this[act](session, action.path, sftpSession);
264 case 'rename':
265 return this[act](session, action.path, action.destination, sftpSession);
266 case 'writefile':
267 return this.writeFile(session, action.path, action.content, sftpSession);
268 default:
269 console.warn(`Unsupported batch action: ${action.name}`);
270 }
271 })
272 // Close socket
273 .then(() => sftpSession.end());
274 });
275 }
276}
277
278module.exports = SftpConnector;