mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
remove filetransfer.
This commit is contained in:
parent
e26e4d2004
commit
5c683fb1b8
@ -1,520 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="files">
|
|
||||||
<div class="files-cwd">
|
|
||||||
<p>{{ cwd }}</p>
|
|
||||||
<i class="fas fa-rotate-right refresh" @click="refresh" />
|
|
||||||
</div>
|
|
||||||
<div class="files-list">
|
|
||||||
<div v-for="item in files" :key="item.name" class="files-list-item">
|
|
||||||
<i :class="fileIcon(item)" />
|
|
||||||
<p class="file-name" :title="item.name">{{ item.name }}</p>
|
|
||||||
<p class="file-size">{{ fileSize(item.size) }}</p>
|
|
||||||
<i v-if="item.type !== 'dir'" class="fas fa-download download" @click="download(item)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="transfer-area">
|
|
||||||
<div class="transfers" v-if="transfers.length > 0">
|
|
||||||
<p v-if="downloads.length > 0" class="transfers-list-header">
|
|
||||||
<span>{{ $t('files.downloads') }}</span>
|
|
||||||
<i class="fas fa-xmark remove-transfer" @click="downloads.forEach((t) => removeTransfer(t))"></i>
|
|
||||||
</p>
|
|
||||||
<div v-for="download in downloads" :key="download.id" class="transfers-list-item">
|
|
||||||
<div class="transfer-info">
|
|
||||||
<i
|
|
||||||
class="fas transfer-status"
|
|
||||||
:class="{
|
|
||||||
'fa-clock': download.status === 'pending',
|
|
||||||
'fa-arrows-rotate': download.status === 'inprogress',
|
|
||||||
'fa-check': download.status === 'completed',
|
|
||||||
'fa-warning': download.status === 'failed',
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
<p class="file-name" :title="download.name">{{ download.name }}</p>
|
|
||||||
<p class="file-size">{{ Math.min(100, Math.round((download.progress / download.size) * 100)) }}%</p>
|
|
||||||
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(download)"></i>
|
|
||||||
</div>
|
|
||||||
<div v-if="download.status === 'failed'" class="transfer-error">{{ download.error }}</div>
|
|
||||||
<progress
|
|
||||||
v-else
|
|
||||||
class="transfer-progress"
|
|
||||||
:aria-label="download.name + ' progress'"
|
|
||||||
:value="download.progress"
|
|
||||||
:max="download.size"
|
|
||||||
></progress>
|
|
||||||
</div>
|
|
||||||
<p v-if="uploads.length > 0" class="transfers-list-header">
|
|
||||||
<span>{{ $t('files.uploads') }}</span>
|
|
||||||
<i class="fas fa-xmark remove-transfer" @click="uploads.forEach((t) => removeTransfer(t))"></i>
|
|
||||||
</p>
|
|
||||||
<div v-for="upload in uploads" :key="upload.id" class="transfers-list-item">
|
|
||||||
<div class="transfer-info">
|
|
||||||
<i
|
|
||||||
class="fas transfer-status"
|
|
||||||
:title="upload.status"
|
|
||||||
:class="{
|
|
||||||
'fa-clock': upload.status === 'pending',
|
|
||||||
'fa-arrows-rotate': upload.status === 'inprogress',
|
|
||||||
'fa-check': upload.status === 'completed',
|
|
||||||
'fa-warning': upload.status === 'failed',
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
<p class="file-name" :title="upload.name">{{ upload.name }}</p>
|
|
||||||
<p class="file-size">{{ Math.min(100, Math.round((upload.progress / upload.size) * 100)) }}%</p>
|
|
||||||
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(upload)"></i>
|
|
||||||
</div>
|
|
||||||
<div v-if="upload.status === 'failed'" class="transfer-error">{{ upload.error }}</div>
|
|
||||||
<progress
|
|
||||||
v-else
|
|
||||||
class="transfer-progress"
|
|
||||||
:aria-label="upload.name + ' progress'"
|
|
||||||
:value="upload.progress"
|
|
||||||
:max="upload.size"
|
|
||||||
></progress>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="upload-area"
|
|
||||||
:class="{ 'upload-area-drag': uploadAreaDrag }"
|
|
||||||
@dragover.prevent="uploadAreaDrag = true"
|
|
||||||
@dragleave.prevent="uploadAreaDrag = false"
|
|
||||||
@drop.prevent="(e) => upload(e.dataTransfer)"
|
|
||||||
@click="openFileBrowser"
|
|
||||||
>
|
|
||||||
<i class="fas fa-file-arrow-up" />
|
|
||||||
<p>{{ $t('files.upload_here') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.files {
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
display: flex;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
.files-cwd {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 10px 10px 0px 10px;
|
|
||||||
padding: 0.5em;
|
|
||||||
font-weight: 600;
|
|
||||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files-list {
|
|
||||||
margin: 10px 10px 10px 10px;
|
|
||||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: $background-tertiary transparent;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: $background-tertiary;
|
|
||||||
border: 2px solid $background-primary;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: $background-floating;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.files-list-item {
|
|
||||||
padding: 0.5em;
|
|
||||||
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transfers-list-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon,
|
|
||||||
.transfer-status {
|
|
||||||
width: 14px;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transfer-error {
|
|
||||||
border: 1px solid $style-error;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files-list-item:last-child {
|
|
||||||
border-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-size {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
color: rgba($color: #fff, $alpha: 0.4);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh:hover,
|
|
||||||
.download:hover,
|
|
||||||
.remove-transfer:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transfer-area {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transfers {
|
|
||||||
margin: 10px 10px 10px 10px;
|
|
||||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
|
||||||
border-radius: 5px;
|
|
||||||
max-height: 50vh;
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: $background-tertiary transparent;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: $background-tertiary;
|
|
||||||
border: 2px solid $background-primary;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: $background-floating;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-area-drag,
|
|
||||||
.upload-area:hover {
|
|
||||||
background-color: rgba($color: #fff, $alpha: 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-area > i {
|
|
||||||
font-size: 4em;
|
|
||||||
margin: 10px 10px 10px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-area > p {
|
|
||||||
margin: 0px 10px 10px 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from 'vue-property-decorator'
|
|
||||||
|
|
||||||
import Markdown from './markdown'
|
|
||||||
import Content from './context.vue'
|
|
||||||
import { FileTransfer, FileListItem } from '~/neko/types'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
name: 'neko-files',
|
|
||||||
components: {
|
|
||||||
'neko-markdown': Markdown,
|
|
||||||
'neko-context': Content,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class extends Vue {
|
|
||||||
public uploadAreaDrag: boolean = false
|
|
||||||
|
|
||||||
get cwd() {
|
|
||||||
return this.$accessor.files.cwd
|
|
||||||
}
|
|
||||||
|
|
||||||
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: FileListItem) {
|
|
||||||
if (this.downloads.map((t) => t.name).includes(item.name)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const url =
|
|
||||||
'/file?pwd=' + encodeURIComponent(this.$accessor.password) + '&filename=' + encodeURIComponent(item.name)
|
|
||||||
const abortController = new AbortController()
|
|
||||||
|
|
||||||
let transfer: FileTransfer = {
|
|
||||||
id: Math.round(Math.random() * 10000),
|
|
||||||
name: item.name,
|
|
||||||
direction: 'download',
|
|
||||||
// this may be smaller than the actual transfer amount, but for large files the
|
|
||||||
// content length is not sent (chunked transfer)
|
|
||||||
size: item.size,
|
|
||||||
progress: 0,
|
|
||||||
status: 'pending',
|
|
||||||
abortController: abortController,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$http
|
|
||||||
.get(url, {
|
|
||||||
responseType: 'blob',
|
|
||||||
signal: abortController.signal,
|
|
||||||
withCredentials: false,
|
|
||||||
onDownloadProgress: (x) => {
|
|
||||||
transfer.progress = x.loaded
|
|
||||||
|
|
||||||
if (x.total && transfer.size !== x.total) {
|
|
||||||
transfer.size = x.total
|
|
||||||
}
|
|
||||||
if (transfer.progress === transfer.size) {
|
|
||||||
transfer.status = 'completed'
|
|
||||||
} else if (transfer.status !== 'inprogress') {
|
|
||||||
transfer.status = 'inprogress'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.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)
|
|
||||||
|
|
||||||
transfer.progress = transfer.size
|
|
||||||
transfer.status = 'completed'
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.$log.error(error)
|
|
||||||
|
|
||||||
transfer.status = 'failed'
|
|
||||||
transfer.error = error.message
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$accessor.files.addTransfer(transfer)
|
|
||||||
}
|
|
||||||
|
|
||||||
upload(dt: DataTransfer) {
|
|
||||||
const url = '/file?pwd=' + encodeURIComponent(this.$accessor.password)
|
|
||||||
this.uploadAreaDrag = false
|
|
||||||
|
|
||||||
for (const file of dt.files) {
|
|
||||||
const abortController = new AbortController()
|
|
||||||
|
|
||||||
const formdata = new FormData()
|
|
||||||
formdata.append('files', file, file.name)
|
|
||||||
|
|
||||||
let transfer: FileTransfer = {
|
|
||||||
id: Math.round(Math.random() * 10000),
|
|
||||||
name: file.name,
|
|
||||||
direction: 'upload',
|
|
||||||
size: file.size,
|
|
||||||
progress: 0,
|
|
||||||
status: 'pending',
|
|
||||||
abortController: abortController,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$http
|
|
||||||
.post(url, formdata, {
|
|
||||||
signal: abortController.signal,
|
|
||||||
withCredentials: false,
|
|
||||||
onUploadProgress: (x: any) => {
|
|
||||||
transfer.progress = x.loaded
|
|
||||||
|
|
||||||
if (transfer.size !== x.total) {
|
|
||||||
transfer.size = x.total
|
|
||||||
}
|
|
||||||
if (transfer.progress === transfer.size) {
|
|
||||||
transfer.status = 'completed'
|
|
||||||
} else if (transfer.status !== 'inprogress') {
|
|
||||||
transfer.status = 'inprogress'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.$log.error(error)
|
|
||||||
|
|
||||||
transfer.status = 'failed'
|
|
||||||
transfer.error = error.message
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$accessor.files.addTransfer(transfer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openFileBrowser() {
|
|
||||||
const input = document.createElement('input')
|
|
||||||
input.type = 'file'
|
|
||||||
input.setAttribute('multiple', 'true')
|
|
||||||
input.onchange = (e: Event) => {
|
|
||||||
if (e === null) return
|
|
||||||
|
|
||||||
const dt = new DataTransfer()
|
|
||||||
const target = e.target as HTMLInputElement
|
|
||||||
if (target.files === null) return
|
|
||||||
|
|
||||||
for (const f of target.files) {
|
|
||||||
dt.items.add(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.upload(dt)
|
|
||||||
}
|
|
||||||
input.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTransfer(transfer: FileTransfer) {
|
|
||||||
if (transfer.status !== 'completed') {
|
|
||||||
transfer.abortController?.abort()
|
|
||||||
}
|
|
||||||
this.$accessor.files.removeTransfer(transfer)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileIcon(file: FileListItem) {
|
|
||||||
let className = 'file-icon fas '
|
|
||||||
// if is directory
|
|
||||||
if (file.type === 'dir') {
|
|
||||||
className += 'fa-folder'
|
|
||||||
return className
|
|
||||||
}
|
|
||||||
// try to get file extension
|
|
||||||
const ext = file.name.split('.').pop()
|
|
||||||
if (ext === undefined) {
|
|
||||||
className += 'fa-file'
|
|
||||||
return className
|
|
||||||
}
|
|
||||||
// try to find icon
|
|
||||||
switch (ext.toLowerCase()) {
|
|
||||||
case 'txt':
|
|
||||||
case 'md':
|
|
||||||
className += 'fa-file-text'
|
|
||||||
break
|
|
||||||
case 'pdf':
|
|
||||||
className += 'fa-file-pdf'
|
|
||||||
break
|
|
||||||
case 'zip':
|
|
||||||
case 'rar':
|
|
||||||
case '7z':
|
|
||||||
case 'gz':
|
|
||||||
className += 'fa-archive'
|
|
||||||
break
|
|
||||||
case 'aac':
|
|
||||||
case 'flac':
|
|
||||||
case 'midi':
|
|
||||||
case 'mp3':
|
|
||||||
case 'ogg':
|
|
||||||
case 'wav':
|
|
||||||
className += 'fa-music'
|
|
||||||
break
|
|
||||||
case 'avi':
|
|
||||||
case 'mkv':
|
|
||||||
case 'mov':
|
|
||||||
case 'mpeg':
|
|
||||||
case 'mp4':
|
|
||||||
case 'webm':
|
|
||||||
className += 'fa-film'
|
|
||||||
break
|
|
||||||
case 'bmp':
|
|
||||||
case 'gif':
|
|
||||||
case 'jpeg':
|
|
||||||
case 'jpg':
|
|
||||||
case 'png':
|
|
||||||
case 'svg':
|
|
||||||
case 'tiff':
|
|
||||||
case 'webp':
|
|
||||||
className += 'fa-image'
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
className += 'fa-file'
|
|
||||||
}
|
|
||||||
return className
|
|
||||||
}
|
|
||||||
|
|
||||||
fileSize(size: number) {
|
|
||||||
if (size < 1024) {
|
|
||||||
return size + ' B'
|
|
||||||
}
|
|
||||||
if (size < 1024 * 1024) {
|
|
||||||
return Math.round(size / 1024) + ' KB'
|
|
||||||
}
|
|
||||||
if (size < 1024 * 1024 * 1024) {
|
|
||||||
return Math.round(size / (1024 * 1024)) + ' MB'
|
|
||||||
}
|
|
||||||
if (size < 1024 * 1024 * 1024 * 1024) {
|
|
||||||
return Math.round(size / (1024 * 1024 * 1024)) + ' GB'
|
|
||||||
}
|
|
||||||
return Math.round(size / (1024 * 1024 * 1024 * 1024)) + ' TB'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -6,10 +6,6 @@
|
|||||||
<i class="fas fa-comment-alt" />
|
<i class="fas fa-comment-alt" />
|
||||||
<span>{{ $t('side.chat') }}</span>
|
<span>{{ $t('side.chat') }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="filetransferAllowed" :class="{ active: tab === 'files' }" @click.stop.prevent="change('files')">
|
|
||||||
<i class="fas fa-file" />
|
|
||||||
<span>{{ $t('side.files') }}</span>
|
|
||||||
</li>
|
|
||||||
<li :class="{ active: tab === 'settings' }" @click.stop.prevent="change('settings')">
|
<li :class="{ active: tab === 'settings' }" @click.stop.prevent="change('settings')">
|
||||||
<i class="fas fa-sliders-h" />
|
<i class="fas fa-sliders-h" />
|
||||||
<span>{{ $t('side.settings') }}</span>
|
<span>{{ $t('side.settings') }}</span>
|
||||||
@ -79,47 +75,23 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Watch } from 'vue-property-decorator'
|
import { Vue, Component } from 'vue-property-decorator'
|
||||||
|
|
||||||
import Settings from '~/components/settings.vue'
|
import Settings from '~/components/settings.vue'
|
||||||
import Chat from '~/components/chat.vue'
|
import Chat from '~/components/chat.vue'
|
||||||
import Files from '~/components/files.vue'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
name: 'neko',
|
name: 'neko',
|
||||||
components: {
|
components: {
|
||||||
'neko-settings': Settings,
|
'neko-settings': Settings,
|
||||||
'neko-chat': Chat,
|
'neko-chat': Chat,
|
||||||
'neko-files': Files,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class extends Vue {
|
export default class extends Vue {
|
||||||
get filetransferAllowed() {
|
|
||||||
return (
|
|
||||||
this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer'))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get tab() {
|
get tab() {
|
||||||
return this.$accessor.client.tab
|
return this.$accessor.client.tab
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('tab', { immediate: true })
|
|
||||||
@Watch('filetransferAllowed', { immediate: true })
|
|
||||||
onTabChange() {
|
|
||||||
// do not show the files tab if file transfer is disabled
|
|
||||||
if (this.tab === 'files' && !this.filetransferAllowed) {
|
|
||||||
this.change('chat')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('filetransferAllowed')
|
|
||||||
onFileTransferAllowedChange() {
|
|
||||||
if (this.filetransferAllowed) {
|
|
||||||
this.$accessor.files.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
change(tab: string) {
|
change(tab: string) {
|
||||||
this.$accessor.client.setTab(tab)
|
this.$accessor.client.setTab(tab)
|
||||||
}
|
}
|
||||||
|
@ -38,10 +38,6 @@ export const EVENT = {
|
|||||||
MESSAGE: 'chat/message',
|
MESSAGE: 'chat/message',
|
||||||
EMOTE: 'chat/emote',
|
EMOTE: 'chat/emote',
|
||||||
},
|
},
|
||||||
FILETRANSFER: {
|
|
||||||
LIST: 'filetransfer/list',
|
|
||||||
REFRESH: 'filetransfer/refresh',
|
|
||||||
},
|
|
||||||
SCREEN: {
|
SCREEN: {
|
||||||
CONFIGURATIONS: 'screen/configurations',
|
CONFIGURATIONS: 'screen/configurations',
|
||||||
RESOLUTION: 'screen/resolution',
|
RESOLUTION: 'screen/resolution',
|
||||||
@ -73,7 +69,6 @@ export type WebSocketEvents =
|
|||||||
| MemberEvents
|
| MemberEvents
|
||||||
| SignalEvents
|
| SignalEvents
|
||||||
| ChatEvents
|
| ChatEvents
|
||||||
| FileTransferEvents
|
|
||||||
| ScreenEvents
|
| ScreenEvents
|
||||||
| BroadcastEvents
|
| BroadcastEvents
|
||||||
| AdminEvents
|
| AdminEvents
|
||||||
@ -97,8 +92,6 @@ export type SignalEvents =
|
|||||||
|
|
||||||
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
|
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
|
||||||
|
|
||||||
export type FileTransferEvents = typeof EVENT.FILETRANSFER.LIST | typeof EVENT.FILETRANSFER.REFRESH
|
|
||||||
|
|
||||||
export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET
|
export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET
|
||||||
|
|
||||||
export type BroadcastEvents =
|
export type BroadcastEvents =
|
||||||
|
@ -22,7 +22,6 @@ import {
|
|||||||
AdminLockMessage,
|
AdminLockMessage,
|
||||||
SystemInitPayload,
|
SystemInitPayload,
|
||||||
AdminLockResource,
|
AdminLockResource,
|
||||||
FileTransferListPayload,
|
|
||||||
} from './messages'
|
} from './messages'
|
||||||
|
|
||||||
interface NekoEvents extends BaseEvents {}
|
interface NekoEvents extends BaseEvents {}
|
||||||
@ -353,14 +352,6 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
|||||||
this.$accessor.chat.newEmote({ type: emote })
|
this.$accessor.chat.newEmote({ type: emote })
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
|
||||||
// File Transfer Events
|
|
||||||
/////////////////////////////
|
|
||||||
protected [EVENT.FILETRANSFER.LIST]({ cwd, files }: FileTransferListPayload) {
|
|
||||||
this.$accessor.files.setCwd(cwd)
|
|
||||||
this.$accessor.files.setFileList(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
// Screen Events
|
// Screen Events
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
|
@ -8,9 +8,8 @@ import {
|
|||||||
ChatEvents,
|
ChatEvents,
|
||||||
ScreenEvents,
|
ScreenEvents,
|
||||||
AdminEvents,
|
AdminEvents,
|
||||||
FileTransferEvents,
|
|
||||||
} from './events'
|
} from './events'
|
||||||
import { FileListItem, Member, ScreenConfigurations, ScreenResolution } from './types'
|
import { Member, ScreenConfigurations, ScreenResolution } from './types'
|
||||||
|
|
||||||
export type WebSocketMessages =
|
export type WebSocketMessages =
|
||||||
| WebSocketMessage
|
| WebSocketMessage
|
||||||
@ -194,18 +193,6 @@ export interface EmojiSendPayload {
|
|||||||
emote: string
|
emote: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
FILE TRANSFER PAYLOADS
|
|
||||||
*/
|
|
||||||
export interface FileTransferListMessage extends WebSocketMessage, FileTransferListPayload {
|
|
||||||
event: FileTransferEvents
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileTransferListPayload {
|
|
||||||
cwd: string
|
|
||||||
files: FileListItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
SCREEN PAYLOADS
|
SCREEN PAYLOADS
|
||||||
*/
|
*/
|
||||||
|
@ -22,20 +22,3 @@ export interface ScreenResolution {
|
|||||||
height: number
|
height: number
|
||||||
rate: number
|
rate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileListItem {
|
|
||||||
name: string
|
|
||||||
type: 'file' | 'dir'
|
|
||||||
size: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileTransfer {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
direction: 'upload' | 'download'
|
|
||||||
size: number
|
|
||||||
progress: number
|
|
||||||
status: 'pending' | 'inprogress' | 'completed' | 'failed'
|
|
||||||
error?: string
|
|
||||||
abortController?: AbortController
|
|
||||||
}
|
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
import { actionTree, getterTree, mutationTree } from 'typed-vuex'
|
|
||||||
import { FileListItem, FileTransfer } from '~/neko/types'
|
|
||||||
import { EVENT } from '~/neko/events'
|
|
||||||
import { accessor } from '~/store'
|
|
||||||
|
|
||||||
export const state = () => ({
|
|
||||||
cwd: '',
|
|
||||||
files: [] as FileListItem[],
|
|
||||||
transfers: [] as FileTransfer[],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const getters = getterTree(state, {
|
|
||||||
//
|
|
||||||
})
|
|
||||||
|
|
||||||
export const mutations = mutationTree(state, {
|
|
||||||
_setCwd(state, cwd: string) {
|
|
||||||
state.cwd = cwd
|
|
||||||
},
|
|
||||||
|
|
||||||
_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)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const actions = actionTree(
|
|
||||||
{ state, getters, mutations },
|
|
||||||
{
|
|
||||||
setCwd(store, cwd: string) {
|
|
||||||
accessor.files._setCwd(cwd)
|
|
||||||
},
|
|
||||||
|
|
||||||
setFileList(store, files: FileListItem[]) {
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelAllTransfers() {
|
|
||||||
for (const t of accessor.files.transfers) {
|
|
||||||
if (t.status !== 'completed') {
|
|
||||||
t.abortController?.abort()
|
|
||||||
}
|
|
||||||
accessor.files.removeTransfer(t)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
if (!accessor.connected) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$client.sendMessage(EVENT.FILETRANSFER.REFRESH)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
@ -7,7 +7,6 @@ import { get, set } from '~/utils/localstorage'
|
|||||||
|
|
||||||
import * as video from './video'
|
import * as video from './video'
|
||||||
import * as chat from './chat'
|
import * as chat from './chat'
|
||||||
import * as files from './files'
|
|
||||||
import * as remote from './remote'
|
import * as remote from './remote'
|
||||||
import * as user from './user'
|
import * as user from './user'
|
||||||
import * as settings from './settings'
|
import * as settings from './settings'
|
||||||
@ -111,7 +110,7 @@ export const storePattern = {
|
|||||||
mutations,
|
mutations,
|
||||||
actions,
|
actions,
|
||||||
getters,
|
getters,
|
||||||
modules: { video, chat, files, user, remote, settings, client, emoji },
|
modules: { video, chat, user, remote, settings, client, emoji },
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@ -13,9 +11,6 @@ type WebSocket struct {
|
|||||||
Locks []string
|
Locks []string
|
||||||
|
|
||||||
ControlProtection bool
|
ControlProtection bool
|
||||||
|
|
||||||
FileTransferEnabled bool
|
|
||||||
FileTransferPath string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (WebSocket) Init(cmd *cobra.Command) error {
|
func (WebSocket) Init(cmd *cobra.Command) error {
|
||||||
@ -39,18 +34,6 @@ func (WebSocket) Init(cmd *cobra.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// File transfer
|
|
||||||
|
|
||||||
cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature")
|
|
||||||
if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.PersistentFlags().String("file_transfer_path", "/home/neko/Downloads", "path to use for file transfer")
|
|
||||||
if err := viper.BindPFlag("file_transfer_path", cmd.PersistentFlags().Lookup("file_transfer_path")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,8 +43,4 @@ func (s *WebSocket) Set() {
|
|||||||
s.Locks = viper.GetStringSlice("locks")
|
s.Locks = viper.GetStringSlice("locks")
|
||||||
|
|
||||||
s.ControlProtection = viper.GetBool("control_protection")
|
s.ControlProtection = viper.GetBool("control_protection")
|
||||||
|
|
||||||
s.FileTransferEnabled = viper.GetBool("file_transfer_enabled")
|
|
||||||
s.FileTransferPath = viper.GetString("file_transfer_path")
|
|
||||||
s.FileTransferPath = filepath.Clean(s.FileTransferPath)
|
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,9 @@ package http
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@ -21,8 +18,6 @@ import (
|
|||||||
"m1k1o/neko/internal/types"
|
"m1k1o/neko/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const FILE_UPLOAD_BUF_SIZE = 65000
|
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
router *chi.Mux
|
router *chi.Mux
|
||||||
@ -118,89 +113,6 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// allow downloading and uploading files
|
|
||||||
if webSocketHandler.FileTransferEnabled() {
|
|
||||||
router.Get("/file", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
password := r.URL.Query().Get("pwd")
|
|
||||||
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isAuthorized {
|
|
||||||
http.Error(w, "bad authorization", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := r.URL.Query().Get("filename")
|
|
||||||
badChars, _ := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename)
|
|
||||||
if filename == "" || badChars {
|
|
||||||
http.Error(w, "bad filename", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := webSocketHandler.FileTransferPath(filename)
|
|
||||||
f, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "not found or unable to open", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
|
||||||
io.Copy(w, f)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Post("/file", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
password := r.URL.Query().Get("pwd")
|
|
||||||
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isAuthorized {
|
|
||||||
http.Error(w, "bad authorization", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.ParseMultipartForm(32 << 20)
|
|
||||||
if err != nil || r.MultipartForm == nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to parse multipart form")
|
|
||||||
http.Error(w, "error parsing form", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, formheader := range r.MultipartForm.File["files"] {
|
|
||||||
filePath := webSocketHandler.FileTransferPath(formheader.Filename)
|
|
||||||
|
|
||||||
formfile, err := formheader.Open()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to open formdata file")
|
|
||||||
http.Error(w, "error writing file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer formfile.Close()
|
|
||||||
|
|
||||||
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "unable to open file for writing", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
io.Copy(f, formfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.MultipartForm.RemoveAll()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to remove multipart form")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
router.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte("true"))
|
_, _ = w.Write([]byte("true"))
|
||||||
})
|
})
|
||||||
|
@ -34,11 +34,6 @@ const (
|
|||||||
CHAT_EMOTE = "chat/emote"
|
CHAT_EMOTE = "chat/emote"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
FILETRANSFER_LIST = "filetransfer/list"
|
|
||||||
FILETRANSFER_REFRESH = "filetransfer/refresh"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SCREEN_CONFIGURATIONS = "screen/configurations"
|
SCREEN_CONFIGURATIONS = "screen/configurations"
|
||||||
SCREEN_RESOLUTION = "screen/resolution"
|
SCREEN_RESOLUTION = "screen/resolution"
|
||||||
|
@ -14,7 +14,6 @@ type SystemInit struct {
|
|||||||
Event string `json:"event"`
|
Event string `json:"event"`
|
||||||
ImplicitHosting bool `json:"implicit_hosting"`
|
ImplicitHosting bool `json:"implicit_hosting"`
|
||||||
Locks map[string]string `json:"locks"`
|
Locks map[string]string `json:"locks"`
|
||||||
FileTransfer bool `json:"file_transfer"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemMessage struct {
|
type SystemMessage struct {
|
||||||
@ -107,12 +106,6 @@ type EmoteSend struct {
|
|||||||
Emote string `json:"emote"`
|
Emote string `json:"emote"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileTransferList struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Cwd string `json:"cwd"`
|
|
||||||
Files []types.FileListItem `json:"files"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Admin struct {
|
type Admin struct {
|
||||||
Event string `json:"event"`
|
Event string `json:"event"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
@ -34,15 +34,4 @@ type WebSocketHandler interface {
|
|||||||
Stats() Stats
|
Stats() Stats
|
||||||
IsLocked(resource string) bool
|
IsLocked(resource string) bool
|
||||||
IsAdmin(password string) (bool, error)
|
IsAdmin(password string) (bool, error)
|
||||||
|
|
||||||
// File Transfer
|
|
||||||
CanTransferFiles(password string) (bool, error)
|
|
||||||
FileTransferPath(filename string) string
|
|
||||||
FileTransferEnabled() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileListItem struct {
|
|
||||||
Filename string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
}
|
}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"m1k1o/neko/internal/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ListFiles(path string) ([]types.FileListItem, error) {
|
|
||||||
items, err := os.ReadDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"m1k1o/neko/internal/types"
|
|
||||||
"m1k1o/neko/internal/types/event"
|
|
||||||
"m1k1o/neko/internal/types/message"
|
|
||||||
"m1k1o/neko/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *MessageHandler) FileTransferRefresh(session types.Session) error {
|
|
||||||
if !h.state.FileTransferEnabled() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fileTransferPath := h.state.FileTransferPath("") // root
|
|
||||||
|
|
||||||
// allow users only if file transfer is not locked
|
|
||||||
if session != nil && !(session.Admin() || !h.state.IsLocked("file_transfer")) {
|
|
||||||
h.logger.Debug().Msg("file transfer is locked for users")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: keep list of files in memory and update it on file changes
|
|
||||||
files, err := utils.ListFiles(fileTransferPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
message := message.FileTransferList{
|
|
||||||
Event: event.FILETRANSFER_LIST,
|
|
||||||
Cwd: fileTransferPath,
|
|
||||||
Files: files,
|
|
||||||
}
|
|
||||||
|
|
||||||
// send to just one user
|
|
||||||
if session != nil {
|
|
||||||
return session.Send(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// broadcast to all admins
|
|
||||||
if h.state.IsLocked("file_transfer") {
|
|
||||||
return h.sessions.AdminBroadcast(message, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// broadcast to all users
|
|
||||||
return h.sessions.Broadcast(message, nil)
|
|
||||||
}
|
|
@ -132,10 +132,6 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
|
|||||||
return h.chatEmote(id, session, payload)
|
return h.chatEmote(id, session, payload)
|
||||||
}), "%s failed", header.Event)
|
}), "%s failed", header.Event)
|
||||||
|
|
||||||
// File Transfer Events
|
|
||||||
case event.FILETRANSFER_REFRESH:
|
|
||||||
return errors.Wrapf(h.FileTransferRefresh(session), "%s failed", header.Event)
|
|
||||||
|
|
||||||
// Screen Events
|
// Screen Events
|
||||||
case event.SCREEN_RESOLUTION:
|
case event.SCREEN_RESOLUTION:
|
||||||
return errors.Wrapf(h.screenResolution(id, session), "%s failed", header.Event)
|
return errors.Wrapf(h.screenResolution(id, session), "%s failed", header.Event)
|
||||||
|
@ -17,7 +17,6 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
|
|||||||
Event: event.SYSTEM_INIT,
|
Event: event.SYSTEM_INIT,
|
||||||
ImplicitHosting: h.webrtc.ImplicitControl(),
|
ImplicitHosting: h.webrtc.ImplicitControl(),
|
||||||
Locks: h.state.AllLocked(),
|
Locks: h.state.AllLocked(),
|
||||||
FileTransfer: h.state.FileTransferEnabled(),
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT)
|
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT)
|
||||||
return err
|
return err
|
||||||
@ -35,13 +34,6 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// send file list if file transfer is enabled
|
|
||||||
if h.state.FileTransferEnabled() && (session.Admin() || !h.state.IsLocked("file_transfer")) {
|
|
||||||
if err := h.FileTransferRefresh(session); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
package state
|
package state
|
||||||
|
|
||||||
import "path/filepath"
|
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
banned map[string]string // IP -> session ID (that banned it)
|
banned map[string]string // IP -> session ID (that banned it)
|
||||||
locked map[string]string // resource name -> session ID (that locked it)
|
locked map[string]string // resource name -> session ID (that locked it)
|
||||||
|
|
||||||
fileTransferEnabled bool
|
|
||||||
fileTransferPath string // path where files are located
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(fileTransferEnabled bool, fileTransferPath string) *State {
|
func New() *State {
|
||||||
return &State{
|
return &State{
|
||||||
banned: make(map[string]string),
|
banned: make(map[string]string),
|
||||||
locked: make(map[string]string),
|
locked: make(map[string]string),
|
||||||
|
|
||||||
fileTransferEnabled: fileTransferEnabled,
|
|
||||||
fileTransferPath: fileTransferPath,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,18 +59,3 @@ func (s *State) GetLocked(resource string) (string, bool) {
|
|||||||
func (s *State) AllLocked() map[string]string {
|
func (s *State) AllLocked() map[string]string {
|
||||||
return s.locked
|
return s.locked
|
||||||
}
|
}
|
||||||
|
|
||||||
// File transfer
|
|
||||||
|
|
||||||
func (s *State) FileTransferPath(filename string) string {
|
|
||||||
if filename == "" {
|
|
||||||
return s.fileTransferPath
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanPath := filepath.Clean(filename)
|
|
||||||
return filepath.Join(s.fileTransferPath, cleanPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) FileTransferEnabled() bool {
|
|
||||||
return s.fileTransferEnabled
|
|
||||||
}
|
|
||||||
|
@ -3,12 +3,10 @@ package websocket
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@ -27,7 +25,7 @@ const CONTROL_PROTECTION_SESSION = "by_control_protection"
|
|||||||
func New(sessions types.SessionManager, desktop types.DesktopManager, capture types.CaptureManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler {
|
func New(sessions types.SessionManager, desktop types.DesktopManager, capture types.CaptureManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler {
|
||||||
logger := log.With().Str("module", "websocket").Logger()
|
logger := log.With().Str("module", "websocket").Logger()
|
||||||
|
|
||||||
state := state.New(conf.FileTransferEnabled, conf.FileTransferPath)
|
state := state.New()
|
||||||
|
|
||||||
// if control protection is enabled
|
// if control protection is enabled
|
||||||
if conf.ControlProtection {
|
if conf.ControlProtection {
|
||||||
@ -35,14 +33,6 @@ func New(sessions types.SessionManager, desktop types.DesktopManager, capture ty
|
|||||||
logger.Info().Msgf("control locked on behalf of control protection")
|
logger.Info().Msgf("control locked on behalf of control protection")
|
||||||
}
|
}
|
||||||
|
|
||||||
// create file transfer directory if not exists
|
|
||||||
if conf.FileTransferEnabled {
|
|
||||||
if _, err := os.Stat(conf.FileTransferPath); os.IsNotExist(err) {
|
|
||||||
err = os.Mkdir(conf.FileTransferPath, os.ModePerm)
|
|
||||||
logger.Err(err).Msg("creating file transfer directory")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply default locks
|
// apply default locks
|
||||||
for _, lock := range conf.Locks {
|
for _, lock := range conf.Locks {
|
||||||
state.Lock(lock, "") // empty session ID
|
state.Lock(lock, "") // empty session ID
|
||||||
@ -216,37 +206,6 @@ func (ws *WebSocketHandler) Start() {
|
|||||||
ws.logger.Err(err).Msg("sync clipboard")
|
ws.logger.Err(err).Msg("sync clipboard")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// watch for file changes and send file list if file transfer is enabled
|
|
||||||
if ws.conf.FileTransferEnabled {
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
ws.logger.Err(err).Msg("unable to start file transfer dir watcher")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case e, ok := <-watcher.Events:
|
|
||||||
if !ok {
|
|
||||||
ws.logger.Info().Msg("file transfer dir watcher closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if e.Has(fsnotify.Create) || e.Has(fsnotify.Remove) || e.Has(fsnotify.Rename) {
|
|
||||||
ws.logger.Debug().Str("event", e.String()).Msg("file transfer dir watcher event")
|
|
||||||
ws.handler.FileTransferRefresh(nil)
|
|
||||||
}
|
|
||||||
case err := <-watcher.Errors:
|
|
||||||
ws.logger.Err(err).Msg("error in file transfer dir watcher")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := watcher.Add(ws.conf.FileTransferPath); err != nil {
|
|
||||||
ws.logger.Err(err).Msg("unable to add file transfer path to watcher")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebSocketHandler) Shutdown() error {
|
func (ws *WebSocketHandler) Shutdown() error {
|
||||||
@ -444,28 +403,3 @@ func (ws *WebSocketHandler) handle(connection *websocket.Conn, id string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// File transfer
|
|
||||||
//
|
|
||||||
|
|
||||||
func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) {
|
|
||||||
if !ws.conf.FileTransferEnabled {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
isAdmin, err := ws.IsAdmin(password)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAdmin || !ws.state.IsLocked("file_transfer"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *WebSocketHandler) FileTransferPath(filename string) string {
|
|
||||||
return ws.state.FileTransferPath(filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *WebSocketHandler) FileTransferEnabled() bool {
|
|
||||||
return ws.conf.FileTransferEnabled
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user