diff --git a/client/src/components/files.vue b/client/src/components/files.vue
index 6dc82cf1..1c99496f 100644
--- a/client/src/components/files.vue
+++ b/client/src/components/files.vue
@@ -8,13 +8,28 @@
{{ item.name }}
+
{{ fileSize(item.size) }}
download(item)" />
-
-
-
Drag files here to upload
+
+
+
Downloads
+
+
+
{{ download.name }}
+
{{ Math.max(100, Math.round(download.progress / download.size * 100))}}%
+
removeTransfer(download)">
+
+
+
+
+
+
+
Drag files here to upload
+
@@ -79,30 +94,63 @@
border-bottom: 0px;
}
- .refresh, .download {
+ .refresh {
margin-left: auto;
}
- .refresh:hover, .download:hover {
+ .file-size {
+ margin-left: auto;
+ margin-right: 0.5em;
+ color: rgba($color: #fff, $alpha: 0.40);
+ }
+
+ .refresh:hover, .download:hover, .remove-transfer:hover {
cursor: pointer;
}
- .files-transfer {
- display: flex;
- flex-direction: column;
- text-align: center;
- justify-content: center;
- margin: auto 10px 10px 10px;
+ .transfer-area {
+ margin-top: auto;
+ }
+
+ .transfers {
+ margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
}
- .files-transfer > i {
+ .transfers > p {
+ padding: 10px;
+ font-weight: 600;
+ }
+
+ .transfer-info {
+ display: flex;
+ flex-direction: row;
+ max-width: 100%;
+ padding: 10px;
+ }
+
+ .transfer-progress {
+ margin: 0px 10px 10px 10px;
+ width: 95%;
+ }
+
+ .upload-area {
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ justify-content: center;
+ margin: 10px 10px 10px 10px;
+ background-color: rgba($color: #fff, $alpha: 0.05);
+ border-radius: 5px;
+ }
+
+ .upload-area > i {
font-size: 4em;
margin: 10px 10px 10px 10px;
}
- .files-transfer > p {
+ .upload-area > p {
margin: 0px 10px 10px 10px;
}
@@ -115,6 +163,7 @@
import Markdown from './markdown'
import Content from './context.vue'
+import { FileTransfer } from '~/neko/types'
@Component({
name: 'neko-files',
@@ -132,13 +181,68 @@
get files() {
return this.$accessor.files.files
}
+
+ get transfers() {
+ return this.$accessor.files.transfers
+ }
+
+ get downloads() {
+ return this.$accessor.files.transfers.filter((t => t.direction === 'download'))
+ }
+
+ get uploads() {
+ return this.$accessor.files.transfers.filter((t => t.direction === 'upload'))
+ }
refresh() {
this.$accessor.files.refresh()
}
download(item: any) {
- console.log(item.name);
+ const url = `/file?pwd=${this.$accessor.password}&filename=${item.name}`
+ let transfer: FileTransfer = {
+ id: Math.round(Math.random() * 10000),
+ name: item.name,
+ direction: 'download',
+ // this is just an estimation, but for large files the content length
+ // is not sent (chunked transfer)
+ size: item.size,
+ progress: 0,
+ status: 'pending',
+ axios: null,
+ // TODO add support for aborting in progress requests, requires axios >=0.22
+ abortController: null
+ }
+ transfer.axios = this.$http.get(url, {
+ responseType: 'blob',
+ onDownloadProgress: (x) => {
+ transfer.progress = x.loaded
+
+ if (x.lengthComputable) {
+ transfer.size = x.total
+ }
+ if (transfer.progress === transfer.size) {
+ transfer.status = 'completed'
+ }
+ }
+ }).then((res) => {
+ const url = window.URL
+ .createObjectURL(new Blob([res.data]))
+ const link = document.createElement('a')
+ link.href = url
+ link.setAttribute('download', item.name)
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ }).catch((err) => {
+ this.$log.error(err)
+ })
+ this.$accessor.files.addTransfer(transfer)
+ }
+
+ removeTransfer(item: FileTransfer) {
+ console.log(item)
+ this.$accessor.files.removeTransfer(item)
}
fileIcon(file: any) {
@@ -169,6 +273,22 @@
return className;
}
+ fileSize(size: number) {
+ if (size < 1000) {
+ return `${size} b`
+ }
+ if (size < 1000 ** 2) {
+ return `${(size / 1000).toFixed(2)} kb`
+ }
+ if (size < 1000 ** 3) {
+ return `${(size / 1000 ** 2).toFixed(2)} mb`
+ }
+ if (size < 1000 ** 4) {
+ return `${(size / 1000 ** 3).toFixed(2)} gb`
+ }
+ return `${(size / 1000 ** 4).toFixed(3)} tb`
+ }
+
onFileDrop(e: any) {
console.log('file dropped', e)
console.log(e.dataTransfer.files)
diff --git a/client/src/neko/base.ts b/client/src/neko/base.ts
index dc767b5b..31ec8add 100644
--- a/client/src/neko/base.ts
+++ b/client/src/neko/base.ts
@@ -187,14 +187,6 @@ export abstract class BaseClient extends EventEmitter {
this._ws!.send(JSON.stringify({ event, ...payload }))
}
- public refreshFiles() {
- if (!this.connected) {
- this.emit('warn', 'attempting to refresh files while disconnected')
- }
- this.emit('debug', `sending event '${EVENT.FILETRANSFER.REFRESH}'`)
- this._ws!.send(JSON.stringify({ event: EVENT.FILETRANSFER.REFRESH }))
- }
-
public async createPeer(lite: boolean, servers: RTCIceServer[]) {
this.emit('debug', `creating peer`)
if (!this.socketOpen) {
diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts
index a066b3b9..15033db3 100644
--- a/client/src/neko/index.ts
+++ b/client/src/neko/index.ts
@@ -71,6 +71,14 @@ export class NekoClient extends BaseClient implements EventEmitter {
})
}
+ public refreshFiles() {
+ if (!this.connected) {
+ this.emit('warn', 'attempting to refresh files while disconnected')
+ }
+ this.emit('debug', `sending event '${EVENT.FILETRANSFER.REFRESH}'`)
+ this._ws!.send(JSON.stringify({ event: EVENT.FILETRANSFER.REFRESH }))
+ }
+
/////////////////////////////
// Internal Events
/////////////////////////////
diff --git a/client/src/neko/types.ts b/client/src/neko/types.ts
index 938fc8cd..31bb44ab 100644
--- a/client/src/neko/types.ts
+++ b/client/src/neko/types.ts
@@ -25,5 +25,17 @@ export interface ScreenResolution {
export interface FileListItem {
name: string,
- type: 'file' | 'dir'
+ type: 'file' | 'dir',
+ size: number
+}
+
+export interface FileTransfer {
+ id: number,
+ name: string,
+ direction: 'upload' | 'download',
+ size: number,
+ progress: number,
+ status: 'pending' | 'inprogress' | 'completed',
+ axios: Promise | null,
+ abortController: AbortController | null
}
diff --git a/client/src/store/files.ts b/client/src/store/files.ts
index 9c8ce065..5eb2c1f7 100644
--- a/client/src/store/files.ts
+++ b/client/src/store/files.ts
@@ -1,10 +1,11 @@
import { actionTree, getterTree, mutationTree } from 'typed-vuex'
-import { FileListItem } from '~/neko/types'
+import { FileListItem, FileTransfer } from '~/neko/types'
import { accessor } from '~/store'
export const state = () => ({
cwd: '',
- files: [] as FileListItem[]
+ files: [] as FileListItem[],
+ transfers: [] as FileTransfer[]
})
export const getters = getterTree(state, {
@@ -18,6 +19,14 @@ export const mutations = mutationTree(state, {
_setFileList(state, files: FileListItem[]) {
state.files = files
+ },
+
+ _addTransfer(state, transfer: FileTransfer) {
+ state.transfers = [...state.transfers, transfer]
+ },
+
+ _removeTransfer(state, transfer: FileTransfer) {
+ state.transfers = state.transfers.filter((t) => t.id !== transfer.id)
}
})
@@ -32,6 +41,17 @@ export const actions = actionTree(
accessor.files._setFileList(files)
},
+ addTransfer(store, transfer: FileTransfer) {
+ if (transfer.status !== 'pending') {
+ return
+ }
+ accessor.files._addTransfer(transfer)
+ },
+
+ removeTransfer(store, transfer: FileTransfer) {
+ accessor.files._removeTransfer(transfer)
+ },
+
refresh(store) {
if (!accessor.connected) {
return
diff --git a/server/internal/http/http.go b/server/internal/http/http.go
index 4b8989c4..82eea6b4 100644
--- a/server/internal/http/http.go
+++ b/server/internal/http/http.go
@@ -35,6 +35,7 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
router.Use(middleware.RequestID) // Create a request ID for each request
router.Use(middleware.RequestLogger(&logformatter{logger}))
router.Use(middleware.Recoverer) // Recover from panics without crashing server
+ router.Use(middleware.Compress(5, "application/octet-stream"))
if conf.PathPrefix != "/" {
router.Use(func(h http.Handler) http.Handler {
diff --git a/server/internal/types/websocket.go b/server/internal/types/websocket.go
index 2fa14796..cb80fccd 100644
--- a/server/internal/types/websocket.go
+++ b/server/internal/types/websocket.go
@@ -41,4 +41,5 @@ type WebSocketHandler interface {
type FileListItem struct {
Filename string `json:"name"`
Type string `json:"type"`
+ Size int64 `json:"size"`
}
diff --git a/server/internal/utils/files.go b/server/internal/utils/files.go
index 7ac3005f..56c714d7 100644
--- a/server/internal/utils/files.go
+++ b/server/internal/utils/files.go
@@ -15,14 +15,20 @@ func ListFiles(path string) (*[]types.FileListItem, error) {
out := make([]types.FileListItem, len(items))
for i, item := range items {
var itemType string = ""
+ var size int64 = 0
if item.IsDir() {
itemType = "dir"
} else {
itemType = "file"
+ info, err := item.Info()
+ if err == nil {
+ size = info.Size()
+ }
}
out[i] = types.FileListItem{
Filename: item.Name(),
Type: itemType,
+ Size: size,
}
}