-
Notifications
You must be signed in to change notification settings - Fork 0
/
cloud-storage.js
447 lines (435 loc) · 17.8 KB
/
cloud-storage.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
/*
* This file defines the main interface for this module. It allows the
* client to specify what storage method will be used (e.g., Dropbox).
* The user can then make calls to `openFile()` or `saveFile()` from the
* standard event handlers in their editor.
*/
( function () {
/*
* Global variable to store the filesystem object that will permit reading
* of folders, and reading and writing of files. It must be an object
* with four methods:
* getAccess ( successCB, failureCB ) -- to log in to the service, if
* needed (or just call the success callback immediately if not)
* readFolder ( fullPath, successCB, failureCB ) -- pass the contents
* of the given folder, as an array of objects with attributes
* `type` (one of "file"/"folder") and `name` (the usual on-disk
* name as a string) and any other metadata, or call the failure
* callback with error details
* readFile ( fullPath, successCB, failureCB ) -- reads a file and
* calls the success callback with its contents as text on success,
* or the failure callback with error details on failure.
* writeFile ( fullPath, content, successCB, failureCB ) -- writes a
* file and calls one of the callbacks, optionally passing details.
*
* An example of an object providing these four methods in a simple context
* is the `JSONFileSystem` defined at the end of this file.
*
* Do not change this variable directly. Call `setFileSystem()`, defined
* below.
*/
var fileSystemBackEnd;
/*
* Stores the iframe element that will be used to represent the popup dialog
* containing a file open/save UI.
*/
var popupDialog;
/*
* Convenience functions for hiding or showing the dialog.
*/
function showDialog () { popupDialog.style.display = 'block'; }
function hideDialog () { popupDialog.style.display = 'none'; }
/*
* Convenience functions for passing a message to an iframe
*/
function tellPopupDialog ()
{
var args = Array.prototype.slice.apply( arguments );
popupDialog.contentWindow.postMessage( args, '*' );
}
function tellIFrame ()
{
var args = Array.prototype.slice.apply( arguments );
var target = args.shift();
target.contentWindow.postMessage( args, '*' );
}
/*
* This function populates the popupDialog variable if and only if it has
* not already been populated, returning the (potentially newly created, or
* potentially cached) result.
*/
function getPopupDialog ()
{
if ( !popupDialog ) {
popupDialog = document.createElement( 'iframe' );
popupDialog.style.position = 'absolute';
popupDialog.style.top = '50%';
popupDialog.style.left = '50%';
popupDialog.style.width = '600px';
popupDialog.style.height = '400px';
popupDialog.style.marginTop = '-200px';
popupDialog.style.marginLeft = '-300px';
popupDialog.style.border = '2px solid black';
popupDialog.style.zIndex = '100';
hideDialog();
document.body.appendChild( popupDialog );
}
return popupDialog;
}
/*
* This function can be used to fill any iframe with the HTML required to be
* used as a File > Open/Save dialog.
*/
function fillIFrame ( iframe, callback )
{
iframe.setAttribute( 'src', './dialog.html' );
function once () {
iframe.removeEventListener( 'load', once, false );
callback();
}
iframe.addEventListener( 'load', once, false );
}
/*
* Function to specify the filesystem object, whose format is defined above.
* The client must call this function before attempting to use any of the
* functions below that require a filesytem, such as openFile() or
* saveFile().
*/
setFileSystem = window.setFileSystem = function ( fileSystem )
{
fileSystemBackEnd = fileSystem;
}
/*
* The last path the user visited using the file open/save dialog window.
* This will be an array of strings rather than a single string separated
* by some kind of slashes.
*/
var lastVisitedPath = [ ];
/*
* If the user browses into a folder, update the `lastVisitedPath` to
* reflect it. Handle `..` specially by going up one level.
*/
function updatePath ( browseIntoThis ) {
if ( browseIntoThis == '..' )
lastVisitedPath.pop();
else
lastVisitedPath.push( browseIntoThis );
}
/*
* This will be initialized later to an event handler for messages from the
* file open/save dialog iframe. Each of the two workhorse functions in
* this module (`openFile()` and `saveFile()`) installs a different handler
* in this global variable to respond differently to messages from the
* dialog. This handler is referenced in the event listener installed
* next.
*/
var messageHandler;
/*
* Receive messages from related windows (most notably the `popupDialog`)
* and if they are an array, treat it as a LISP-style expression, that is,
* of the form [command,arg1,arg2,...,argN], and pass it that way to the
* message handler, if there is one.
*/
window.addEventListener( 'message', function ( event ) {
if ( !( event.data instanceof Array ) ) return;
var command = event.data.shift();
var args = event.data;
if ( messageHandler ) messageHandler( command, args );
} );
/*
* We define two placeholder functions that are useful when testing and
* debugging. These are used as the default success/failure callbacks in
* many of the functions below. This way, if you wish to call `openFile()`
* or `saveFile()` from the browser console, for example, you do not need
* to specify callbacks; these debugging callbacks will be used by default,
* and are useful in such testing/debugging contexts.
*/
function successDebug () {
console.log( 'Success callback:',
Array.prototype.slice.apply( arguments ) );
}
function failureDebug () {
console.log( 'Failure callback:',
Array.prototype.slice.apply( arguments ) );
}
/*
* Show a "File > Open" dialog box.
*
* If the user chooses a file to open, call the success callback with an
* object containing some file metadata including its full `path`, and a
* `get ( successCB, failureCB )` method the user can call thereafter. That
* method will either call the success callback with the file contents as
* text, or will call the failure callback with error details.
*
* If the user doesn't log in to the service or cancels the dialog, the
* failure callback will be called.
*
* The object passed to the success callback also contains an `update`
* member, which can be used to save new content over top of the content of
* the file opened using this method. See details in the `saveFile`
* function implemented, after the `openFile` function, further below.
*
* By default, this routine displays an iframe to contain the File > Open
* dialog it shows to the user. If the client already has an iframe to use
* as the dialog, it can be passed as the third parameter, and will be used.
* That parameter is optional; if not provided, an iframe created by this
* module is used. When providing an iframe, the client will need to be
* sure to hide/close it when either the success/failure callback is called.
*
* Example use:
*
* // prompt the user with a File > Open dialog
* openFile ( function ( chosenFile ) {
* console.log( 'The user chose this file:', chosenFile.path );
* // now try to get the file contents from the storage provider
* chosenFile.get( function ( contents ) {
* // success!
* console.log( 'File contents:', contents );
* }, function ( error ) { console.log( 'Fetch error:', error ); } );
* }, function ( error ) { console.log( 'No file chosen:', error ); } );
*/
openFile = window.openFile = function ( successCB, failureCB, iframe )
{
if ( !successCB ) successCB = successDebug;
if ( !failureCB ) failureCB = failureDebug;
var dialog = iframe || getPopupDialog();
fillIFrame( dialog, function () {
fileSystemBackEnd.getAccess( function () {
tellIFrame( dialog, 'setDialogType', 'open' );
messageHandler = function ( command, args ) {
if ( command == 'dialogBrowse' ) {
updatePath( args[0] );
openFile( successCB, failureCB );
} else if ( command == 'dialogOpen' ) {
if ( dialog != iframe ) hideDialog();
var path = lastVisitedPath.concat( [ args[0] ] );
successCB( {
path : path,
get : function ( succ, fail ) {
if ( !succ ) succ = successDebug;
if ( !fail ) fail = failureDebug;
fileSystemBackEnd.readFile( path, succ, fail );
},
update : function ( content, succ, fail ) {
if ( !succ ) succ = successDebug;
if ( !fail ) fail = failureDebug;
fileSystemBackEnd.writeFile( path, content,
succ, fail );
}
} );
} else {
if ( dialog != iframe ) hideDialog();
failureCB( 'User canceled dialog.' );
}
};
fileSystemBackEnd.readFolder( lastVisitedPath,
function ( list ) {
list.unshift( 'showList' );
tellIFrame.apply( null, [ dialog ].concat( list ) );
}, failureCB );
if ( dialog != iframe ) showDialog();
}, failureCB );
} );
}
/*
* Show a "File > Save" dialog box.
*
* If the user chooses a destination to save, call the success callback
* with an object containing some file metadata including its full `path`,
* and an `update ( content, successCB, failureCB )` method the user can
* call thereafter to write content to the storage provider at the user's
* chosen location. That method will either call the success callback with
* storage-provider-specific details about the successful write operation,
* or will call the failure callback with error details. The content to
* write must be text data. Arbitrary JSON data can be saved by first
* applying `JSON.stringify()` to it, and saving the results as text.
*
* The application may retain the object given to the first success callback
* for longer than is needed to call `update()` once. Thus, for instance,
* if the user later chooses to "Save" (rather than "Save as...") the same
* `update()` function can be called again with new file contents, to save
* new data at the same chosen location.
*
* If the user doesn't log in to the service or cancels the dialog, the
* failure callback will be called.
*
* By default, this routine displays an iframe to contain the File > Open
* dialog it shows to the user. If the client already has an iframe to use
* as the dialog, it can be passed as the third parameter, and will be used.
* That parameter is optional; if not provided, an iframe created by this
* module is used. When providing an iframe, the client will need to be
* sure to hide/close it when either the success/failure callback is called.
*
* Example use:
*
* // prompt the user with a File > Save dialog
* saveFile ( function ( saveHere ) {
* console.log( 'The user chose to save here:', saveHere.path );
* // now try to write the file contents to the storage provider
* saveHere.update( stringToSave, function ( optionalData ) {
* // success!
* console.log( 'File saved.', optionalData );
* }, function ( error ) { console.log( 'Write error:', error ); } );
* }, function ( error ) { console.log( 'No destination chosen:', error ); } );
*/
saveFile = window.saveFile = function ( successCB, failureCB, iframe )
{
if ( !successCB ) successCB = successDebug;
if ( !failureCB ) failureCB = failureDebug;
var dialog = iframe || getPopupDialog();
fillIFrame( dialog, function () {
fileSystemBackEnd.getAccess( function () {
tellIFrame( dialog, 'setDialogType', 'save' );
messageHandler = function ( command, args ) {
if ( command == 'dialogBrowse' ) {
updatePath( args[0] );
saveFile( successCB, failureCB );
} else if ( command == 'dialogSave' ) {
if ( dialog != iframe ) hideDialog();
var path = lastVisitedPath.concat( [ args[0] ] );
successCB( {
path : path,
update : function ( content, succ, fail ) {
if ( !succ ) succ = successDebug;
if ( !fail ) fail = failureDebug;
fileSystemBackEnd.writeFile( path, content,
succ, fail );
}
} );
} else {
if ( dialog != iframe ) hideDialog();
failureCB( 'User canceled dialog.' );
}
}
fileSystemBackEnd.readFolder( lastVisitedPath,
function ( list ) {
list.unshift( 'showList' );
tellIFrame.apply( null, [ dialog ].concat( list ) );
}, failureCB );
if ( dialog != iframe ) showDialog();
}, failureCB );
} );
}
/*
* We provide here an example implementation of a filesystem object, as
* defined at the top of this file. It is a read-only filesystem
* represented by a JSON hierarchy of files and folders.
*
* In a JSON filesystem, a file is an object with `type:'file'` and
* `contents:'some text'` and any other metadata you wish to add.
* A folder is an object with `type:'folder'` and `contents` mapping to an
* object whose keys are names and whose values are files or folders.
* A JSON filesystem is a folder object serving as the root.
*
* Example:
*
* setFileSystem( new JSONFileSystem( {
* type : 'folder', // filesystem root
* contents : {
* 'example.txt' : {
* type : 'file',
* contents : 'This is an example text file.\nThat\'s all.'
* },
* 'My Pictures' : {
* type : 'folder',
* contents : {
* 'README.md' : {
* type : 'file',
* contents : 'No photos yet.\n\n# SO SAD'
* }
* }
* }
* }
* } ) );
*/
JSONFileSystem = window.JSONFileSystem = function ( jsonObject )
{
/*
* Utility function for walking paths from the root into the filesystem
*/
function find ( fullPath, type ) {
var walk = jsonObject;
for ( var i = 0 ; i < fullPath.length ; i++ ) {
if ( !walk.hasOwnProperty( 'contents' ) )
throw( 'Invalid JSON filesystem structure in ' + fullPath );
if ( !walk.contents.hasOwnProperty( fullPath[i] ) )
throw( 'Could not find the folder specified: ' + fullPath );
walk = walk.contents[fullPath[i]];
}
if ( walk.type != type )
throw( 'Path does not point to a ' + type + ': ' + fullPath );
return walk;
}
return {
contents : jsonObject,
/*
* No login required; just call success.
*/
getAccess : function ( successCB, failureCB ) { successCB(); },
/*
* Convert JSONFileSystem format into format expected by
* `readFolder()`
*/
readFolder : function ( fullPath, successCB, failureCB ) {
try {
var folder = find( fullPath, 'folder' );
var contents = [ ];
for ( var key in folder.contents ) {
if ( folder.contents.hasOwnProperty( key ) ) {
contents.push( {
name : key,
type : folder.contents[key].type
} );
}
}
if ( folder != jsonObject )
contents.unshift( { name : '..', type : 'folder' } );
successCB( contents );
} catch ( error ) { failureCB( error ); }
},
/*
* Find file and return contents if it exists
*/
readFile : function ( fullPath, successCB, failureCB ) {
try { successCB( find( fullPath, 'file' ).contents ); }
catch ( error ) { failureCB( error ); }
},
/*
* There are several cases, each handled with comments inline below.
*/
writeFile : function ( fullPath, content, successCB, failureCB ) {
try {
var existingFile = find( fullPath, 'file' );
// The file exists, great! Just change its contents:
existingFile.contents = content;
successCB( 'File updated successfully.' );
} catch ( error ) {
if ( /Could not find/.test( error ) ) {
// The file does not exist.
// Does its parent folder exist? Let's check.
try {
fullPath = fullPath.slice();
var fileName = fullPath.pop();
var parentFolder = find( fullPath, 'folder' );
// Yes, it exists! Create a new file there.
parentFolder.contents[fileName] = {
type : 'file',
contents : content
};
successCB( 'File written successfully.' );
} catch ( error ) {
// Parent folder doesn't exist. Signal an error.
failureCB( error );
}
} else {
// Some other error we can't solve. Propagate it.
failureCB( error );
}
}
}
}
}
/*
* End of module IIFE.
*/
} )();