UNPKG

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