added file downloads to frontend

This commit is contained in:
William Harrell 2022-11-11 20:27:15 -05:00
parent 7c6029aa99
commit b9f31cc19c
8 changed files with 185 additions and 25 deletions

View File

@ -8,15 +8,30 @@
<div v-for="item in files" :key="item.name" class="files-list-item"> <div v-for="item in files" :key="item.name" class="files-list-item">
<i :class="fileIcon(item)" /> <i :class="fileIcon(item)" />
<p>{{ item.name }}</p> <p>{{ item.name }}</p>
<p class="file-size">{{ fileSize(item.size) }}</p>
<i v-if="item.type !== 'dir'" class="fas fa-download download" <i v-if="item.type !== 'dir'" class="fas fa-download download"
@click="() => download(item)" /> @click="() => download(item)" />
</div> </div>
</div> </div>
<div class="files-transfer" @dragover.prevent @drop.prevent="onFileDrop"> <div class="transfer-area">
<div class="transfers" v-if="transfers.length > 0">
<p>Downloads</p>
<div v-for="download in downloads" :key="download.name" class="transfers-list-item">
<div class="transfer-info">
<p>{{ download.name }}</p>
<p class="file-size">{{ Math.max(100, Math.round(download.progress / download.size * 100))}}%</p>
<i class="fas fa-xmark remove-transfer" @click="() => removeTransfer(download)"></i>
</div>
<progress class="transfer-progress" :aria-label="download.name + ' progress'" :value="download.progress"
:max="download.size"></progress>
</div>
</div>
<div class="upload-area" @dragover.prevent @drop.prevent="onFileDrop">
<i class="fas fa-file-arrow-up" /> <i class="fas fa-file-arrow-up" />
<p>Drag files here to upload</p> <p>Drag files here to upload</p>
</div> </div>
</div> </div>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -79,30 +94,63 @@
border-bottom: 0px; border-bottom: 0px;
} }
.refresh, .download { .refresh {
margin-left: auto; 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; cursor: pointer;
} }
.files-transfer { .transfer-area {
display: flex; margin-top: auto;
flex-direction: column; }
text-align: center;
justify-content: center; .transfers {
margin: auto 10px 10px 10px; margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05); background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px; 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; font-size: 4em;
margin: 10px 10px 10px 10px; margin: 10px 10px 10px 10px;
} }
.files-transfer > p { .upload-area > p {
margin: 0px 10px 10px 10px; margin: 0px 10px 10px 10px;
} }
@ -115,6 +163,7 @@
import Markdown from './markdown' import Markdown from './markdown'
import Content from './context.vue' import Content from './context.vue'
import { FileTransfer } from '~/neko/types'
@Component({ @Component({
name: 'neko-files', name: 'neko-files',
@ -133,12 +182,67 @@
return this.$accessor.files.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() { refresh() {
this.$accessor.files.refresh() this.$accessor.files.refresh()
} }
download(item: any) { 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) { fileIcon(file: any) {
@ -169,6 +273,22 @@
return className; 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) { onFileDrop(e: any) {
console.log('file dropped', e) console.log('file dropped', e)
console.log(e.dataTransfer.files) console.log(e.dataTransfer.files)

View File

@ -187,14 +187,6 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this._ws!.send(JSON.stringify({ event, ...payload })) 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[]) { public async createPeer(lite: boolean, servers: RTCIceServer[]) {
this.emit('debug', `creating peer`) this.emit('debug', `creating peer`)
if (!this.socketOpen) { if (!this.socketOpen) {

View File

@ -71,6 +71,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
}) })
} }
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 // Internal Events
///////////////////////////// /////////////////////////////

View File

@ -25,5 +25,17 @@ export interface ScreenResolution {
export interface FileListItem { export interface FileListItem {
name: string, 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<void> | null,
abortController: AbortController | null
} }

View File

@ -1,10 +1,11 @@
import { actionTree, getterTree, mutationTree } from 'typed-vuex' import { actionTree, getterTree, mutationTree } from 'typed-vuex'
import { FileListItem } from '~/neko/types' import { FileListItem, FileTransfer } from '~/neko/types'
import { accessor } from '~/store' import { accessor } from '~/store'
export const state = () => ({ export const state = () => ({
cwd: '', cwd: '',
files: [] as FileListItem[] files: [] as FileListItem[],
transfers: [] as FileTransfer[]
}) })
export const getters = getterTree(state, { export const getters = getterTree(state, {
@ -18,6 +19,14 @@ export const mutations = mutationTree(state, {
_setFileList(state, files: FileListItem[]) { _setFileList(state, files: FileListItem[]) {
state.files = files 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) 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) { refresh(store) {
if (!accessor.connected) { if (!accessor.connected) {
return return

View File

@ -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.RequestID) // Create a request ID for each request
router.Use(middleware.RequestLogger(&logformatter{logger})) router.Use(middleware.RequestLogger(&logformatter{logger}))
router.Use(middleware.Recoverer) // Recover from panics without crashing server router.Use(middleware.Recoverer) // Recover from panics without crashing server
router.Use(middleware.Compress(5, "application/octet-stream"))
if conf.PathPrefix != "/" { if conf.PathPrefix != "/" {
router.Use(func(h http.Handler) http.Handler { router.Use(func(h http.Handler) http.Handler {

View File

@ -41,4 +41,5 @@ type WebSocketHandler interface {
type FileListItem struct { type FileListItem struct {
Filename string `json:"name"` Filename string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Size int64 `json:"size"`
} }

View File

@ -15,14 +15,20 @@ func ListFiles(path string) (*[]types.FileListItem, error) {
out := make([]types.FileListItem, len(items)) out := make([]types.FileListItem, len(items))
for i, item := range items { for i, item := range items {
var itemType string = "" var itemType string = ""
var size int64 = 0
if item.IsDir() { if item.IsDir() {
itemType = "dir" itemType = "dir"
} else { } else {
itemType = "file" itemType = "file"
info, err := item.Info()
if err == nil {
size = info.Size()
}
} }
out[i] = types.FileListItem{ out[i] = types.FileListItem{
Filename: item.Name(), Filename: item.Name(),
Type: itemType, Type: itemType,
Size: size,
} }
} }