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 |
|
31 |
|
32 |
|
33 | class SftpConnector {
|
34 | |
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
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 |
|
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 |
|
87 | session.username = auth.user;
|
88 | session.password = auth.password;
|
89 | } catch (e) {
|
90 | return Promise.reject(e);
|
91 | }
|
92 |
|
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 |
|
101 |
|
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 |
|
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 |
|
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 |
|
273 | .then(() => sftpSession.end());
|
274 | });
|
275 | }
|
276 | }
|
277 |
|
278 | module.exports = SftpConnector;
|