added file downloads to frontend
This commit is contained in:
parent
7c6029aa99
commit
b9f31cc19c
@ -8,13 +8,28 @@
|
|||||||
<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">
|
||||||
<i class="fas fa-file-arrow-up" />
|
<div class="transfers" v-if="transfers.length > 0">
|
||||||
<p>Drag files here to upload</p>
|
<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" />
|
||||||
|
<p>Drag files here to upload</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -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',
|
||||||
@ -132,13 +181,68 @@
|
|||||||
get files() {
|
get files() {
|
||||||
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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user