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 // need to wait for DOM element to exist
154 setTimeout(function () {
155 $('iframe').on('load', function (e
) {
156 if (!e
.target
.contentWindow
.document
.body
) return;
158 e
.target
.contentWindow
.document
.body
.style
.display
= 'flex'
159 e
.target
.contentWindow
.document
.body
.style
.justifyContent
= 'center'
164 function uploadFiles(files
) {
165 if (!files
|| !files
.length
) return;
167 app
.uploadStatus
.busy
= true;
168 app
.uploadStatus
.count
= files
.length
;
169 app
.uploadStatus
.size
= 0;
170 app
.uploadStatus
.done
= 0;
171 app
.uploadStatus
.percentDone
= 0;
173 for (var i
= 0; i
< files
.length
; ++i
) {
174 app
.uploadStatus
.size
+= files
[i
].size
;
177 asyncForEach(files
, function (file
, callback
) {
178 var path
= encode(sanitize(app
.path
+ '/' + (file
.webkitRelativePath
|| file
.name
)));
180 var formData
= new FormData();
181 formData
.append('file', file
);
183 var finishedUploadSize
= app
.uploadStatus
.done
;
185 superagent
.post('/api/files' + path
)
186 .query({ access_token: localStorage
.accessToken
})
188 .on('progress', function (event
) {
189 // only handle upload events
190 if (!(event
.target
instanceof XMLHttpRequestUpload
)) return;
192 app
.uploadStatus
.done
= finishedUploadSize
+ event
.loaded
;
193 var tmp
= Math
.round(app
.uploadStatus
.done
/ app
.uploadStatus
.size
* 100);
194 app
.uploadStatus
.percentDone
= tmp
> 100 ? 100 : tmp
;
195 }).end(function (error
, result
) {
196 if (result
&& result
.statusCode
=== 401) return logout();
197 if (result
&& result
.statusCode
!== 201) return callback('Error uploading file: ', result
.statusCode
);
198 if (error
) return callback(error
);
202 }, function (error
) {
203 if (error
) console
.error(error
);
205 app
.uploadStatus
.busy
= false;
206 app
.uploadStatus
.count
= 0;
207 app
.uploadStatus
.size
= 0;
208 app
.uploadStatus
.done
= 0;
209 app
.uploadStatus
.percentDone
= 100;
215 function dragOver(event
) {
216 event
.stopPropagation();
217 event
.preventDefault();
218 event
.dataTransfer
.dropEffect
= 'copy';
221 function drop(event
) {
222 event
.stopPropagation();
223 event
.preventDefault();
225 if (!event
.dataTransfer
.items
[0]) return;
227 // figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
230 folderItem
= event
.dataTransfer
.items
[0].webkitGetAsEntry();
231 if (folderItem
.isFile
) return uploadFiles(event
.dataTransfer
.files
);
233 return uploadFiles(event
.dataTransfer
.files
);
236 // if we got here we have a folder drop and a modern browser
237 // now traverse the folder tree and create a file list
238 app
.uploadStatus
.busy
= true;
239 app
.uploadStatus
.uploadListCount
= 0;
242 function traverseFileTree(item
, path
, callback
) {
245 item
.file(function (file
) {
247 ++app
.uploadStatus
.uploadListCount
;
250 } else if (item
.isDirectory
) {
251 // Get folder contents
252 var dirReader
= item
.createReader();
253 dirReader
.readEntries(function (entries
) {
254 asyncForEach(entries
, function (entry
, callback
) {
255 traverseFileTree(entry
, path
+ item
.name
+ '/', callback
);
261 traverseFileTree(folderItem
, '', function (error
) {
262 app
.uploadStatus
.busy
= false;
263 app
.uploadStatus
.uploadListCount
= 0;
265 if (error
) return console
.error(error
);
267 uploadFiles(fileList
);
276 origin: window
.location
.origin
,
289 folderListingEnabled: false,
295 previewDrawerVisible: false,
299 accessTokensDialogVisible: false
302 onLogin: function () {
305 that
.loginData
.busy
= true;
307 superagent
.post('/api/login').send({ username: that
.loginData
.username
, password: that
.loginData
.password
}).end(function (error
, result
) {
308 that
.loginData
.busy
= false;
310 if (error
&& !result
) return that
.$message
.error(error
.message
);
311 if (result
.statusCode
=== 401) return that
.$message
.error('Wrong username or password');
313 initWithToken(result
.body
.accessToken
);
316 onOptionsMenu: function (command
) {
317 if (command
=== 'folderListing') {
318 superagent
.put('/api/settings').send({ folderListingEnabled: this.folderListingEnabled
}).query({ access_token: localStorage
.accessToken
}).end(function (error
) {
319 if (error
) console
.error(error
);
321 } else if (command
=== 'about') {
323 title: 'About Surfer',
324 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>.',
325 dangerouslyUseHTMLString: true,
326 confirmButtonText: 'OK',
327 showCancelButton: false,
330 }).then(function () {}).catch(function () {});
331 } else if (command
=== 'logout') {
333 } else if (command
=== 'apiAccess') {
334 this.accessTokensDialogVisible
= true;
337 onDownload: function (entry
) {
338 if (entry
.isDirectory
) return;
339 window
.location
.href
= encode('/api/files/' + sanitize(this.path
+ '/' + entry
.filePath
)) + '?access_token=' + localStorage
.accessToken
;
341 onUpload: function () {
344 $(this.$refs
.upload
).on('change', function () {
345 // detach event handler
346 $(that
.$refs
.upload
).off('change');
347 uploadFiles(that
.$refs
.upload
.files
|| []);
350 // reset the form first to make the change handler retrigger even on the same file selected
351 this.$refs
.upload
.value
= '';
352 this.$refs
.upload
.click();
354 onUploadFolder: function () {
357 $(this.$refs
.uploadFolder
).on('change', function () {
358 // detach event handler
359 $(that
.$refs
.uploadFolder
).off('change');
360 uploadFiles(that
.$refs
.uploadFolder
.files
|| []);
363 // reset the form first to make the change handler retrigger even on the same file selected
364 this.$refs
.uploadFolder
.value
= '';
365 this.$refs
.uploadFolder
.click();
367 onDelete: function (entry
) {
370 var title
= 'Really delete ' + (entry
.isDirectory
? 'folder ' : '') + entry
.filePath
;
371 this.$confirm('', title
, { confirmButtonText: 'Yes', cancelButtonText: 'No' }).then(function () {
372 var path
= encode(sanitize(that
.path
+ '/' + entry
.filePath
));
374 superagent
.del('/api/files' + path
).query({ access_token: localStorage
.accessToken
, recursive: true }).end(function (error
, result
) {
375 if (result
&& result
.statusCode
=== 401) return logout();
376 if (result
&& result
.statusCode
!== 200) return that
.$message
.error('Error deleting file: ' + result
.statusCode
);
377 if (error
) return that
.$message
.error(error
.message
);
381 }).catch(function () {});
383 onRename: function (entry
, scope
) {
384 if (entry
.rename
) return entry
.rename
= false;
388 Vue
.nextTick(function () {
389 var elem
= document
.getElementById('filePathRenameInputId-' + scope
.$index
);
392 if (typeof elem
.selectionStart
!= "undefined") {
393 elem
.selectionStart
= 0;
394 elem
.selectionEnd
= entry
.filePath
.lastIndexOf('.');
398 onRenameEnd: function (entry
) {
399 entry
.rename
= false;
400 entry
.filePathNew
= entry
.filePath
;
402 onRenameSubmit: function (entry
) {
405 entry
.rename
= false;
407 if (entry
.filePathNew
=== entry
.filePath
) return;
409 var path
= encode(sanitize(this.path
+ '/' + entry
.filePath
));
410 var newFilePath
= sanitize(this.path
+ '/' + entry
.filePathNew
);
412 superagent
.put('/api/files' + path
).query({ access_token: localStorage
.accessToken
}).send({ newFilePath: newFilePath
}).end(function (error
, result
) {
413 if (result
&& result
.statusCode
=== 401) return logout();
414 if (result
&& result
.statusCode
!== 200) return that
.$message
.error('Error renaming file: ' + result
.statusCode
);
415 if (error
) return that
.$message
.error(error
.message
);
417 entry
.filePath
= entry
.filePathNew
;
420 onNewFolder: function () {
423 var title
= 'Create New Folder';
424 this.$prompt('', title
, { confirmButtonText: 'Yes', cancelButtonText: 'No', inputPlaceholder: 'new foldername' }).then(function (data
) {
425 var path
= encode(sanitize(that
.path
+ '/' + data
.value
));
427 superagent
.post('/api/files' + path
).query({ access_token: localStorage
.accessToken
, directory: true }).end(function (error
, result
) {
428 if (result
&& result
.statusCode
=== 401) return logout();
429 if (result
&& result
.statusCode
=== 403) return that
.$message
.error('Folder name not allowed');
430 if (result
&& result
.statusCode
=== 409) return that
.$message
.error('Folder already exists');
431 if (result
&& result
.statusCode
!== 201) return that
.$message
.error('Error creating folder: ' + result
.statusCode
);
432 if (error
) return that
.$message
.error(error
.message
);
436 }).catch(function () {});
438 refreshAccessTokens: function () {
441 superagent
.get('/api/tokens').query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
442 if (error
&& !result
) return that
.$message
.error(error
.message
);
444 that
.accessTokens
= result
.body
.accessTokens
;
447 onCopyAccessToken: function (event
) {
448 event
.target
.select();
449 document
.execCommand('copy');
451 this.$message({ type: 'success', message: 'Access token copied to clipboard' });
453 onCreateAccessToken: function () {
456 superagent
.post('/api/tokens').query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
457 if (error
&& !result
) return that
.$message
.error(error
.message
);
459 that
.refreshAccessTokens();
462 onDeleteAccessToken: function (token
) {
465 this.$confirm('All actions from apps using this token will fail!', 'Really delete this access token?', { confirmButtonText: 'Yes Delete', cancelButtonText: 'No' }).then(function () {
466 superagent
.delete('/api/tokens/' + token
).query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
467 if (error
&& !result
) return that
.$message
.error(error
.message
);
469 that
.refreshAccessTokens();
471 }).catch(function () {});
474 prettyDate: function (row
, column
, cellValue
, index
) {
475 var date
= new Date(cellValue
),
476 diff
= (((new Date()).getTime() - date
.getTime()) / 1000),
477 day_diff
= Math
.floor(diff
/ 86400);
479 if (isNaN(day_diff
) || day_diff
< 0)
482 return day_diff
=== 0 && (
483 diff
< 60 && 'just now' ||
484 diff
< 120 && '1 minute ago' ||
485 diff
< 3600 && Math
.floor( diff
/ 60 ) + ' minutes ago' ||
486 diff
< 7200 && '1 hour ago' ||
487 diff
< 86400 && Math
.floor( diff
/ 3600 ) + ' hours ago') ||
488 day_diff
=== 1 && 'Yesterday' ||
489 day_diff
< 7 && day_diff
+ ' days ago' ||
490 day_diff
< 31 && Math
.ceil( day_diff
/ 7 ) + ' weeks ago' ||
491 day_diff
< 365 && Math
.round( day_diff
/ 30 ) + ' months ago' ||
492 Math
.round( day_diff
/ 365 ) + ' years ago';
494 prettyFileSize: function (row
, column
, cellValue
, index
) {
495 return filesize(cellValue
);
497 loadDirectory: loadDirectory
,
499 window
.location
.hash
= sanitize(this.path
.split('/').slice(0, -1).filter(function (p
) { return !!p
; }).join('/'));
508 initWithToken(localStorage
.accessToken
);
510 $(window
).on('hashchange', function () {
511 loadDirectory(decode(window
.location
.hash
.slice(1)));