4 /* global superagent */
10 function asyncForEach(items
, handler
, callback
) {
13 if (items
.length
=== 0) return callback();
15 (function iterator() {
16 handler(items
[cur
], function (error
) {
17 if (error
) return callback(error
);
18 if (cur
>= items
.length
-1) return callback();
26 function initWithToken(accessToken
) {
27 superagent
.get('/api/profile').query({ access_token: accessToken
}).end(function (error
, result
) {
30 if (error
&& !error
.response
) return console
.error(error
);
31 if (result
.statusCode
!== 200) {
32 delete localStorage
.accessToken
;
36 localStorage
.accessToken
= accessToken
;
37 app
.session
.username
= result
.body
.username
;
38 app
.session
.valid
= true;
40 superagent
.get('/api/settings').query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
41 if (error
) console
.error(error
);
43 app
.folderListingEnabled
= !!result
.body
.folderListingEnabled
;
45 loadDirectory(decode(window
.location
.hash
.slice(1)));
47 app
.refreshAccessTokens();
52 function sanitize(filePath
) {
53 filePath
= '/' + filePath
;
54 return filePath
.replace(/\/+/g, '/');
57 function encode(filePath
) {
58 return filePath
.split('/').map(encodeURIComponent
).join('/');
61 function decode(filePath
) {
62 return filePath
.split('/').map(decodeURIComponent
).join('/');
66 images: [ '.png', '.jpg', '.jpeg', '.tiff', '.gif' ],
67 text: [ '.txt', '.md' ],
69 html: [ '.html', '.htm', '.php' ],
70 video: [ '.mp4', '.mpg', '.mpeg', '.ogg', '.mkv', '.avi', '.mov' ]
73 function getPreviewUrl(entry
, basePath
) {
74 var path
= '/_admin/img/';
76 if (entry
.isDirectory
) return path
+ 'directory.png';
77 if (mimeTypes
.images
.some(function (e
) { return entry
.filePath
.endsWith(e
); })) return sanitize(basePath
+ '/' + entry
.filePath
);
78 if (mimeTypes
.text
.some(function (e
) { return entry
.filePath
.endsWith(e
); })) return path
+'text.png';
79 if (mimeTypes
.pdf
.some(function (e
) { return entry
.filePath
.endsWith(e
); })) return path
+ 'pdf.png';
80 if (mimeTypes
.html
.some(function (e
) { return entry
.filePath
.endsWith(e
); })) return path
+ 'html.png';
81 if (mimeTypes
.video
.some(function (e
) { return entry
.filePath
.endsWith(e
); })) return path
+ 'video.png';
83 return path
+ 'unknown.png';
86 // simple extension detection, does not work with double extension like .tar.gz
87 function getExtension(entry
) {
88 if (entry
.isFile
) return entry
.filePath
.slice(entry
.filePath
.lastIndexOf('.') + 1);
93 loadDirectory(app
.path
);
97 superagent
.post('/api/logout').query({ access_token: localStorage
.accessToken
}).end(function (error
) {
98 if (error
) console
.error(error
);
100 app
.session
.valid
= false;
102 delete localStorage
.accessToken
;
106 function loadDirectory(filePath
) {
109 filePath
= filePath
? sanitize(filePath
) : '/';
111 superagent
.get('/api/files/' + encode(filePath
)).query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
114 if (result
&& result
.statusCode
=== 401) return logout();
115 if (error
) return console
.error(error
);
117 result
.body
.entries
.sort(function (a
, b
) { return a
.isDirectory
&& b
.isFile
? -1 : 1; });
118 app
.entries
= result
.body
.entries
.map(function (entry
) {
119 entry
.previewUrl
= getPreviewUrl(entry
, filePath
);
120 entry
.extension
= getExtension(entry
);
121 entry
.rename
= false;
122 entry
.filePathNew
= entry
.filePath
;
126 app
.pathParts
= decode(filePath
).split('/').filter(function (e
) { return !!e
; }).map(function (e
, i
, a
) {
129 link: '#' + sanitize('/' + a
.slice(0, i
).join('/') + '/' + e
)
133 // update in case this was triggered from code
134 window
.location
.hash
= app
.path
;
138 function open(row
, column
, event
) {
139 // ignore item open on row clicks if we are renaming this entry
140 if (row
.rename
) return;
142 var path
= sanitize(app
.path
+ '/' + row
.filePath
);
144 if (row
.isDirectory
) {
145 window
.location
.hash
= path
;
149 app
.activeEntry
= row
;
150 app
.activeEntry
.fullPath
= encode(sanitize(app
.path
+ '/' + row
.filePath
));
151 app
.previewDrawerVisible
= true
153 // window.open(encode(path));
156 function uploadFiles(files
) {
157 if (!files
|| !files
.length
) return;
159 app
.uploadStatus
.busy
= true;
160 app
.uploadStatus
.count
= files
.length
;
161 app
.uploadStatus
.size
= 0;
162 app
.uploadStatus
.done
= 0;
163 app
.uploadStatus
.percentDone
= 0;
165 for (var i
= 0; i
< files
.length
; ++i
) {
166 app
.uploadStatus
.size
+= files
[i
].size
;
169 asyncForEach(files
, function (file
, callback
) {
170 var path
= encode(sanitize(app
.path
+ '/' + (file
.webkitRelativePath
|| file
.name
)));
172 var formData
= new FormData();
173 formData
.append('file', file
);
175 var finishedUploadSize
= app
.uploadStatus
.done
;
177 superagent
.post('/api/files' + path
)
178 .query({ access_token: localStorage
.accessToken
})
180 .on('progress', function (event
) {
181 // only handle upload events
182 if (!(event
.target
instanceof XMLHttpRequestUpload
)) return;
184 app
.uploadStatus
.done
= finishedUploadSize
+ event
.loaded
;
185 var tmp
= Math
.round(app
.uploadStatus
.done
/ app
.uploadStatus
.size
* 100);
186 app
.uploadStatus
.percentDone
= tmp
> 100 ? 100 : tmp
;
187 }).end(function (error
, result
) {
188 if (result
&& result
.statusCode
=== 401) return logout();
189 if (result
&& result
.statusCode
!== 201) return callback('Error uploading file: ', result
.statusCode
);
190 if (error
) return callback(error
);
194 }, function (error
) {
195 if (error
) console
.error(error
);
197 app
.uploadStatus
.busy
= false;
198 app
.uploadStatus
.count
= 0;
199 app
.uploadStatus
.size
= 0;
200 app
.uploadStatus
.done
= 0;
201 app
.uploadStatus
.percentDone
= 100;
207 function dragOver(event
) {
208 event
.stopPropagation();
209 event
.preventDefault();
210 event
.dataTransfer
.dropEffect
= 'copy';
213 function drop(event
) {
214 event
.stopPropagation();
215 event
.preventDefault();
217 if (!event
.dataTransfer
.items
[0]) return;
219 // figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
222 folderItem
= event
.dataTransfer
.items
[0].webkitGetAsEntry();
223 if (folderItem
.isFile
) return uploadFiles(event
.dataTransfer
.files
);
225 return uploadFiles(event
.dataTransfer
.files
);
228 // if we got here we have a folder drop and a modern browser
229 // now traverse the folder tree and create a file list
230 app
.uploadStatus
.busy
= true;
231 app
.uploadStatus
.uploadListCount
= 0;
234 function traverseFileTree(item
, path
, callback
) {
237 item
.file(function (file
) {
239 ++app
.uploadStatus
.uploadListCount
;
242 } else if (item
.isDirectory
) {
243 // Get folder contents
244 var dirReader
= item
.createReader();
245 dirReader
.readEntries(function (entries
) {
246 asyncForEach(entries
, function (entry
, callback
) {
247 traverseFileTree(entry
, path
+ item
.name
+ '/', callback
);
253 traverseFileTree(folderItem
, '', function (error
) {
254 app
.uploadStatus
.busy
= false;
255 app
.uploadStatus
.uploadListCount
= 0;
257 if (error
) return console
.error(error
);
259 uploadFiles(fileList
);
268 origin: window
.location
.origin
,
281 folderListingEnabled: false,
287 previewDrawerVisible: false,
291 accessTokensDialogVisible: false
294 onLogin: function () {
297 that
.loginData
.busy
= true;
299 superagent
.post('/api/login').send({ username: that
.loginData
.username
, password: that
.loginData
.password
}).end(function (error
, result
) {
300 that
.loginData
.busy
= false;
302 if (error
&& !result
) return that
.$message
.error(error
.message
);
303 if (result
.statusCode
=== 401) return that
.$message
.error('Wrong username or password');
305 initWithToken(result
.body
.accessToken
);
308 onOptionsMenu: function (command
) {
309 if (command
=== 'folderListing') {
310 superagent
.put('/api/settings').send({ folderListingEnabled: this.folderListingEnabled
}).query({ access_token: localStorage
.accessToken
}).end(function (error
) {
311 if (error
) console
.error(error
);
313 } else if (command
=== 'about') {
315 title: 'About Surfer',
316 message: 'Surfer is a static file server written by <a href="https://cloudron.io" target="_blank">Cloudron</a>.<br/><br/>The source code is licensed under MIT and available <a href="https://git.cloudron.io/cloudron/surfer" target="_blank">here</a>.',
317 dangerouslyUseHTMLString: true,
318 confirmButtonText: 'OK',
319 showCancelButton: false,
322 }).then(function () {}).catch(function () {});
323 } else if (command
=== 'logout') {
325 } else if (command
=== 'apiAccess') {
326 this.accessTokensDialogVisible
= true;
329 onDownload: function (entry
) {
330 if (entry
.isDirectory
) return;
331 window
.location
.href
= encode('/api/files/' + sanitize(this.path
+ '/' + entry
.filePath
)) + '?access_token=' + localStorage
.accessToken
;
333 onUpload: function () {
336 $(this.$refs
.upload
).on('change', function () {
337 // detach event handler
338 $(that
.$refs
.upload
).off('change');
339 uploadFiles(that
.$refs
.upload
.files
|| []);
342 // reset the form first to make the change handler retrigger even on the same file selected
343 this.$refs
.upload
.value
= '';
344 this.$refs
.upload
.click();
346 onUploadFolder: function () {
349 $(this.$refs
.uploadFolder
).on('change', function () {
350 // detach event handler
351 $(that
.$refs
.uploadFolder
).off('change');
352 uploadFiles(that
.$refs
.uploadFolder
.files
|| []);
355 // reset the form first to make the change handler retrigger even on the same file selected
356 this.$refs
.uploadFolder
.value
= '';
357 this.$refs
.uploadFolder
.click();
359 onDelete: function (entry
) {
362 var title
= 'Really delete ' + (entry
.isDirectory
? 'folder ' : '') + entry
.filePath
;
363 this.$confirm('', title
, { confirmButtonText: 'Yes', cancelButtonText: 'No' }).then(function () {
364 var path
= encode(sanitize(that
.path
+ '/' + entry
.filePath
));
366 superagent
.del('/api/files' + path
).query({ access_token: localStorage
.accessToken
, recursive: true }).end(function (error
, result
) {
367 if (result
&& result
.statusCode
=== 401) return logout();
368 if (result
&& result
.statusCode
!== 200) return that
.$message
.error('Error deleting file: ' + result
.statusCode
);
369 if (error
) return that
.$message
.error(error
.message
);
373 }).catch(function () {});
375 onRename: function (entry
, scope
) {
376 if (entry
.rename
) return entry
.rename
= false;
380 Vue
.nextTick(function () {
381 var elem
= document
.getElementById('filePathRenameInputId-' + scope
.$index
);
384 if (typeof elem
.selectionStart
!= "undefined") {
385 elem
.selectionStart
= 0;
386 elem
.selectionEnd
= entry
.filePath
.lastIndexOf('.');
390 onRenameEnd: function (entry
) {
391 entry
.rename
= false;
392 entry
.filePathNew
= entry
.filePath
;
394 onRenameSubmit: function (entry
) {
397 entry
.rename
= false;
399 if (entry
.filePathNew
=== entry
.filePath
) return;
401 var path
= encode(sanitize(this.path
+ '/' + entry
.filePath
));
402 var newFilePath
= sanitize(this.path
+ '/' + entry
.filePathNew
);
404 superagent
.put('/api/files' + path
).query({ access_token: localStorage
.accessToken
}).send({ newFilePath: newFilePath
}).end(function (error
, result
) {
405 if (result
&& result
.statusCode
=== 401) return logout();
406 if (result
&& result
.statusCode
!== 200) return that
.$message
.error('Error renaming file: ' + result
.statusCode
);
407 if (error
) return that
.$message
.error(error
.message
);
409 entry
.filePath
= entry
.filePathNew
;
412 onNewFolder: function () {
415 var title
= 'Create New Folder';
416 this.$prompt('', title
, { confirmButtonText: 'Yes', cancelButtonText: 'No', inputPlaceholder: 'new foldername' }).then(function (data
) {
417 var path
= encode(sanitize(that
.path
+ '/' + data
.value
));
419 superagent
.post('/api/files' + path
).query({ access_token: localStorage
.accessToken
, directory: true }).end(function (error
, result
) {
420 if (result
&& result
.statusCode
=== 401) return logout();
421 if (result
&& result
.statusCode
=== 403) return that
.$message
.error('Folder name not allowed');
422 if (result
&& result
.statusCode
=== 409) return that
.$message
.error('Folder already exists');
423 if (result
&& result
.statusCode
!== 201) return that
.$message
.error('Error creating folder: ' + result
.statusCode
);
424 if (error
) return that
.$message
.error(error
.message
);
428 }).catch(function () {});
430 refreshAccessTokens: function () {
433 superagent
.get('/api/tokens').query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
434 if (error
&& !result
) return that
.$message
.error(error
.message
);
436 that
.accessTokens
= result
.body
.accessTokens
;
439 onCopyAccessToken: function (event
) {
440 event
.target
.select();
441 document
.execCommand('copy');
443 this.$message({ type: 'success', message: 'Access token copied to clipboard' });
445 onCreateAccessToken: function () {
448 superagent
.post('/api/tokens').query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
449 if (error
&& !result
) return that
.$message
.error(error
.message
);
451 that
.refreshAccessTokens();
454 onDeleteAccessToken: function (token
) {
457 this.$confirm('All actions from apps using this token will fail!', 'Really delete this access token?', { confirmButtonText: 'Yes Delete', cancelButtonText: 'No' }).then(function () {
458 superagent
.delete('/api/tokens/' + token
).query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
459 if (error
&& !result
) return that
.$message
.error(error
.message
);
461 that
.refreshAccessTokens();
463 }).catch(function () {});
466 prettyDate: function (row
, column
, cellValue
, index
) {
467 var date
= new Date(cellValue
),
468 diff
= (((new Date()).getTime() - date
.getTime()) / 1000),
469 day_diff
= Math
.floor(diff
/ 86400);
471 if (isNaN(day_diff
) || day_diff
< 0)
474 return day_diff
=== 0 && (
475 diff
< 60 && 'just now' ||
476 diff
< 120 && '1 minute ago' ||
477 diff
< 3600 && Math
.floor( diff
/ 60 ) + ' minutes ago' ||
478 diff
< 7200 && '1 hour ago' ||
479 diff
< 86400 && Math
.floor( diff
/ 3600 ) + ' hours ago') ||
480 day_diff
=== 1 && 'Yesterday' ||
481 day_diff
< 7 && day_diff
+ ' days ago' ||
482 day_diff
< 31 && Math
.ceil( day_diff
/ 7 ) + ' weeks ago' ||
483 day_diff
< 365 && Math
.round( day_diff
/ 30 ) + ' months ago' ||
484 Math
.round( day_diff
/ 365 ) + ' years ago';
486 prettyFileSize: function (row
, column
, cellValue
, index
) {
487 return filesize(cellValue
);
489 loadDirectory: loadDirectory
,
491 window
.location
.hash
= sanitize(this.path
.split('/').slice(0, -1).filter(function (p
) { return !!p
; }).join('/'));
499 initWithToken(localStorage
.accessToken
);
501 $(window
).on('hashchange', function () {
502 loadDirectory(decode(window
.location
.hash
.slice(1)));