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 window
.open(encode(path
));
152 function uploadFiles(files
) {
153 if (!files
|| !files
.length
) return;
155 app
.uploadStatus
.busy
= true;
156 app
.uploadStatus
.count
= files
.length
;
157 app
.uploadStatus
.size
= 0;
158 app
.uploadStatus
.done
= 0;
159 app
.uploadStatus
.percentDone
= 0;
161 for (var i
= 0; i
< files
.length
; ++i
) {
162 app
.uploadStatus
.size
+= files
[i
].size
;
165 asyncForEach(files
, function (file
, callback
) {
166 var path
= encode(sanitize(app
.path
+ '/' + (file
.webkitRelativePath
|| file
.name
)));
168 var formData
= new FormData();
169 formData
.append('file', file
);
171 var finishedUploadSize
= app
.uploadStatus
.done
;
173 superagent
.post('/api/files' + path
)
174 .query({ access_token: localStorage
.accessToken
})
176 .on('progress', function (event
) {
177 // only handle upload events
178 if (!(event
.target
instanceof XMLHttpRequestUpload
)) return;
180 app
.uploadStatus
.done
= finishedUploadSize
+ event
.loaded
;
181 var tmp
= Math
.round(app
.uploadStatus
.done
/ app
.uploadStatus
.size
* 100);
182 app
.uploadStatus
.percentDone
= tmp
> 100 ? 100 : tmp
;
183 }).end(function (error
, result
) {
184 if (result
&& result
.statusCode
=== 401) return logout();
185 if (result
&& result
.statusCode
!== 201) return callback('Error uploading file: ', result
.statusCode
);
186 if (error
) return callback(error
);
190 }, function (error
) {
191 if (error
) console
.error(error
);
193 app
.uploadStatus
.busy
= false;
194 app
.uploadStatus
.count
= 0;
195 app
.uploadStatus
.size
= 0;
196 app
.uploadStatus
.done
= 0;
197 app
.uploadStatus
.percentDone
= 100;
203 function dragOver(event
) {
204 event
.stopPropagation();
205 event
.preventDefault();
206 event
.dataTransfer
.dropEffect
= 'copy';
209 function drop(event
) {
210 event
.stopPropagation();
211 event
.preventDefault();
213 if (!event
.dataTransfer
.items
[0]) return;
215 // figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
218 folderItem
= event
.dataTransfer
.items
[0].webkitGetAsEntry();
219 if (folderItem
.isFile
) return uploadFiles(event
.dataTransfer
.files
);
221 return uploadFiles(event
.dataTransfer
.files
);
224 // if we got here we have a folder drop and a modern browser
225 // now traverse the folder tree and create a file list
226 app
.uploadStatus
.busy
= true;
227 app
.uploadStatus
.uploadListCount
= 0;
230 function traverseFileTree(item
, path
, callback
) {
233 item
.file(function (file
) {
235 ++app
.uploadStatus
.uploadListCount
;
238 } else if (item
.isDirectory
) {
239 // Get folder contents
240 var dirReader
= item
.createReader();
241 dirReader
.readEntries(function (entries
) {
242 asyncForEach(entries
, function (entry
, callback
) {
243 traverseFileTree(entry
, path
+ item
.name
+ '/', callback
);
249 traverseFileTree(folderItem
, '', function (error
) {
250 app
.uploadStatus
.busy
= false;
251 app
.uploadStatus
.uploadListCount
= 0;
253 if (error
) return console
.error(error
);
255 uploadFiles(fileList
);
264 origin: window
.location
.origin
,
277 folderListingEnabled: false,
285 accessTokensDialogVisible: false
288 onLogin: function () {
291 that
.loginData
.busy
= true;
293 superagent
.post('/api/login').send({ username: that
.loginData
.username
, password: that
.loginData
.password
}).end(function (error
, result
) {
294 that
.loginData
.busy
= false;
296 if (error
&& !result
) return that
.$message
.error(error
.message
);
297 if (result
.statusCode
=== 401) return that
.$message
.error('Wrong username or password');
299 initWithToken(result
.body
.accessToken
);
302 onOptionsMenu: function (command
) {
303 if (command
=== 'folderListing') {
304 superagent
.put('/api/settings').send({ folderListingEnabled: this.folderListingEnabled
}).query({ access_token: localStorage
.accessToken
}).end(function (error
) {
305 if (error
) console
.error(error
);
307 } else if (command
=== 'about') {
309 title: 'About Surfer',
310 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>.',
311 dangerouslyUseHTMLString: true,
312 confirmButtonText: 'OK',
313 showCancelButton: false,
316 }).then(function () {}).catch(function () {});
317 } else if (command
=== 'logout') {
319 } else if (command
=== 'apiAccess') {
320 this.accessTokensDialogVisible
= true;
323 onDownload: function (entry
) {
324 if (entry
.isDirectory
) return;
325 window
.location
.href
= encode('/api/files/' + sanitize(this.path
+ '/' + entry
.filePath
)) + '?access_token=' + localStorage
.accessToken
;
327 onUpload: function () {
330 $(this.$refs
.upload
).on('change', function () {
331 // detach event handler
332 $(that
.$refs
.upload
).off('change');
333 uploadFiles(that
.$refs
.upload
.files
|| []);
336 // reset the form first to make the change handler retrigger even on the same file selected
337 this.$refs
.upload
.value
= '';
338 this.$refs
.upload
.click();
340 onUploadFolder: function () {
343 $(this.$refs
.uploadFolder
).on('change', function () {
344 // detach event handler
345 $(that
.$refs
.uploadFolder
).off('change');
346 uploadFiles(that
.$refs
.uploadFolder
.files
|| []);
349 // reset the form first to make the change handler retrigger even on the same file selected
350 this.$refs
.uploadFolder
.value
= '';
351 this.$refs
.uploadFolder
.click();
353 onDelete: function (entry
) {
356 var title
= 'Really delete ' + (entry
.isDirectory
? 'folder ' : '') + entry
.filePath
;
357 this.$confirm('', title
, { confirmButtonText: 'Yes', cancelButtonText: 'No' }).then(function () {
358 var path
= encode(sanitize(that
.path
+ '/' + entry
.filePath
));
360 superagent
.del('/api/files' + path
).query({ access_token: localStorage
.accessToken
, recursive: true }).end(function (error
, result
) {
361 if (result
&& result
.statusCode
=== 401) return logout();
362 if (result
&& result
.statusCode
!== 200) return that
.$message
.error('Error deleting file: ' + result
.statusCode
);
363 if (error
) return that
.$message
.error(error
.message
);
367 }).catch(function () {});
369 onRename: function (entry
, scope
) {
370 if (entry
.rename
) return entry
.rename
= false;
374 Vue
.nextTick(function () {
375 var elem
= document
.getElementById('filePathRenameInputId-' + scope
.$index
);
378 if (typeof elem
.selectionStart
!= "undefined") {
379 elem
.selectionStart
= 0;
380 elem
.selectionEnd
= entry
.filePath
.lastIndexOf('.');
384 onRenameEnd: function (entry
) {
385 entry
.rename
= false;
386 entry
.filePathNew
= entry
.filePath
;
388 onRenameSubmit: function (entry
) {
391 entry
.rename
= false;
393 if (entry
.filePathNew
=== entry
.filePath
) return;
395 var path
= encode(sanitize(this.path
+ '/' + entry
.filePath
));
396 var newFilePath
= sanitize(this.path
+ '/' + entry
.filePathNew
);
398 superagent
.put('/api/files' + path
).query({ access_token: localStorage
.accessToken
}).send({ newFilePath: newFilePath
}).end(function (error
, result
) {
399 if (result
&& result
.statusCode
=== 401) return logout();
400 if (result
&& result
.statusCode
!== 200) return that
.$message
.error('Error renaming file: ' + result
.statusCode
);
401 if (error
) return that
.$message
.error(error
.message
);
403 entry
.filePath
= entry
.filePathNew
;
406 onNewFolder: function () {
409 var title
= 'Create New Folder';
410 this.$prompt('', title
, { confirmButtonText: 'Yes', cancelButtonText: 'No', inputPlaceholder: 'new foldername' }).then(function (data
) {
411 var path
= encode(sanitize(that
.path
+ '/' + data
.value
));
413 superagent
.post('/api/files' + path
).query({ access_token: localStorage
.accessToken
, directory: true }).end(function (error
, result
) {
414 if (result
&& result
.statusCode
=== 401) return logout();
415 if (result
&& result
.statusCode
=== 403) return that
.$message
.error('Folder name not allowed');
416 if (result
&& result
.statusCode
=== 409) return that
.$message
.error('Folder already exists');
417 if (result
&& result
.statusCode
!== 201) return that
.$message
.error('Error creating folder: ' + result
.statusCode
);
418 if (error
) return that
.$message
.error(error
.message
);
422 }).catch(function () {});
424 refreshAccessTokens: function () {
427 superagent
.get('/api/tokens').query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
428 if (error
&& !result
) return that
.$message
.error(error
.message
);
430 that
.accessTokens
= result
.body
.accessTokens
;
433 onCopyAccessToken: function (event
) {
434 event
.target
.select();
435 document
.execCommand('copy');
437 this.$message({ type: 'success', message: 'Access token copied to clipboard' });
439 onCreateAccessToken: function () {
442 superagent
.post('/api/tokens').query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
443 if (error
&& !result
) return that
.$message
.error(error
.message
);
445 that
.refreshAccessTokens();
448 onDeleteAccessToken: function (token
) {
451 this.$confirm('All actions from apps using this token will fail!', 'Really delete this access token?', { confirmButtonText: 'Yes Delete', cancelButtonText: 'No' }).then(function () {
452 superagent
.delete('/api/tokens/' + token
).query({ access_token: localStorage
.accessToken
}).end(function (error
, result
) {
453 if (error
&& !result
) return that
.$message
.error(error
.message
);
455 that
.refreshAccessTokens();
457 }).catch(function () {});
460 prettyDate: function (row
, column
, cellValue
, index
) {
461 var date
= new Date(cellValue
),
462 diff
= (((new Date()).getTime() - date
.getTime()) / 1000),
463 day_diff
= Math
.floor(diff
/ 86400);
465 if (isNaN(day_diff
) || day_diff
< 0)
468 return day_diff
=== 0 && (
469 diff
< 60 && 'just now' ||
470 diff
< 120 && '1 minute ago' ||
471 diff
< 3600 && Math
.floor( diff
/ 60 ) + ' minutes ago' ||
472 diff
< 7200 && '1 hour ago' ||
473 diff
< 86400 && Math
.floor( diff
/ 3600 ) + ' hours ago') ||
474 day_diff
=== 1 && 'Yesterday' ||
475 day_diff
< 7 && day_diff
+ ' days ago' ||
476 day_diff
< 31 && Math
.ceil( day_diff
/ 7 ) + ' weeks ago' ||
477 day_diff
< 365 && Math
.round( day_diff
/ 30 ) + ' months ago' ||
478 Math
.round( day_diff
/ 365 ) + ' years ago';
480 prettyFileSize: function (row
, column
, cellValue
, index
) {
481 return filesize(cellValue
);
483 loadDirectory: loadDirectory
,
485 window
.location
.hash
= sanitize(this.path
.split('/').slice(0, -1).filter(function (p
) { return !!p
; }).join('/'));
493 initWithToken(localStorage
.accessToken
);
495 $(window
).on('hashchange', function () {
496 loadDirectory(decode(window
.location
.hash
.slice(1)));