1 | 'use strict';
|
2 |
|
3 | const Mime = require('mime');
|
4 | const Promise = require('bluebird');
|
5 | const {PassThrough} = require('stream');
|
6 | const SFTPClient = require('sftp-promises');
|
7 | const {Client} = require('ssh2');
|
8 |
|
9 | const Tools = require('unifile-common-tools');
|
10 |
|
11 | const NAME = 'sftp';
|
12 |
|
13 | function 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 | function protectPath(path) {
|
31 | return path.length ? path : '/';
|
32 | }
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | class SftpConnector {
|
38 | |
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
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 |
|
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 |
|
91 | session.username = auth.user;
|
92 | session.password = auth.password;
|
93 | } catch (e) {
|
94 | return Promise.reject(e);
|
95 | }
|
96 |
|
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 |
|
105 |
|
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 |
|
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 |
|
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 |
|
277 | .then(() => sftpSession.end());
|
278 | });
|
279 | }
|
280 | }
|
281 |
|
282 | module.exports = SftpConnector;
|