UNPKG

8.2 kBJavaScriptView Raw
1'use strict';
2
3const Promise = require('bluebird');
4const WebFinger = require('webfinger.js');
5const request = require('request');
6const Mime = require('mime');
7
8const Tools = require('unifile-common-tools');
9
10const NAME = 'remotestorage';
11
12/**
13 * Create the commons option for the API request
14 * @param {string} url - URL of the request
15 * @param {string} path - Path of the endpoint
16 * @param {string} token - Bearer token for OAuth
17 * @return {Object} request options
18 * @private
19 */
20function 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 * Performs a GET request to the API
32 * @param {string} url - URL of the request
33 * @param {string} path - Path of the endpoint
34 * @param {string} token - Bearer token for OAuth
35 * @param {boolean} isStream - Access the API as a stream or not
36 * @return {Promise<Object>} a promise of the body returned
37 * @private
38 */
39function 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 * Performs a PUT request to the API
53 * @param {string} url - URL of the request
54 * @param {string} path - Path of the endpoint
55 * @param {Object} content - Content of the request body
56 * @param {string} token - Bearer token for OAuth
57 * @param {boolean} isStream - Access the API as a stream or not
58 * @return {Promise<Object>} a promise of the body returned
59 * @private
60 */
61function 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 * Performs a DELETE request to the API
76 * @param {string} url - URL of the request
77 * @param {string} path - Path of the endpoint
78 * @param {string} token - Bearer token for OAuth
79 * @return {Promise<Object>} a promise of the body returned
80 * @private
81 */
82function 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
91function 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 * Service connector for {@link https://remotestorage.io/|remoteStorage} server.
103 *
104 * This connector use WebFinger to find the endpoint of your server.
105 */
106class RemoteStorageConnector {
107 /**
108 * @constructor
109 * @param {Object} config - Configuration object
110 * @param {string} config.redirectUri - RemoteStorage application redirect URI
111 * @param {string} config.userAddress - Url of the user RemoteStorage server.
112 * @param {ConnectorStaticInfos} [config.infos] - Connector infos to override
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
292module.exports = RemoteStorageConnector;