1 | 'use strict';
|
2 |
|
3 | const Promise = require('bluebird');
|
4 | const WebFinger = require('webfinger.js');
|
5 | const request = require('request');
|
6 | const Mime = require('mime');
|
7 |
|
8 | const Tools = require('unifile-common-tools');
|
9 |
|
10 | const NAME = 'remotestorage';
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | function getRequestOptions(url, path, token) {
|
21 | return {
|
22 | url: url + '/' + path,
|
23 | headers: {
|
24 | 'Authorization': 'Bearer ' + token
|
25 | },
|
26 | encoding: null
|
27 | };
|
28 | }
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | function get(url, path, token, isStream = false) {
|
40 | const opts = getRequestOptions(url, path, token);
|
41 | if(isStream) return request.get(opts);
|
42 | else {
|
43 | return new Promise(function(resolve, reject) {
|
44 | request.get(opts, function(err, res, body) {
|
45 | resolve(body);
|
46 | });
|
47 | });
|
48 | }
|
49 | }
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | function put(url, path, content, token, isStream = false) {
|
62 | const opts = getRequestOptions(url, path, token);
|
63 | if(isStream) return request.put(opts);
|
64 | else {
|
65 | opts.body = content;
|
66 | return new Promise(function(resolve, reject) {
|
67 | request.put(opts, function(err, res, body) {
|
68 | resolve(body);
|
69 | });
|
70 | });
|
71 | }
|
72 | }
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 | function del(url, path, token) {
|
83 | const opts = getRequestOptions(url, path, token);
|
84 | return new Promise(function(resolve, reject) {
|
85 | request.del(opts, function(err, res, body) {
|
86 | resolve(body);
|
87 | });
|
88 | });
|
89 | }
|
90 |
|
91 | function toFileInfos(filename, stat) {
|
92 | return {
|
93 | size: stat['Content-Length'] || 'N/A',
|
94 | modified: 'N/A',
|
95 | name: filename,
|
96 | isDir: filename.endsWith('/'),
|
97 | mime: Mime.getType(filename)
|
98 | };
|
99 | }
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | class RemoteStorageConnector {
|
107 | |
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | constructor(config) {
|
115 | this.config = config;
|
116 | this.infos = Tools.mergeInfos(config.infos, {
|
117 | name: NAME,
|
118 | displayName: 'RemoteStorage',
|
119 | icon: '../assets/rs.png',
|
120 | description: 'Edit files on a RemoteStorage service'
|
121 | });
|
122 | this.name = this.infos.name;
|
123 | }
|
124 |
|
125 | getInfos(session) {
|
126 | return Object.assign({
|
127 | isLoggedIn: (session && 'token' in session),
|
128 | isOAuth: true,
|
129 | username: this.config.userAddress
|
130 | }, this.infos);
|
131 | }
|
132 |
|
133 | getAuthorizeURL(session) {
|
134 | return new Promise((resolve, reject) => {
|
135 | new WebFinger().lookup(session.userAddress, (err, res) => {
|
136 | if(err)
|
137 | reject(err);
|
138 | else {
|
139 | const infos = res.object.links[0];
|
140 | session.infos = {
|
141 | href: infos.href,
|
142 | storageType: infos.properties['http://remotestorage.io/spec/version'],
|
143 | authURL: infos.properties['http://tools.ietf.org/html/rfc6749#section-4.2'],
|
144 | properties: infos.properties
|
145 | };
|
146 | let query = 'redirect_uri=' + this.config.redirectUri
|
147 | + '&client_id=Unifile'
|
148 | + '&scope=*:rw'
|
149 | + '&response_type=token';
|
150 | if(session.infos.authURL.indexOf('?') > -1) query = '&' + query;
|
151 | else query = '?' + query;
|
152 |
|
153 | resolve(session.infos.authURL + query);
|
154 | }
|
155 | });
|
156 | });
|
157 | }
|
158 |
|
159 | setAccessToken(session, token) {
|
160 | session.token = token;
|
161 | return Promise.resolve(token);
|
162 | }
|
163 |
|
164 | clearAccessToken(session) {
|
165 | Tools.clearSession(session);
|
166 | return Promise.resolve();
|
167 | }
|
168 |
|
169 | login(session, loginInfos) {
|
170 | if(loginInfos.constructor === String) {
|
171 | return this.setAccessToken(session, loginInfos);
|
172 | } else {
|
173 | return this.setAccessToken(session, loginInfos.token);
|
174 | }
|
175 | }
|
176 |
|
177 | readdir(session, path) {
|
178 | if(!path.endsWith('/')) {
|
179 | return Promise.reject('Folder path must end with a /.'
|
180 | + 'If you want to see a file content, call readFile() instead');
|
181 | } else {
|
182 | return get(session.infos.href, path, session.token)
|
183 | .then((result) => {
|
184 | var obj = JSON.parse(result);
|
185 | return Object.keys(obj.items).map((key) => {
|
186 | return toFileInfos(key, obj.items[key]);
|
187 | });
|
188 | });
|
189 | }
|
190 | }
|
191 |
|
192 | stat(session, path) {
|
193 | return new Promise(function(resolve, reject) {
|
194 | request.get(getRequestOptions(session.infos.href, path, session.token), function(err, res, body) {
|
195 | resolve(body, res.headers['content-length']);
|
196 | });
|
197 | })
|
198 | .then((body, size) => {
|
199 | var obj = JSON.parse(body);
|
200 | return toFileInfos(obj.name, {'Content-Length': size});
|
201 | });
|
202 | }
|
203 |
|
204 | mkdir(session, path) {
|
205 | if(!path.endsWith('/')) {
|
206 | return Promise.reject('Folder path must end with a /. If you want to create a file, call writeFile() instead');
|
207 | } else {
|
208 | return put(session.infos.href, path + '/.keep', '', session.token);
|
209 | }
|
210 | }
|
211 |
|
212 | writeFile(session, path, content) {
|
213 | if(path.endsWith('/')) {
|
214 | return Promise.reject('File path cannot end with a /. If you want to create a folder, call mkdir() instead');
|
215 | } else {
|
216 | return put(session.infos.href, path, content, session.token);
|
217 | }
|
218 | }
|
219 |
|
220 | createWriteStream(session, path) {
|
221 | if(path.endsWith('/')) {
|
222 | return Promise.reject('File path cannot end with a /. If you want to create a folder, call mkdir() instead');
|
223 | } else {
|
224 | return put(session.infos.href, path, null, session.token, true);
|
225 | }
|
226 | }
|
227 |
|
228 | readFile(session, path) {
|
229 | if(path.endsWith('/')) {
|
230 | return Promise.reject('File path cannot end with a /.'
|
231 | + 'If you want to see a folder listing, call readdir() instead');
|
232 | } else {
|
233 | return get(session.infos.href, path, session.token);
|
234 | }
|
235 | }
|
236 |
|
237 | createReadStream(session, path) {
|
238 | if(path.endsWith('/')) {
|
239 | return Promise.reject('File path cannot end with a /.'
|
240 | + 'If you want to see a folder listing, call readdir() instead');
|
241 | } else {
|
242 | return get(session.infos.href, path, session.token, true);
|
243 | }
|
244 | }
|
245 |
|
246 | rename(session, src, dest) {
|
247 | let originContent;
|
248 | return get(session.infos.href, src, session.token)
|
249 | .then((content) => originContent = content)
|
250 | .then(() => del(session.infos.href, src, session.token))
|
251 | .then(() => put(session.infos.href, dest, originContent, session.token));
|
252 | }
|
253 |
|
254 | unlink(session, path) {
|
255 | if(path.endsWith('/')) {
|
256 | return Promise.reject('File path cannot end with a /. If you want to delete a folder, call rmdir() instead');
|
257 | } else {
|
258 | return del(session.infos.href, path, session.token);
|
259 | }
|
260 | }
|
261 |
|
262 | rmdir(session, path) {
|
263 | if(!path.endsWith('/')) {
|
264 | return Promise.reject('Folder path must end with a /. If you want to delete a file, call unlink() instead');
|
265 | } else {
|
266 | return del(session.infos.href, path + '/.keep', session.token);
|
267 | }
|
268 | }
|
269 |
|
270 | batch(session, actions, message) {
|
271 | return Promise.each(actions, (action) => {
|
272 | const act = action.name.toLowerCase();
|
273 | switch (act) {
|
274 | case 'unlink':
|
275 | case 'rmdir':
|
276 | case 'mkdir':
|
277 | this[act](session, action.path);
|
278 | break;
|
279 | case 'rename':
|
280 | this[act](session, action.path, action.destination);
|
281 | break;
|
282 | case 'writefile':
|
283 | this.writeFile(session, action.path, action.content);
|
284 | break;
|
285 | default:
|
286 | console.warn(`Unsupported batch action: ${action.name}`);
|
287 | }
|
288 | });
|
289 | }
|
290 | }
|
291 |
|
292 | module.exports = RemoteStorageConnector;
|