diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4aabe3f..28108eb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,7 +6,7 @@ on:
- '!v*.*.*'
jobs:
- build-macOS-clients:
+ build-clients-macOS:
runs-on: macos-latest
strategy:
@@ -51,7 +51,7 @@ jobs:
build-others:
- needs: [ build-macOS-clients ]
+ needs: [ build-clients-macOS ]
runs-on: ubuntu-latest
strategy:
@@ -77,29 +77,18 @@ jobs:
export PATH=$PATH:~/go/bin/
go install github.com/rakyll/statik
- - name: Get artifacts from previous job (arm64)
+ - name: Get artifact from previous job (darwin_arm64)
uses: actions/download-artifact@v3
with:
name: darwin_arm64
path: ./built
- - name: Get artifacts from previous job (amd64)
+ - name: Get artifact from previous job (darwin_amd64)
uses: actions/download-artifact@v3
with:
name: darwin_amd64
path: ./built
- - name: Build and embed clients
- run: |
- chmod +x ./build.client.sh
- export GOMOD=`pwd`/go.mod
- export CGO_ENABLED=0
- go mod tidy
- go mod download
-
- ./build.client.sh
- statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
-
- name: Build and pack static resources
run: |
cd ./web
@@ -107,17 +96,23 @@ jobs:
npm run build-prod
statik -m -src="./dist" -f -dest="../server/embed" -p web -ns web
cd ..
- zip -q -r ./embed.zip ./server/embed
- - name: Build server
+ - name: Set up go dependencies
run: |
- chmod +x ./build.server.sh
export GOMOD=`pwd`/go.mod
export CGO_ENABLED=0
go mod tidy
go mod download
+
+ - name: Build clients and servers
+ run: |
+ chmod +x ./scripts/build.client.sh
+ ./scripts/build.client.sh
+ statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
+
+ chmod +x ./scripts/build.server.sh
mkdir ./releases
- ./build.server.sh
+ ./scripts/build.server.sh
- name: Prepare release note
run: |
@@ -128,6 +123,8 @@ jobs:
run: |
cd ./releases
sudo apt install zip tar -y
+ tar -zcvf server_darwin_arm64.tar.gz server_darwin_arm64
+ tar -zcvf server_darwin_amd64.tar.gz server_darwin_amd64
tar -zcvf server_linux_arm.tar.gz server_linux_arm
tar -zcvf server_linux_arm64.tar.gz server_linux_arm64
tar -zcvf server_linux_i386.tar.gz server_linux_i386
@@ -137,17 +134,13 @@ jobs:
zip -r server_windows_i386.zip server_windows_i386.exe
zip -r server_windows_amd64.zip server_windows_amd64.exe
- - name: Upload embedding resources (embed.zip)
- uses: actions/upload-artifact@v2
- with:
- name: embed.zip
- path: embed.zip
-
- name: Release
uses: softprops/action-gh-release@v1
with:
body_path: CHANGELOG.md
files: |
+ releases/server_darwin_arm64.tar.gz
+ releases/server_darwin_amd64.tar.gz
releases/server_linux_arm.tar.gz
releases/server_linux_arm64.tar.gz
releases/server_linux_i386.tar.gz
@@ -157,64 +150,9 @@ jobs:
releases/server_windows_i386.zip
releases/server_windows_amd64.zip
-
-
- build-macOS-servers:
- needs: [ build-others ]
- runs-on: macos-latest
-
- strategy:
- matrix:
- go-version: [ 1.17 ]
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Set up golang
- uses: actions/setup-go@v3
- with:
- go-version: ${{ matrix.go-version }}
-
- - name: Get artifacts from previous job (embed.zip)
- uses: actions/download-artifact@v3
- with:
- name: embed.zip
- path: .
-
- - name: Build and compress servers
- run: |
- rm -rf ./server/embed
- unzip -q embed.zip
-
- export COMMIT=`git rev-parse HEAD`
- export GOMOD=`pwd`/go.mod
- export CGO_ENABLED=0
- go mod tidy
- go mod download
- mkdir ./releases
-
- export GOOS=darwin
- export GOARCH=arm64
- go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
- export GOARCH=amd64
- go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
-
- cd ./releases
- tar -zcvf server_darwin_arm64.tar.gz server_darwin_arm64
- tar -zcvf server_darwin_amd64.tar.gz server_darwin_amd64
- cd ..
-
- - name: Release
- uses: softprops/action-gh-release@v1
- with:
- files: |
- releases/server_darwin_arm64.tar.gz
- releases/server_darwin_amd64.tar.gz
-
- - name: Clean up
- uses: geekyeggo/delete-artifact@v1
- with:
- name: |
- embed.zip
- darwin_arm64
- darwin_amd64
+ - name: Clean up
+ uses: geekyeggo/delete-artifact@v1
+ with:
+ name: |
+ darwin_arm64
+ darwin_amd64
diff --git a/API.ZH.md b/API.ZH.md
index 51e2230..18daeba 100644
--- a/API.ZH.md
+++ b/API.ZH.md
@@ -5,21 +5,25 @@
## 通用
所有请求均为`POST`。
-
+
每次请求都必须在Header中带上`Authorization`。
-
+
`Authorization`请求头格式:`Basic `(basic auth)。
```
Authorization: Basic
```
+例如:
+```
+Authorization: Basic WFpCOjEyNDg=
+```
---
## 响应
所有响应均是JSON格式。
-
+
`code` 有三种结果,分别为`-1`,`0`和`1`,含义如下。
| code | meaning |
@@ -55,9 +59,13 @@ Authorization: Basic
参数:**无**
-设备的`id`是一串64位的字符串,每台设备独一无二,一般不会变化。识别设备主要靠这个。下文中提到的设备ID也指的是这个。
-
-每个device对象所对应的key,是它的本次连接的连接ID,这个ID是随机、临时的,每次重连就会变化,不建议使用。
+设备的`id`是一串64位的字符串,每台设备独一无二,一般不会变化。
+
+识别设备主要靠这个。下文中提到的设备ID也指的是这个。
+
+每个device对象所对应的key,是它的本次连接的连接ID。
+
+连接ID是随机、临时的,每次重连就会变化,不建议使用。
```
{
@@ -123,7 +131,9 @@ Authorization: Basic
参数:`device`(设备ID)
-如果截屏获取成功,则会直接以图片的形式输出。如果截屏失败,如下响应会被输出(错误信息不止这一个)。
+如果截屏获取成功,则会直接以图片的形式输出。
+
+如果截屏失败,如下响应会被输出(错误信息不一定是这一个)。
```
{
@@ -138,7 +148,9 @@ Authorization: Basic
参数:`file`(文件路径) 以及 `device`(设备ID)
-如果文件存在且可访问,则文件会直接输出。否则,会给出以下响应。
+如果文件存在且可访问,则文件会直接输出。
+
+否则,会给出错误原因。
```
{
@@ -169,11 +181,54 @@ Authorization: Basic
---
+### 上传文件到目录:`/device/file/upload`
+
+**GET**参数:`file`(文件名)、`path`(路径)和`device`(设备ID)
+
+文件内容需要作为**请求体body**发送。
+
+**请求体body**中的任何内容都会被写到指定地文件中。
+
+如果存在同名文件,则会被**覆盖**!
+
+Example:
+```http request
+POST http://localhost:8000/api/device/file/upload?path=D%3A%5C&file=Test.txt&device=bc7e49f8f794f80ffb0032a4ba516c86d76041bf2023e1be6c5dda3b1ee0cf4c HTTP/1.1
+Host: localhost:8000
+Content-Length: 12
+Content-Type: application/octet-stream
+User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.47
+Origin: http://localhost:8000
+Referer: http://localhost:8000/
+
+Hello World.
+```
+
+如果文件上传成功,则`code`为`1`。
+
+文件`D:\Test.txt`会写入:`Hello World.`。
+
+```
+{
+ "code": 0
+}
+```
+```
+{
+ "code": 1,
+ "msg": "${i18n|fileOrDirNotExist}"
+}
+```
+
+---
+
### 列举设备上的文件和目录:`/device/file/list`
参数:`path`(父目录路径) 以及 `device`(设备ID)
-如果`path`为空,windows下会给出磁盘列表,其它系统会默认输出`/`目录下的文件和目录。
+如果`path`为空,windows下会给出磁盘列表。
+
+其它系统会默认输出`/`目录下的文件和目录。
`type`有三种结果:`0`代表文件,`1`代表目录,`2`代表磁盘(windows)。
diff --git a/API.md b/API.md
index 51f7c04..13273ba 100644
--- a/API.md
+++ b/API.md
@@ -5,14 +5,18 @@
## Common
Only `POST` requests are allowed.
-
+
For every request, you should have `Authorization` on its header.
-
+
Authorization header is a string like `Basic `(basic auth).
```
Authorization: Basic
```
+Example:
+```
+Authorization: Basic WFpCOjEyNDg=
+```
---
@@ -53,8 +57,12 @@ All responses are JSON encoded.
Parameters: **None**
-The `id` of device is persistent, its length always equals 64. It's unique for every device and won't change, so you should identify every device by this.
-
+The `id` of device is persistent, its length always equals 64.
+
+It's unique for every device and won't change.
+
+You're recommend to recognize your device by device ID.
+
The key of the device object is its connection UUID, it's random and temporary.
```
@@ -166,6 +174,47 @@ If file exists and is deleted successfully, then `code` will be `0`.
---
+### Upload file: `/device/file/upload`
+
+**Query Parameters**: `file` (file name), `path` and `device` (device ID)
+
+File itself should be sent in the request **body**.
+
+**Anything** represented in the request **body** will be saved to the device.
+
+If same file exists, then it will be **overwritten**.
+
+Example:
+```http request
+POST http://localhost:8000/api/device/file/upload?path=D%3A%5C&file=Test.txt&device=bc7e49f8f794f80ffb0032a4ba516c86d76041bf2023e1be6c5dda3b1ee0cf4c HTTP/1.1
+Host: localhost:8000
+Content-Length: 12
+Content-Type: application/octet-stream
+User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.47
+Origin: http://localhost:8000
+Referer: http://localhost:8000/
+
+Hello World.
+```
+
+If file uploaded successfully, then `code` will be `0`.
+
+And `D:\Test.txt` will be created with the content of `Hello World.`.
+
+```
+{
+ "code": 0
+}
+```
+```
+{
+ "code": 1,
+ "msg": "${i18n|fileOrDirNotExist}"
+}
+```
+
+---
+
### List files: `/device/file/list`
Parameters: `path` (folder to be listed) and `device` (device ID)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0d78f1..30e9c77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,13 @@
+## v0.0.8
+
+* Add: file upload.
+* Optimize: project structure.
+
+* 新增: 文件上传功能。
+* 优化: 项目结构。
+
+
+
## v0.0.7
* Add: detail info tooltip of cpu, ram and disk.
diff --git a/README.ZH.md b/README.ZH.md
index 73e3adb..5fb3ce3 100644
--- a/README.ZH.md
+++ b/README.ZH.md
@@ -66,6 +66,20 @@
---
+## 截图
+
+![overview](./screenshots/overview.ZH.png)
+
+![terminal](./screenshots/terminal.ZH.png)
+
+![procmgr](./screenshots/procmgr.ZH.png)
+
+![explorer](./screenshots/explorer.ZH.png)
+
+![overview.cpu](./screenshots/overview.cpu.ZH.png)
+
+---
+
## **开发**
### 注意
@@ -121,18 +135,6 @@ $ ./build.server.sh
---
-## 截图
-
-![overview](./screenshots/overview.ZH.png)
-
-![terminal](./screenshots/terminal.ZH.png)
-
-![procmgr](./screenshots/procmgr.ZH.png)
-
-![explorer](./screenshots/explorer.ZH.png)
-
----
-
## 项目依赖
Spark使用了许多第三方的开源项目。
diff --git a/README.md b/README.md
index 87f7b96..f867f33 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ Only local installation are available yet.
|-----------------|---------|-------|-------|
| Process manager | ✔ | ✔ | ✔ |
| Kill process | ✔ | ✔ | ✔ |
-| Network Traffic | ✔ | ✔ | ✔ |
+| Network traffic | ✔ | ✔ | ✔ |
| File explorer | ✔ | ✔ | ✔ |
| File transfer | ✔ | ✔ | ✔ |
| Delete file | ✔ | ✔ | ✔ |
@@ -66,6 +66,20 @@ Only local installation are available yet.
---
+## Screenshots
+
+![overview](./screenshots/overview.png)
+
+![terminal](./screenshots/terminal.png)
+
+![procmgr](./screenshots/procmgr.png)
+
+![explorer](./screenshots/explorer.png)
+
+![overview.cpu](./screenshots/overview.cpu.png)
+
+---
+
## **Development**
### note
@@ -123,18 +137,6 @@ Copy configuration file mentioned above into this dir, and then you can execute
---
-## Screenshots
-
-![overview](./screenshots/overview.png)
-
-![terminal](./screenshots/terminal.png)
-
-![procmgr](./screenshots/procmgr.png)
-
-![explorer](./screenshots/explorer.png)
-
----
-
## Dependencies
Spark contains many third-party open-source projects.
diff --git a/client/client.go b/client/client.go
index dad88fc..ad58ee4 100644
--- a/client/client.go
+++ b/client/client.go
@@ -48,6 +48,11 @@ func init() {
}
func main() {
+ update()
+ core.Start()
+}
+
+func update() {
if len(os.Args) > 1 && os.Args[1] == `--update` {
thisPath := os.Args[0]
destPath := thisPath[:len(thisPath)-4]
@@ -69,7 +74,6 @@ func main() {
<-time.After(time.Second)
os.Remove(os.Args[0] + `.tmp`)
}
- core.Start()
}
func decrypt(data []byte, key []byte) ([]byte, error) {
diff --git a/client/config/config.go b/client/config/config.go
index e37123c..ff8c53e 100644
--- a/client/config/config.go
+++ b/client/config/config.go
@@ -14,10 +14,11 @@ type Cfg struct {
Key string `json:"key"`
}
-// localhost for debug only
+// Localhost for my development only.
+// Shall be commented out when development is done.
//var CfgBuffer = "\x00\xcd\x90\x50\x43\xfc\x3d\x36\x56\x6d\xf6\x01\xd1\xcd\x81\xc3\x1b\x80\xc9\x61\xd8\xdf\x5b\x76\x48\x88\xc5\xb1\x74\x22\x23\xab\x3b\xfc\x8b\xbe\x98\x27\xed\x05\xec\xbb\x40\x4f\xe9\xe7\xe5\xe0\x84\xaa\xb7\xfd\x4a\x30\x71\x08\x6c\x02\x50\xe9\xc5\x22\xcf\xcb\x89\x16\x0a\x89\x08\xd4\x26\xdc\x5c\xc1\xc9\xbf\xc4\xac\x0d\x92\x2f\x34\x7f\x45\xeb\x55\xa0\x6d\xf6\x64\xbc\xd5\x15\x40\x96\x43\x64\xe0\x24\x51\xfb\xe8\xc9\x7f\x48\x60\xcd\x30\x5e\x5e\x78\xba\xb6\x6f\x07\x64\xe8\x59\x81\x0b\x91\x13\x92\x1a\xdd\x49\x8f\x28\xe7\x74\xea\xff\x5b\x45\x0e\x4a\x2d\x60\x4e\xc9\xde\x9c\xbe\x50\xc6\x12\xc7\x45\xa2\x15\xa0\x58\x62\x45\x86\x74\x9f\xa5\x14\x5c\x17\x8a\xcc\x56\x73\xa7\x75\xb7\xf6\x6d\x52\x0f\xb8\xc1\xff\x9c\x39\x39\x00\x74\xe1\x4d\x65\x73\x9c\x02\x57\x8b\xcf\xdf\x0a\x20\x4c\xed\xe2\x25\xea\x01\x36\x12\x37\x12\x2e\x1a\x03\x41\x19\x2e\xc9\xdd\x71\xac\x73\x90\xfa\x5e\x60\x08\x43\x35\xef\x61\x45\xf9\xe3\xba\xcb\xb1\xc5\x7c\xf0\x11\xcd\x47\x57\x53\xdc\x35\x6b\x9f\xac\xad\x43\x4a\xc7\x54\x20\xb8\xd0\xf8\xb5\x0c\x45\x76\x57\xb9\xee\x4a\x3f\xd2\xda\xf7\x94\x54\x74\xf3\x91\xf3\x4d\x49\x98\xc6\xf8\x60\x80\xad\x84\x04\xef\x35\xca\x3a\xcf\xd3\x7e\x74\xc2\x4b\xb8\xb3\x9f\xb2\x83\xb8\xbd\x29\x13\x9f\x2b\xaa\x60\x47\x24\x7e\x20\xb2\x85\xdc\x47\xfe\x8f\x68\xb6\xc3\x43\xad\x61\x3d\x9b\x35\x60\x2e\x6c\x44\xf0\xaf\xb2\xf3\xdb\xe2\x1b\x8a\xec\x0a\x48\x5e\x43\xa9\xb3\x3a\x5e\xb6\x90\xa9\x3d\xee\x4f\xa1\x57\x7c\x94\xf4\xb1\x36\xda\x04\xa8\x5e\x48\x2a\xc3\xa1\xf0\x97\xf0\xe0\x10\x46\x32\x10\xe5\xd8\x36\x5a\x56\xa5\xbb\x37\x3c\x9f\xbd\xef\xf5\x2f"
-// none
+// None
var CfgBuffer = "\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19"
// COMMIT means this commit hash, help to identify version and self upgrade.
diff --git a/client/core/core.go b/client/core/core.go
index 6073aa5..c41f732 100644
--- a/client/core/core.go
+++ b/client/core/core.go
@@ -40,6 +40,7 @@ var handlers = map[string]func(pack modules.Packet, wsConn *common.Conn){
`resizeTerminal`: resizeTerminal,
`killTerminal`: killTerminal,
`listFiles`: listFiles,
+ `fetchFile`: fetchFile,
`removeFile`: removeFile,
`uploadFile`: uploadFile,
`listProcesses`: listProcesses,
@@ -54,14 +55,14 @@ func Start() {
}
common.WSConn, err = connectWS()
if err != nil && !stop {
- golog.Error(err)
+ golog.Error(`Connection error: `, err)
<-time.After(3 * time.Second)
continue
}
err = reportWS(common.WSConn)
if err != nil && !stop {
- golog.Error(err)
+ golog.Error(`Register error: `, err)
<-time.After(3 * time.Second)
continue
}
@@ -72,7 +73,7 @@ func Start() {
err = handleWS(common.WSConn)
if err != nil && !stop {
- golog.Error(err)
+ golog.Error(`Execution error: `, err)
<-time.After(3 * time.Second)
continue
}
@@ -103,7 +104,7 @@ func reportWS(wsConn *common.Conn) error {
if err != nil {
return err
}
- pack := modules.CommonPack{Act: `report`, Data: device}
+ pack := modules.CommonPack{Act: `report`, Data: *device}
err = common.SendPack(pack, wsConn)
common.WSConn.SetWriteDeadline(time.Time{})
if err != nil {
@@ -226,7 +227,7 @@ func heartbeat(wsConn *common.Conn) error {
if t >= 20 {
t = 0
}
- err = common.SendPack(modules.CommonPack{Act: `setDevice`, Data: device}, wsConn)
+ err = common.SendPack(modules.CommonPack{Act: `setDevice`, Data: *device}, wsConn)
if err != nil {
return err
}
diff --git a/client/core/device.go b/client/core/device.go
index 6835050..4fd726d 100644
--- a/client/core/device.go
+++ b/client/core/device.go
@@ -245,7 +245,7 @@ func GetDevice() (*modules.Device, error) {
}, nil
}
-func GetPartialInfo(getDisk bool) (modules.Device, error) {
+func GetPartialInfo(getDisk bool) (*modules.Device, error) {
cpuInfo, err := GetCPUInfo()
if err != nil {
cpuInfo = modules.CPU{
@@ -280,7 +280,7 @@ func GetPartialInfo(getDisk bool) (modules.Device, error) {
if err != nil {
uptime = 0
}
- return modules.Device{
+ return &modules.Device{
Net: netInfo,
CPU: cpuInfo,
RAM: memInfo,
diff --git a/client/core/handler.go b/client/core/handler.go
index 75f42ab..a8714d6 100644
--- a/client/core/handler.go
+++ b/client/core/handler.go
@@ -13,41 +13,6 @@ import (
"strconv"
)
-func getPackData(pack modules.Packet, key string, t reflect.Kind) (interface{}, bool) {
- data, ok := pack.Data[key]
- if !ok {
- return nil, false
- }
- switch t {
- case reflect.String:
- val, ok := data.(string)
- return val, ok
- case reflect.Uint:
- val, ok := data.(uint)
- return val, ok
- case reflect.Uint32:
- val, ok := data.(uint32)
- return val, ok
- case reflect.Uint64:
- val, ok := data.(uint64)
- return val, ok
- case reflect.Int:
- val, ok := data.(int)
- return val, ok
- case reflect.Int64:
- val, ok := data.(int64)
- return val, ok
- case reflect.Bool:
- val, ok := data.(bool)
- return val, ok
- case reflect.Float64:
- val, ok := data.(float64)
- return val, ok
- default:
- return nil, false
- }
-}
-
func offline(pack modules.Packet, wsConn *common.Conn) {
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
stop = true
@@ -110,8 +75,16 @@ func shutdown(pack modules.Packet, wsConn *common.Conn) {
}
func screenshot(pack modules.Packet, wsConn *common.Conn) {
- if len(pack.Event) > 0 {
- Screenshot.GetScreenshot(pack.Event)
+ var bridge string
+ if val, ok := pack.GetData(`bridge`, reflect.String); !ok {
+ common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack, wsConn)
+ return
+ } else {
+ bridge = val.(string)
+ }
+ err := Screenshot.GetScreenshot(bridge)
+ if err != nil {
+ common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
}
}
@@ -136,7 +109,7 @@ func killTerminal(pack modules.Packet, wsConn *common.Conn) {
func listFiles(pack modules.Packet, wsConn *common.Conn) {
path := `/`
- if val, ok := getPackData(pack, `path`, reflect.String); ok {
+ if val, ok := pack.GetData(`path`, reflect.String); ok {
path = val.(string)
}
files, err := file.ListFiles(path)
@@ -147,9 +120,35 @@ func listFiles(pack modules.Packet, wsConn *common.Conn) {
}
}
+func fetchFile(pack modules.Packet, wsConn *common.Conn) {
+ var path, filename, bridge string
+ if val, ok := pack.GetData(`path`, reflect.String); !ok {
+ common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|fileOrDirNotExist}`}, pack, wsConn)
+ return
+ } else {
+ path = val.(string)
+ }
+ if val, ok := pack.GetData(`file`, reflect.String); !ok {
+ common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack, wsConn)
+ return
+ } else {
+ filename = val.(string)
+ }
+ if val, ok := pack.GetData(`bridge`, reflect.String); !ok {
+ common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack, wsConn)
+ return
+ } else {
+ bridge = val.(string)
+ }
+ err := file.FetchFile(path, filename, bridge)
+ if err != nil {
+ common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
+ }
+}
+
func removeFile(pack modules.Packet, wsConn *common.Conn) {
var path string
- if val, ok := getPackData(pack, `file`, reflect.String); !ok {
+ if val, ok := pack.GetData(`file`, reflect.String); !ok {
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|fileOrDirNotExist}`}, pack, wsConn)
return
} else {
@@ -165,18 +164,24 @@ func removeFile(pack modules.Packet, wsConn *common.Conn) {
func uploadFile(pack modules.Packet, wsConn *common.Conn) {
var start, end int64
- var path string
- if val, ok := getPackData(pack, `file`, reflect.String); !ok {
+ var path, bridge string
+ if val, ok := pack.GetData(`file`, reflect.String); !ok {
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|fileOrDirNotExist}`}, pack, wsConn)
return
} else {
path = val.(string)
}
+ if val, ok := pack.GetData(`bridge`, reflect.String); !ok {
+ common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack, wsConn)
+ return
+ } else {
+ bridge = val.(string)
+ }
{
- if val, ok := getPackData(pack, `start`, reflect.Float64); ok {
+ if val, ok := pack.GetData(`start`, reflect.Float64); ok {
start = int64(val.(float64))
}
- if val, ok := getPackData(pack, `end`, reflect.Float64); ok {
+ if val, ok := pack.GetData(`end`, reflect.Float64); ok {
end = int64(val.(float64))
if end > 0 {
end++
@@ -187,7 +192,7 @@ func uploadFile(pack modules.Packet, wsConn *common.Conn) {
return
}
}
- err := file.UploadFile(path, pack.Event, start, end)
+ err := file.UploadFile(path, bridge, start, end)
if err != nil {
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
}
@@ -207,7 +212,7 @@ func killProcess(pack modules.Packet, wsConn *common.Conn) {
pid int64
err error
)
- if val, ok := getPackData(pack, `pid`, reflect.String); ok {
+ if val, ok := pack.GetData(`pid`, reflect.String); ok {
pid, err = strconv.ParseInt(val.(string), 10, 32)
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
return
diff --git a/client/service/file/file.go b/client/service/file/file.go
index ae02752..e9cea12 100644
--- a/client/service/file/file.go
+++ b/client/service/file/file.go
@@ -6,12 +6,13 @@ import (
"io"
"io/ioutil"
"os"
+ "path"
"strconv"
"github.com/imroc/req/v3"
)
-type file struct {
+type File struct {
Name string `json:"name"`
Size uint64 `json:"size"`
Time int64 `json:"time"`
@@ -19,8 +20,8 @@ type file struct {
}
// listFiles returns files and directories find in path.
-func listFiles(path string) ([]file, error) {
- result := make([]file, 0)
+func listFiles(path string) ([]File, error) {
+ result := make([]File, 0)
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
@@ -30,7 +31,7 @@ func listFiles(path string) ([]file, error) {
if files[i].IsDir() {
itemType = 1
}
- result = append(result, file{
+ result = append(result, File{
Name: files[i].Name(),
Size: uint64(files[i].Size()),
Time: files[i].ModTime().Unix(),
@@ -40,6 +41,62 @@ func listFiles(path string) ([]file, error) {
return result, nil
}
+// FetchFile saves file from bridge to local.
+// Save body as temp file and when done, rename it to file.
+func FetchFile(dir, file, bridge string) error {
+ url := config.GetBaseURL(false) + `/api/bridge/pull`
+ client := req.C().DisableAutoReadResponse()
+ resp, err := client.R().SetQueryParam(`bridge`, bridge).Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ // If dest file exists, write to temp file first.
+ dest := path.Join(dir, file)
+ tmpFile := dest
+ destExists := false
+ if _, err := os.Stat(dest); !os.IsNotExist(err) {
+ tmpFile = getTempFileName(dir, file)
+ destExists = true
+ }
+
+ fh, err := os.Create(tmpFile)
+ if err != nil {
+ return err
+ }
+ for {
+ buf := make([]byte, 1024)
+ n, err := resp.Body.Read(buf)
+ if err != nil && err != io.EOF {
+ fh.Truncate(0)
+ fh.Close()
+ os.Remove(tmpFile)
+ return err
+ }
+ if n == 0 {
+ break
+ }
+ _, err = fh.Write(buf[:n])
+ if err != nil {
+ fh.Truncate(0)
+ fh.Close()
+ os.Remove(tmpFile)
+ return err
+ }
+ fh.Sync()
+ }
+ fh.Close()
+
+ // Delete old file if exists.
+ // Then rename temp file to file.
+ if destExists {
+ os.Remove(dest)
+ err = os.Rename(tmpFile, dest)
+ }
+ return err
+}
+
func RemoveFile(path string) error {
if path == `\` || path == `/` || len(path) == 0 {
return errors.New(`${i18n|fileOrDirNotExist}`)
@@ -51,7 +108,7 @@ func RemoveFile(path string) error {
return nil
}
-func UploadFile(path, trigger string, start, end int64) error {
+func UploadFile(path, bridge string, start, end int64) error {
file, err := os.Open(path)
if err != nil {
return err
@@ -65,7 +122,6 @@ func UploadFile(path, trigger string, start, end int64) error {
}
size := stat.Size()
headers := map[string]string{
- `Trigger`: trigger,
`FileName`: stat.Name(),
`FileSize`: strconv.FormatInt(size, 10),
}
@@ -96,8 +152,25 @@ func UploadFile(path, trigger string, start, end int64) error {
}
writer.Close()
}()
- url := config.GetBaseURL(false) + `/api/device/file/put`
- _, err = uploadReq.SetBody(reader).SetHeaders(headers).Send(`PUT`, url)
+ url := config.GetBaseURL(false) + `/api/bridge/push`
+ _, err = uploadReq.
+ SetBody(reader).
+ SetHeaders(headers).
+ SetQueryParam(`bridge`, bridge).
+ Send(`PUT`, url)
reader.Close()
return err
}
+
+func getTempFileName(dir, file string) string {
+ exists := true
+ tempFile := ``
+ for i := 0; exists; i++ {
+ tempFile = path.Join(dir, file+`.tmp.`+strconv.Itoa(i))
+ _, err := os.Stat(tempFile)
+ if os.IsNotExist(err) {
+ exists = false
+ }
+ }
+ return tempFile
+}
diff --git a/client/service/file/file_others.go b/client/service/file/file_others.go
index cbf9125..89266f2 100644
--- a/client/service/file/file_others.go
+++ b/client/service/file/file_others.go
@@ -1,8 +1,9 @@
+//go:build !windows
// +build !windows
package file
-func ListFiles(path string) ([]file, error) {
+func ListFiles(path string) ([]File, error) {
if len(path) == 0 {
path = `/`
}
diff --git a/client/service/file/file_windows.go b/client/service/file/file_windows.go
index a3e1c64..1a30455 100644
--- a/client/service/file/file_windows.go
+++ b/client/service/file/file_windows.go
@@ -8,8 +8,8 @@ import "github.com/shirou/gopsutil/v3/disk"
// ListFiles will only be called when path is root and
// current system is Windows.
// It will return mount points of all volumes.
-func ListFiles(path string) ([]file, error) {
- result := make([]file, 0)
+func ListFiles(path string) ([]File, error) {
+ result := make([]File, 0)
if len(path) == 0 || path == `\` || path == `/` {
partitions, err := disk.Partitions(true)
if err != nil {
@@ -23,7 +23,7 @@ func ListFiles(path string) ([]file, error) {
} else {
size = stat.Total
}
- result = append(result, file{Name: partitions[i].Mountpoint, Type: 2, Size: size})
+ result = append(result, File{Name: partitions[i].Mountpoint, Type: 2, Size: size})
}
return result, nil
}
diff --git a/client/service/screenshot/screenshot.go b/client/service/screenshot/screenshot.go
deleted file mode 100644
index e9eaf5b..0000000
--- a/client/service/screenshot/screenshot.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package screenshot
-
-import (
- "Spark/client/config"
- "github.com/imroc/req/v3"
-)
-
-func putScreenshot(trigger, err string, body interface{}) (*req.Response, error) {
- return req.R().
- SetBody(body).
- SetHeaders(map[string]string{
- `Trigger`: trigger,
- `Error`: err,
- }).
- Send(`PUT`, config.GetBaseURL(false)+`/api/device/screenshot/put`)
-}
diff --git a/client/service/screenshot/supported.go b/client/service/screenshot/supported.go
index 09e44a8..7d7f33c 100644
--- a/client/service/screenshot/supported.go
+++ b/client/service/screenshot/supported.go
@@ -3,30 +3,30 @@
package screenshot
import (
+ "Spark/client/config"
"bytes"
"errors"
+ "github.com/imroc/req/v3"
"github.com/kbinani/screenshot"
"image/png"
)
-func GetScreenshot(trigger string) error {
+func GetScreenshot(bridge string) error {
writer := new(bytes.Buffer)
num := screenshot.NumActiveDisplays()
if num == 0 {
err := errors.New(`${i18n|noDisplayFound}`)
- putScreenshot(trigger, err.Error(), nil)
return err
}
img, err := screenshot.CaptureDisplay(0)
if err != nil {
- putScreenshot(trigger, err.Error(), nil)
return err
}
err = png.Encode(writer, img)
if err != nil {
- putScreenshot(trigger, err.Error(), nil)
return err
}
- _, err = putScreenshot(trigger, ``, writer)
+ url := config.GetBaseURL(false) + `/api/bridge/push`
+ _, err = req.R().SetBody(writer.Bytes()).SetQueryParam(`bridge`, bridge).Put(url)
return err
}
diff --git a/client/service/screenshot/unsupported.go b/client/service/screenshot/unsupported.go
index f06de4d..ced7966 100644
--- a/client/service/screenshot/unsupported.go
+++ b/client/service/screenshot/unsupported.go
@@ -2,9 +2,6 @@
package screenshot
-import "Spark/utils"
-
-func GetScreenshot(trigger string) error {
- _, err := putScreenshot(trigger, utils.ErrUnsupported.Error(), nil)
- return err
+func GetScreenshot(bridge string) error {
+ return utils.ErrUnsupported
}
diff --git a/client/service/terminal/terminal_others.go b/client/service/terminal/terminal_others.go
index 552bcee..bad8bc9 100644
--- a/client/service/terminal/terminal_others.go
+++ b/client/service/terminal/terminal_others.go
@@ -10,13 +10,14 @@ import (
"github.com/creack/pty"
"os"
"os/exec"
+ "reflect"
"time"
)
type terminal struct {
lastPack int64
- event string
- pty *os.File
+ event string
+ pty *os.File
}
func init() {
@@ -30,8 +31,8 @@ func InitTerminal(pack modules.Packet) error {
return err
}
termSession := &terminal{
- pty: ptySession,
- event: pack.Event,
+ pty: ptySession,
+ event: pack.Event,
lastPack: time.Now().Unix(),
}
terminals.Set(pack.Data[`terminal`].(string), termSession)
@@ -55,86 +56,54 @@ func InitTerminal(pack modules.Packet) error {
}
func InputTerminal(pack modules.Packet) error {
- if pack.Data == nil {
- return errDataNotFound
- }
- val, ok := pack.Data[`input`]
- if !ok {
- return errDataNotFound
- }
- hexStr, ok := val.(string)
+ val, ok := pack.GetData(`input`, reflect.String)
if !ok {
return errDataNotFound
}
- data, err := hex.DecodeString(hexStr)
+ data, err := hex.DecodeString(val.(string))
if err != nil {
return errDataInvalid
}
- val, ok = pack.Data[`terminal`]
- if !ok {
- return errUUIDNotFound
- }
- termUUID, ok := val.(string)
+ val, ok = pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
}
+ termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
- terminal, ok := val.(*terminal)
- if !ok {
- common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
- return nil
- }
-
+ terminal := val.(*terminal)
terminal.lastPack = time.Now().Unix()
terminal.pty.Write(data)
return nil
}
func ResizeTerminal(pack modules.Packet) error {
- if pack.Data == nil {
- return errDataNotFound
- }
- val, ok := pack.Data[`width`]
- if !ok {
- return errDataInvalid
- }
- width, ok := val.(float64)
- if !ok {
- return errDataInvalid
- }
- val, ok = pack.Data[`height`]
+ val, ok := pack.GetData(`width`, reflect.Float64)
if !ok {
return errDataInvalid
}
- height, ok := val.(float64)
+ width := val.(float64)
+ val, ok = pack.GetData(`height`, reflect.Float64)
if !ok {
return errDataInvalid
}
+ height := val.(float64)
- val, ok = pack.Data[`terminal`]
- if !ok {
- return errUUIDNotFound
- }
- termUUID, ok := val.(string)
+ val, ok = pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
}
+ termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
- terminal, ok := val.(*terminal)
- if !ok {
- common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
- return nil
- }
-
+ terminal := val.(*terminal)
pty.Setsize(terminal.pty, &pty.Winsize{
Rows: uint16(height),
Cols: uint16(width),
@@ -143,28 +112,17 @@ func ResizeTerminal(pack modules.Packet) error {
}
func KillTerminal(pack modules.Packet) error {
- if pack.Data == nil {
- return errUUIDNotFound
- }
- val, ok := pack.Data[`terminal`]
- if !ok {
- return errUUIDNotFound
- }
- termUUID, ok := val.(string)
+ val, ok := pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
}
+ termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
- terminal, ok := val.(*terminal)
- if !ok {
- terminals.Remove(termUUID)
- common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
- return nil
- }
+ terminal := val.(*terminal)
doKillTerminal(terminal)
return nil
}
@@ -187,17 +145,13 @@ func getTerminal() string {
}
func healthCheck() {
- const MaxInterval = 180
+ const MaxInterval = 300
for now := range time.NewTicker(30 * time.Second).C {
timestamp := now.Unix()
// stores sessions to be disconnected
queue := make([]string, 0)
terminals.IterCb(func(uuid string, t interface{}) bool {
- termSession, ok := t.(*terminal)
- if !ok {
- queue = append(queue, uuid)
- return true
- }
+ termSession := t.(*terminal)
if timestamp-termSession.lastPack > MaxInterval {
queue = append(queue, uuid)
doKillTerminal(termSession)
diff --git a/client/service/terminal/terminal_windows.go b/client/service/terminal/terminal_windows.go
index 789c0f8..06f5612 100644
--- a/client/service/terminal/terminal_windows.go
+++ b/client/service/terminal/terminal_windows.go
@@ -9,6 +9,7 @@ import (
"io/ioutil"
"os"
"os/exec"
+ "reflect"
"runtime"
"time"
@@ -18,11 +19,11 @@ import (
type terminal struct {
lastPack int64
- event string
- cmd *exec.Cmd
- stdout *io.ReadCloser
- stderr *io.ReadCloser
- stdin *io.WriteCloser
+ event string
+ cmd *exec.Cmd
+ stdout *io.ReadCloser
+ stderr *io.ReadCloser
+ stdin *io.WriteCloser
}
func init() {
@@ -47,11 +48,11 @@ func InitTerminal(pack modules.Packet) error {
return err
}
termSession := &terminal{
- cmd: cmd,
- event: pack.Event,
- stdout: &stdout,
- stderr: &stderr,
- stdin: &stdin,
+ cmd: cmd,
+ event: pack.Event,
+ stdout: &stdout,
+ stderr: &stderr,
+ stdin: &stdin,
lastPack: time.Now().Unix(),
}
terminals.Set(pack.Data[`terminal`].(string), termSession)
@@ -86,41 +87,26 @@ func InitTerminal(pack modules.Packet) error {
}
func InputTerminal(pack modules.Packet) error {
- if pack.Data == nil {
- return errDataNotFound
- }
- val, ok := pack.Data[`input`]
- if !ok {
- return errDataNotFound
- }
- hexStr, ok := val.(string)
+ val, ok := pack.GetData(`input`, reflect.String)
if !ok {
return errDataNotFound
}
- data, err := hex.DecodeString(hexStr)
+ data, err := hex.DecodeString(val.(string))
if err != nil {
return errDataInvalid
}
- val, ok = pack.Data[`terminal`]
- if !ok {
- return errUUIDNotFound
- }
- termUUID, ok := val.(string)
+ val, ok = pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
}
+ termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
- terminal, ok := val.(*terminal)
- if !ok {
- common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
- return nil
- }
-
+ terminal := val.(*terminal)
terminal.lastPack = time.Now().Unix()
if len(data) == 1 && data[0] == '\x03' {
terminal.cmd.Process.Signal(os.Interrupt)
@@ -136,28 +122,17 @@ func ResizeTerminal(pack modules.Packet) error {
}
func KillTerminal(pack modules.Packet) error {
- if pack.Data == nil {
- return errUUIDNotFound
- }
- val, ok := pack.Data[`terminal`]
- if !ok {
- return errUUIDNotFound
- }
- termUUID, ok := val.(string)
+ val, ok := pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
}
+ termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
- terminal, ok := val.(*terminal)
- if !ok {
- terminals.Remove(termUUID)
- common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
- return nil
- }
+ terminal := val.(*terminal)
doKillTerminal(terminal)
return nil
}
@@ -210,17 +185,13 @@ func utf8ToGbk(s []byte) ([]byte, error) {
}
func healthCheck() {
- const MaxInterval = 180
+ const MaxInterval = 300
for now := range time.NewTicker(30 * time.Second).C {
timestamp := now.Unix()
// stores sessions to be disconnected
queue := make([]string, 0)
terminals.IterCb(func(uuid string, t interface{}) bool {
- termSession, ok := t.(*terminal)
- if !ok {
- queue = append(queue, uuid)
- return true
- }
+ termSession := t.(*terminal)
if timestamp-termSession.lastPack > MaxInterval {
queue = append(queue, uuid)
doKillTerminal(termSession)
diff --git a/modules/modules.go b/modules/modules.go
index 0f72ed4..c1664f6 100644
--- a/modules/modules.go
+++ b/modules/modules.go
@@ -1,5 +1,7 @@
package modules
+import "reflect"
+
type Packet struct {
Code int `json:"code"`
Act string `json:"act,omitempty"`
@@ -52,3 +54,41 @@ type Net struct {
Sent uint64 `json:"sent"`
Recv uint64 `json:"recv"`
}
+
+func (p *Packet) GetData(key string, t reflect.Kind) (interface{}, bool) {
+ if p.Data == nil {
+ return nil, false
+ }
+ data, ok := p.Data[key]
+ if !ok {
+ return nil, false
+ }
+ switch t {
+ case reflect.String:
+ val, ok := data.(string)
+ return val, ok
+ case reflect.Uint:
+ val, ok := data.(uint)
+ return val, ok
+ case reflect.Uint32:
+ val, ok := data.(uint32)
+ return val, ok
+ case reflect.Uint64:
+ val, ok := data.(uint64)
+ return val, ok
+ case reflect.Int:
+ val, ok := data.(int)
+ return val, ok
+ case reflect.Int64:
+ val, ok := data.(int64)
+ return val, ok
+ case reflect.Bool:
+ val, ok := data.(bool)
+ return val, ok
+ case reflect.Float64:
+ val, ok := data.(float64)
+ return val, ok
+ default:
+ return nil, false
+ }
+}
diff --git a/screenshots/explorer.ZH.png b/screenshots/explorer.ZH.png
index ef7ebf2..6010e05 100644
Binary files a/screenshots/explorer.ZH.png and b/screenshots/explorer.ZH.png differ
diff --git a/screenshots/explorer.png b/screenshots/explorer.png
index 8c5ec3b..0e5ad35 100644
Binary files a/screenshots/explorer.png and b/screenshots/explorer.png differ
diff --git a/screenshots/overview.ZH.png b/screenshots/overview.ZH.png
index f13a1e0..2919de7 100644
Binary files a/screenshots/overview.ZH.png and b/screenshots/overview.ZH.png differ
diff --git a/screenshots/overview.cpu.ZH.png b/screenshots/overview.cpu.ZH.png
new file mode 100644
index 0000000..d9a6926
Binary files /dev/null and b/screenshots/overview.cpu.ZH.png differ
diff --git a/screenshots/overview.cpu.png b/screenshots/overview.cpu.png
new file mode 100644
index 0000000..bacec4e
Binary files /dev/null and b/screenshots/overview.cpu.png differ
diff --git a/screenshots/overview.png b/screenshots/overview.png
index d9ab91e..1862fee 100644
Binary files a/screenshots/overview.png and b/screenshots/overview.png differ
diff --git a/screenshots/procmgr.ZH.png b/screenshots/procmgr.ZH.png
index a0f5f9a..eb86b6d 100644
Binary files a/screenshots/procmgr.ZH.png and b/screenshots/procmgr.ZH.png differ
diff --git a/screenshots/procmgr.png b/screenshots/procmgr.png
index c3e6bf9..63fb884 100644
Binary files a/screenshots/procmgr.png and b/screenshots/procmgr.png differ
diff --git a/screenshots/terminal.ZH.png b/screenshots/terminal.ZH.png
index 64833a7..81a9e69 100644
Binary files a/screenshots/terminal.ZH.png and b/screenshots/terminal.ZH.png differ
diff --git a/screenshots/terminal.png b/screenshots/terminal.png
index 84134a2..d4ca089 100644
Binary files a/screenshots/terminal.png and b/screenshots/terminal.png differ
diff --git a/build.client.bat b/scripts/build.client.bat
similarity index 100%
rename from build.client.bat
rename to scripts/build.client.bat
diff --git a/build.client.sh b/scripts/build.client.sh
similarity index 100%
rename from build.client.sh
rename to scripts/build.client.sh
diff --git a/scripts/build.server.bat b/scripts/build.server.bat
new file mode 100644
index 0000000..57d2333
--- /dev/null
+++ b/scripts/build.server.bat
@@ -0,0 +1,38 @@
+cd ..
+mkdir .\releases
+for /F %%i in ('git rev-parse HEAD') do ( set COMMIT=%%i)
+
+
+
+set GOOS=linux
+
+set GOARCH=arm
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_arm Spark/Server
+set GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_arm64 Spark/Server
+set GOARCH=386
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_i386 Spark/Server
+set GOARCH=amd64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_amd64 Spark/Server
+
+
+
+set GOOS=windows
+
+set GOARCH=arm
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_arm.exe Spark/Server
+set GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_arm64.exe Spark/Server
+set GOARCH=386
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/Server
+set GOARCH=amd64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/Server
+
+
+
+set GOOS=darwin
+
+set GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
+set GOARCH=amd64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
diff --git a/build.server.sh b/scripts/build.server.sh
similarity index 80%
rename from build.server.sh
rename to scripts/build.server.sh
index 52cf8d0..4c3ecb7 100644
--- a/build.server.sh
+++ b/scripts/build.server.sh
@@ -26,3 +26,12 @@ export GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/server
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/server
+
+
+
+export GOOS=darwin
+
+export GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
+export GOARCH=amd64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
diff --git a/server/common/common.go b/server/common/common.go
index fb90ae1..849ffee 100644
--- a/server/common/common.go
+++ b/server/common/common.go
@@ -10,7 +10,9 @@ import (
"crypto/cipher"
"encoding/hex"
"github.com/gin-gonic/gin"
+ "net"
"net/http"
+ "strings"
"time"
)
@@ -18,7 +20,7 @@ var Melody = melody.New()
var Devices = cmap.New()
var BuiltFS http.FileSystem
-func SendPackUUID(pack modules.Packet, uuid string) bool {
+func SendPackByUUID(pack modules.Packet, uuid string) bool {
session, ok := Melody.GetSessionByUUID(uuid)
if !ok {
return false
@@ -68,8 +70,7 @@ func Decrypt(data []byte, session *melody.Session) ([]byte, bool) {
return dec, true
}
-func WSHealthCheck(container *melody.Melody) {
- const MaxInterval = 90
+func HealthCheckWS(maxIdleSeconds int64, container *melody.Melody) {
go func() {
// ping client and update latency every 3 seconds
ping := func(uuid string, s *melody.Session) {
@@ -79,10 +80,8 @@ func WSHealthCheck(container *melody.Melody) {
AddEventOnce(func(packet modules.Packet, session *melody.Session) {
val, ok := Devices.Get(uuid)
if ok {
- deviceInfo, ok := val.(*modules.Device)
- if ok {
- deviceInfo.Latency = uint(time.Now().UnixMilli()-t) / 2
- }
+ deviceInfo := val.(*modules.Device)
+ deviceInfo.Latency = uint(time.Now().UnixMilli()-t) / 2
}
}, uuid, trigger, 3*time.Second)
}
@@ -108,7 +107,7 @@ func WSHealthCheck(container *melody.Melody) {
queue = append(queue, s)
return true
}
- if timestamp-lastPack > MaxInterval {
+ if timestamp-lastPack > maxIdleSeconds {
queue = append(queue, s)
}
return true
@@ -119,26 +118,71 @@ func WSHealthCheck(container *melody.Melody) {
}
}
-func CheckClientReq(ctx *gin.Context, cb func(*melody.Session)) bool {
+func GetRemoteAddr(ctx *gin.Context) string {
+ if remote, ok := ctx.RemoteIP(); ok {
+ if remote.IsLoopback() {
+ forwarded := ctx.GetHeader(`X-Forwarded-For`)
+ if len(forwarded) > 0 {
+ return forwarded
+ }
+ realIP := ctx.GetHeader(`X-Real-IP`)
+ if len(realIP) > 0 {
+ return realIP
+ }
+ } else {
+ if ip := remote.To4(); ip != nil {
+ return ip.String()
+ }
+ if ip := remote.To16(); ip != nil {
+ return ip.String()
+ }
+ }
+ }
+
+ remote := net.ParseIP(ctx.Request.RemoteAddr)
+ if remote != nil {
+ if remote.IsLoopback() {
+ forwarded := ctx.GetHeader(`X-Forwarded-For`)
+ if len(forwarded) > 0 {
+ return forwarded
+ }
+ realIP := ctx.GetHeader(`X-Real-IP`)
+ if len(realIP) > 0 {
+ return realIP
+ }
+ } else {
+ if ip := remote.To4(); ip != nil {
+ return ip.String()
+ }
+ if ip := remote.To16(); ip != nil {
+ return ip.String()
+ }
+ }
+ }
+ addr := ctx.Request.RemoteAddr
+ if pos := strings.LastIndex(addr, `:`); pos > -1 {
+ return strings.Trim(addr[:pos], `[]`)
+ }
+ return addr
+}
+
+func CheckClientReq(ctx *gin.Context) *melody.Session {
secret, err := hex.DecodeString(ctx.GetHeader(`Secret`))
if err != nil || len(secret) != 32 {
- return false
+ return nil
}
- find := false
+ var result *melody.Session = nil
Melody.IterSessions(func(uuid string, s *melody.Session) bool {
if val, ok := s.Get(`Secret`); ok {
// Check if there's a connection matches this secret.
if b, ok := val.([]byte); ok && bytes.Equal(b, secret) {
- find = true
- if cb != nil {
- cb(s)
- }
+ result = s
return false
}
}
return true
})
- return find
+ return result
}
func CheckDevice(deviceID, connUUID string) (string, bool) {
diff --git a/server/common/event.go b/server/common/event.go
index b2337fc..2f9e517 100644
--- a/server/common/event.go
+++ b/server/common/event.go
@@ -7,14 +7,15 @@ import (
"time"
)
+type EventCallback func(modules.Packet, *melody.Session)
type event struct {
connection string
callback EventCallback
- channel chan bool
+ finish chan bool
+ remove chan bool
}
-type EventCallback func(modules.Packet, *melody.Session)
-var eventTable = cmap.New()
+var events = cmap.New()
// CallEvent tries to call the callback with the given uuid
// after that, it will notify the caller via the channel
@@ -22,7 +23,7 @@ func CallEvent(pack modules.Packet, session *melody.Session) {
if len(pack.Event) == 0 {
return
}
- v, ok := eventTable.Get(pack.Event)
+ v, ok := events.Get(pack.Event)
if !ok {
return
}
@@ -31,12 +32,8 @@ func CallEvent(pack modules.Packet, session *melody.Session) {
return
}
ev.callback(pack, session)
- if ev.channel != nil {
- defer close(ev.channel)
- select {
- case ev.channel <- true:
- default:
- }
+ if ev.finish != nil {
+ ev.finish <- true
}
}
@@ -44,17 +41,21 @@ func CallEvent(pack modules.Packet, session *melody.Session) {
// can call back the event with the given event trigger.
// Event trigger should be uuid to make every event unique.
func AddEventOnce(fn EventCallback, connUUID, trigger string, timeout time.Duration) bool {
- done := make(chan bool)
ev := &event{
connection: connUUID,
callback: fn,
- channel: done,
+ finish: make(chan bool),
+ remove: make(chan bool),
}
- eventTable.Set(trigger, ev)
- defer eventTable.Remove(trigger)
+ events.Set(trigger, ev)
+ defer events.Remove(trigger)
+ defer close(ev.finish)
+ defer close(ev.remove)
select {
- case <-done:
- return true
+ case ok := <-ev.finish:
+ return ok
+ case ok := <-ev.remove:
+ return ok
case <-time.After(timeout):
return false
}
@@ -66,17 +67,26 @@ func AddEvent(fn EventCallback, connUUID, trigger string) {
ev := &event{
connection: connUUID,
callback: fn,
- channel: nil,
}
- eventTable.Set(trigger, ev)
+ events.Set(trigger, ev)
}
// RemoveEvent deletes the event with the given event trigger.
-func RemoveEvent(trigger string) {
- eventTable.Remove(trigger)
+// The ok will be returned to caller if the event is temp (only once).
+func RemoveEvent(trigger string, ok ...bool) {
+ v, found := events.Get(trigger)
+ if !found {
+ return
+ }
+ events.Remove(trigger)
+ if ev := v.(*event); ev.remove != nil {
+ if len(ok) > 0 {
+ ev.remove <- ok[0]
+ }
+ }
}
// HasEvent returns if the event exists.
func HasEvent(trigger string) bool {
- return eventTable.Has(trigger)
+ return events.Has(trigger)
}
diff --git a/server/handler/bridge.go b/server/handler/bridge.go
new file mode 100644
index 0000000..f3182a7
--- /dev/null
+++ b/server/handler/bridge.go
@@ -0,0 +1,182 @@
+package handler
+
+import (
+ "Spark/modules"
+ "Spark/utils/cmap"
+ "github.com/gin-gonic/gin"
+ "github.com/kataras/golog"
+ "io"
+ "net/http"
+ "sync"
+ "time"
+)
+
+// Bridge is a utility that handles the binary flow from the client
+// to the browser or flow from the browser to the client.
+
+type bridge struct {
+ creation int64
+ using bool
+ uuid string
+ lock *sync.Mutex
+ dest *gin.Context
+ src *gin.Context
+ ext interface{}
+ OnPull func(bridge *bridge)
+ OnPush func(bridge *bridge)
+ OnFinish func(bridge *bridge)
+}
+
+var bridges = cmap.New()
+
+func init() {
+ go func() {
+ for now := range time.NewTicker(10 * time.Second).C {
+ var queue []*bridge
+ bridges.IterCb(func(k string, v interface{}) bool {
+ b := v.(*bridge)
+ if b.creation < now.Unix()-60 && !b.using {
+ queue = append(queue, b)
+ }
+ return true
+ })
+ for _, b := range queue {
+ bridges.Remove(b.uuid)
+ b.lock.Lock()
+ if b.src != nil && b.src.Request.Body != nil {
+ b.src.Request.Body.Close()
+ }
+ b.src = nil
+ b.dest = nil
+ b.lock.Unlock()
+ b = nil
+ }
+ }
+ }()
+}
+
+func checkBridge(ctx *gin.Context) *bridge {
+ var form struct {
+ Bridge string `json:"bridge" yaml:"bridge" form:"bridge" binding:"required"`
+ }
+ if err := ctx.ShouldBind(&form); err != nil {
+ golog.Error(err)
+ ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ return nil
+ }
+ val, ok := bridges.Get(form.Bridge)
+ if !ok {
+ ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidBridgeID}`})
+ return nil
+ }
+ return val.(*bridge)
+}
+
+func bridgePush(ctx *gin.Context) {
+ bridge := checkBridge(ctx)
+ if bridge == nil {
+ return
+ }
+ bridge.lock.Lock()
+ if bridge.using || (bridge.src != nil && bridge.dest != nil) {
+ bridge.lock.Unlock()
+ ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
+ return
+ }
+ bridge.src = ctx
+ bridge.using = true
+ bridge.lock.Unlock()
+ if bridge.OnPush != nil {
+ bridge.OnPush(bridge)
+ }
+ if bridge.dest != nil && bridge.dest.Writer != nil {
+ io.Copy(bridge.dest.Writer, bridge.src.Request.Body)
+ bridge.src.Status(http.StatusOK)
+ if bridge.OnFinish != nil {
+ bridge.OnFinish(bridge)
+ }
+ removeBridge(bridge.uuid)
+ bridge = nil
+ }
+}
+
+func bridgePull(ctx *gin.Context) {
+ bridge := checkBridge(ctx)
+ if bridge == nil {
+ return
+ }
+ bridge.lock.Lock()
+ if bridge.using || (bridge.src != nil && bridge.dest != nil) {
+ bridge.lock.Unlock()
+ ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
+ return
+ }
+ bridge.dest = ctx
+ bridge.using = true
+ bridge.lock.Unlock()
+ if bridge.OnPull != nil {
+ bridge.OnPull(bridge)
+ }
+ if bridge.src != nil && bridge.src.Request.Body != nil {
+ io.Copy(bridge.dest.Writer, bridge.src.Request.Body)
+ bridge.src.Status(http.StatusOK)
+ if bridge.OnFinish != nil {
+ bridge.OnFinish(bridge)
+ }
+ removeBridge(bridge.uuid)
+ bridge = nil
+ }
+}
+
+func addBridge(ext interface{}, uuid string) *bridge {
+ bridge := &bridge{
+ creation: time.Now().Unix(),
+ uuid: uuid,
+ using: false,
+ lock: &sync.Mutex{},
+ ext: ext,
+ }
+ bridges.Set(uuid, bridge)
+ return bridge
+}
+
+func addBridgeWithSrc(ext interface{}, uuid string, src *gin.Context) *bridge {
+ bridge := &bridge{
+ creation: time.Now().Unix(),
+ uuid: uuid,
+ using: false,
+ lock: &sync.Mutex{},
+ ext: ext,
+ src: src,
+ }
+ bridges.Set(uuid, bridge)
+ return bridge
+}
+
+func addBridgeWithDest(ext interface{}, uuid string, dest *gin.Context) *bridge {
+ bridge := &bridge{
+ creation: time.Now().Unix(),
+ uuid: uuid,
+ using: false,
+ lock: &sync.Mutex{},
+ ext: ext,
+ dest: dest,
+ }
+ bridges.Set(uuid, bridge)
+ return bridge
+}
+
+func removeBridge(uuid string) {
+ val, ok := bridges.Get(uuid)
+ if !ok {
+ return
+ }
+ bridges.Remove(uuid)
+ b := val.(*bridge)
+ if b.src != nil && b.src.Request.Body != nil {
+ b.src.Request.Body.Close()
+ }
+ b.src = nil
+ b.dest = nil
+ b = nil
+}
diff --git a/server/handler/file.go b/server/handler/file.go
index 426aeb6..c69623b 100644
--- a/server/handler/file.go
+++ b/server/handler/file.go
@@ -7,7 +7,6 @@ import (
"Spark/utils/melody"
"fmt"
"github.com/gin-gonic/gin"
- "io/ioutil"
"net/http"
"net/url"
"path"
@@ -27,7 +26,7 @@ func removeDeviceFile(ctx *gin.Context) {
return
}
trigger := utils.GetStrUUID()
- common.SendPackUUID(modules.Packet{Code: 0, Act: `removeFile`, Data: gin.H{`file`: form.File}, Event: trigger}, target)
+ common.SendPackByUUID(modules.Packet{Code: 0, Act: `removeFile`, Data: gin.H{`file`: form.File}, Event: trigger}, target)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
@@ -50,7 +49,7 @@ func listDeviceFiles(ctx *gin.Context) {
return
}
trigger := utils.GetStrUUID()
- common.SendPackUUID(modules.Packet{Act: `listFiles`, Data: gin.H{`path`: form.Path}, Event: trigger}, target)
+ common.SendPackByUUID(modules.Packet{Act: `listFiles`, Data: gin.H{`path`: form.Path}, Event: trigger}, target)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
@@ -73,12 +72,13 @@ func getDeviceFile(ctx *gin.Context) {
if !ok {
return
}
+ bridgeID := utils.GetStrUUID()
trigger := utils.GetStrUUID()
var rangeStart, rangeEnd int64
var err error
partial := false
{
- command := gin.H{`file`: form.File}
+ command := gin.H{`file`: form.File, `bridge`: bridgeID}
rangeHeader := ctx.GetHeader(`Range`)
if len(rangeHeader) > 6 {
if rangeHeader[:6] != `bytes=` {
@@ -112,76 +112,57 @@ func getDeviceFile(ctx *gin.Context) {
command[`start`] = rangeStart
partial = true
}
- common.SendPackUUID(modules.Packet{Code: 0, Act: `uploadFile`, Data: command, Event: trigger}, target)
+ common.SendPackByUUID(modules.Packet{Code: 0, Act: `uploadFile`, Data: command, Event: trigger}, target)
}
-
wait := make(chan bool)
called := false
common.AddEvent(func(p modules.Packet, _ *melody.Session) {
+ wait <- false
called = true
+ removeBridge(bridgeID)
common.RemoveEvent(trigger)
- if p.Code != 0 {
- wait <- false
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
- return
- } else {
- val, ok := p.Data[`request`]
- if !ok {
- wait <- false
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|fileUploadFailed}`})
- return
- }
- req, ok := val.(*http.Request)
- if !ok || req == nil || req.Body == nil {
- wait <- false
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|fileUploadFailed}`})
- return
- }
-
- if req.ContentLength > 0 {
- ctx.Header(`Content-Length`, strconv.FormatInt(req.ContentLength, 10))
- }
- ctx.Header(`Accept-Ranges`, `bytes`)
- ctx.Header(`Content-Transfer-Encoding`, `binary`)
- ctx.Header(`Content-Type`, `application/octet-stream`)
- filename := ctx.GetHeader(`FileName`)
- if len(filename) == 0 {
- filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
- }
- filename = url.PathEscape(filename)
- ctx.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
+ ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ }, target, trigger)
+ instance := addBridgeWithDest(nil, bridgeID, ctx)
+ instance.OnPush = func(bridge *bridge) {
+ called = true
+ common.RemoveEvent(trigger)
+ src := bridge.src
+ if src.Request.ContentLength > 0 {
+ ctx.Header(`Content-Length`, strconv.FormatInt(src.Request.ContentLength, 10))
+ }
+ ctx.Header(`Accept-Ranges`, `bytes`)
+ ctx.Header(`Content-Transfer-Encoding`, `binary`)
+ ctx.Header(`Content-Type`, `application/octet-stream`)
+ filename := src.GetHeader(`FileName`)
+ if len(filename) == 0 {
+ filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
+ }
+ filename = url.PathEscape(filename)
+ ctx.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
- if partial {
- if rangeEnd == 0 {
- rangeEnd, err = strconv.ParseInt(req.Header.Get(`FileSize`), 10, 64)
- if err == nil {
- ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%d`, rangeStart, rangeEnd-1, rangeEnd))
- }
- } else {
- ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%v`, rangeStart, rangeEnd, req.Header.Get(`FileSize`)))
+ if partial {
+ if rangeEnd == 0 {
+ rangeEnd, err = strconv.ParseInt(src.GetHeader(`FileSize`), 10, 64)
+ if err == nil {
+ ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%d`, rangeStart, rangeEnd-1, rangeEnd))
}
- ctx.Status(http.StatusPartialContent)
} else {
- ctx.Status(http.StatusOK)
- }
-
- for {
- buffer := make([]byte, 8192)
- n, err := req.Body.Read(buffer)
- buffer = buffer[:n]
- ctx.Writer.Write(buffer)
- ctx.Writer.Flush()
- if n == 0 || err != nil {
- wait <- false
- break
- }
+ ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%v`, rangeStart, rangeEnd, src.GetHeader(`FileSize`)))
}
+ ctx.Status(http.StatusPartialContent)
+ } else {
+ ctx.Status(http.StatusOK)
}
- }, target, trigger)
+ }
+ instance.OnFinish = func(bridge *bridge) {
+ wait <- false
+ }
select {
case <-wait:
case <-time.After(5 * time.Second):
if !called {
+ removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else {
@@ -190,36 +171,62 @@ func getDeviceFile(ctx *gin.Context) {
}
}
-// putDeviceFile will be called by client.
-// It will transfer binary stream from client to browser.
-func putDeviceFile(ctx *gin.Context) {
- original := ctx.Request.Body
- ctx.Request.Body = ioutil.NopCloser(ctx.Request.Body)
-
- errMsg := ctx.GetHeader(`Error`)
- trigger := ctx.GetHeader(`Trigger`)
- if len(trigger) == 0 {
- original.Close()
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+// uploadToDevice handles file from browser
+// and transfer it to device.
+func uploadToDevice(ctx *gin.Context) {
+ var form struct {
+ Path string `json:"path" yaml:"path" form:"path" binding:"required"`
+ File string `json:"file" yaml:"file" form:"file" binding:"required"`
+ }
+ target, ok := checkForm(ctx, &form)
+ if !ok {
return
}
- if len(errMsg) > 0 {
- common.CallEvent(modules.Packet{
- Code: 1,
- Msg: fmt.Sprintf(`${i18n|fileUploadFailed}: %v`, errMsg),
- Event: trigger,
- }, nil)
- original.Close()
+ bridgeID := utils.GetStrUUID()
+ trigger := utils.GetStrUUID()
+ wait := make(chan bool)
+ called := false
+ common.AddEvent(func(p modules.Packet, _ *melody.Session) {
+ wait <- false
+ called = true
+ removeBridge(bridgeID)
+ common.RemoveEvent(trigger)
+ ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ }, target, trigger)
+ instance := addBridgeWithSrc(nil, bridgeID, ctx)
+ instance.OnPull = func(bridge *bridge) {
+ called = true
+ common.RemoveEvent(trigger)
+ dest := bridge.dest
+ if ctx.Request.ContentLength > 0 {
+ dest.Header(`Content-Length`, strconv.FormatInt(ctx.Request.ContentLength, 10))
+ }
+ dest.Header(`Accept-Ranges`, `none`)
+ dest.Header(`Content-Transfer-Encoding`, `binary`)
+ dest.Header(`Content-Type`, `application/octet-stream`)
+ filename := form.File
+ filename = url.PathEscape(filename)
+ dest.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
+ }
+ instance.OnFinish = func(bridge *bridge) {
+ wait <- false
+ }
+ common.SendPackByUUID(modules.Packet{Code: 0, Act: `fetchFile`, Data: gin.H{
+ `path`: form.Path,
+ `file`: form.File,
+ `bridge`: bridgeID,
+ }, Event: trigger}, target)
+ select {
+ case <-wait:
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
- return
+ case <-time.After(5 * time.Second):
+ if !called {
+ removeBridge(bridgeID)
+ common.RemoveEvent(trigger)
+ ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+ } else {
+ <-wait
+ ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
+ }
}
- common.CallEvent(modules.Packet{
- Code: 0,
- Data: map[string]interface{}{
- `request`: ctx.Request,
- },
- Event: trigger,
- }, nil)
- original.Close()
- ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
diff --git a/server/handler/handler.go b/server/handler/handler.go
index 35891f9..b09c109 100644
--- a/server/handler/handler.go
+++ b/server/handler/handler.go
@@ -3,30 +3,23 @@ package handler
import (
"Spark/modules"
"Spark/server/common"
- "Spark/server/config"
- "Spark/utils"
- "Spark/utils/melody"
- "bytes"
- "fmt"
"github.com/gin-gonic/gin"
- "github.com/kataras/golog"
"net/http"
- "strconv"
- "time"
)
-// APIRouter will initialize http and websocket routers.
-func APIRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
- ctx.PUT(`/device/screenshot/put`, putScreenshot) // Client, upload screenshot and forward to browser.
- ctx.PUT(`/device/file/put`, putDeviceFile) // Client, to upload file and forward to browser.
- ctx.Any(`/device/terminal`, initTerminal) // Browser, handle websocket events for web terminal.
- ctx.Any(`/client/update`, checkUpdate) // Client, for update.
+// InitRouter will initialize http and websocket routers.
+func InitRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
+ ctx.Any(`/bridge/push`, bridgePush)
+ ctx.Any(`/bridge/pull`, bridgePull)
+ ctx.Any(`/device/terminal`, initTerminal) // Browser, handle websocket events for web terminal.
+ ctx.Any(`/client/update`, checkUpdate) // Client, for update.
group := ctx.Group(`/`, auth)
{
group.POST(`/device/screenshot/get`, getScreenshot)
group.POST(`/device/process/list`, listDeviceProcesses)
group.POST(`/device/process/kill`, killDeviceProcess)
group.POST(`/device/file/remove`, removeDeviceFile)
+ group.POST(`/device/file/upload`, uploadToDevice)
group.POST(`/device/file/list`, listDeviceFiles)
group.POST(`/device/file/get`, getDeviceFile)
group.POST(`/device/list`, getDevices)
@@ -36,126 +29,6 @@ func APIRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
}
}
-// checkUpdate will check if client need update and return latest client if so.
-func checkUpdate(ctx *gin.Context) {
- var form struct {
- OS string `form:"os" binding:"required"`
- Arch string `form:"arch" binding:"required"`
- Commit string `form:"commit" binding:"required"`
- }
- if err := ctx.ShouldBind(&form); err != nil {
- golog.Error(err)
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
- return
- }
- if form.Commit == config.COMMIT {
- ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
- return
- }
- tpl, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
- if err != nil {
- ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
- return
- }
-
- const MaxBodySize = 384 // This is size of client config buffer.
- if ctx.Request.ContentLength > MaxBodySize {
- ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
- return
- }
- body, err := ctx.GetRawData()
- if err != nil {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
- return
- }
- auth := common.CheckClientReq(ctx, nil)
- if !auth {
- ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
- }
-
- ctx.Header(`Accept-Ranges`, `none`)
- ctx.Header(`Content-Transfer-Encoding`, `binary`)
- ctx.Header(`Content-Type`, `application/octet-stream`)
- if stat, err := tpl.Stat(); err == nil {
- ctx.Header(`Content-Length`, strconv.FormatInt(stat.Size(), 10))
- }
- cfgBuffer := bytes.Repeat([]byte{'\x19'}, 384)
- prevBuffer := make([]byte, 0)
- for {
- thisBuffer := make([]byte, 1024)
- n, err := tpl.Read(thisBuffer)
- thisBuffer = thisBuffer[:n]
- tempBuffer := append(prevBuffer, thisBuffer...)
- bufIndex := bytes.Index(tempBuffer, cfgBuffer)
- if bufIndex > -1 {
- tempBuffer = bytes.Replace(tempBuffer, cfgBuffer, body, -1)
- }
- ctx.Writer.Write(tempBuffer[:len(prevBuffer)])
- prevBuffer = tempBuffer[len(prevBuffer):]
- if err != nil {
- break
- }
- }
- if len(prevBuffer) > 0 {
- ctx.Writer.Write(prevBuffer)
- prevBuffer = []byte{}
- }
-}
-
-// getDevices will return all info about all clients.
-func getDevices(ctx *gin.Context) {
- devices := make(map[string]modules.Device)
- common.Devices.IterCb(func(uuid string, v interface{}) bool {
- device, ok := v.(*modules.Device)
- if ok {
- devices[uuid] = *device
- }
- return true
- })
- ctx.JSON(http.StatusOK, modules.CommonPack{Code: 0, Data: devices})
-}
-
-// callDevice will call client with command from browser.
-func callDevice(ctx *gin.Context) {
- act := ctx.Param(`act`)
- if len(act) == 0 {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
- return
- }
- {
- actions := []string{`lock`, `logoff`, `hibernate`, `suspend`, `restart`, `shutdown`, `offline`}
- ok := false
- for _, v := range actions {
- if v == act {
- ok = true
- break
- }
- }
- if !ok {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
- return
- }
- }
- connUUID, ok := checkForm(ctx, nil)
- if !ok {
- return
- }
- trigger := utils.GetStrUUID()
- common.SendPackUUID(modules.Packet{Act: act, Event: trigger}, connUUID)
- ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
- if p.Code != 0 {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
- } else {
- ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
- }
- }, connUUID, trigger, 5*time.Second)
- if !ok {
- //This means the client is offline.
- //So we take this as a success.
- ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
- }
-}
-
// checkForm checks if the form contains the required fields.
// Every request must contain connection UUID or device ID.
func checkForm(ctx *gin.Context, form interface{}) (string, bool) {
@@ -178,75 +51,3 @@ func checkForm(ctx *gin.Context, form interface{}) (string, bool) {
}
return connUUID, true
}
-
-// WSDevice handles events about device info.
-// Such as websocket handshake and update device info.
-func WSDevice(data []byte, session *melody.Session) error {
- var pack struct {
- Code int `json:"code,omitempty"`
- Act string `json:"act,omitempty"`
- Msg string `json:"msg,omitempty"`
- Device modules.Device `json:"data"`
- }
- err := utils.JSON.Unmarshal(data, &pack)
- if err != nil {
- golog.Error(err)
- session.Close()
- return err
- }
-
- addr, ok := session.Get(`Address`)
- if ok {
- pack.Device.WAN = addr.(string)
- } else {
- pack.Device.WAN = `Unknown`
- }
-
- if pack.Act == `report` {
- // Check if this device has already connected.
- // If so, then find the session and let client quit.
- // This will keep only one connection remained per device.
- exSession := ``
- common.Devices.IterCb(func(uuid string, v interface{}) bool {
- device := v.(*modules.Device)
- if device.ID == pack.Device.ID {
- exSession = uuid
- target, ok := common.Melody.GetSessionByUUID(uuid)
- if ok {
- common.SendPack(modules.Packet{Act: `offline`}, target)
- target.Close()
- }
- return false
- }
- return true
- })
- if len(exSession) > 0 {
- common.Devices.Remove(exSession)
- }
- }
- common.SendPack(modules.Packet{Code: 0}, session)
-
- {
- val, ok := common.Devices.Get(session.UUID)
- if ok {
- deviceInfo, ok := val.(*modules.Device)
- if ok {
- deviceInfo.CPU = pack.Device.CPU
- deviceInfo.RAM = pack.Device.RAM
- deviceInfo.Net = pack.Device.Net
- if pack.Device.Disk.Total > 0 {
- deviceInfo.Disk = pack.Device.Disk
- }
- deviceInfo.Uptime = pack.Device.Uptime
- return nil
- }
- }
- common.Devices.Set(session.UUID, &pack.Device)
- }
- return nil
-}
-
-// WSRouter handles all packets from client.
-func WSRouter(pack modules.Packet, session *melody.Session) {
- common.CallEvent(pack, session)
-}
diff --git a/server/handler/process.go b/server/handler/process.go
index f347df1..f30896f 100644
--- a/server/handler/process.go
+++ b/server/handler/process.go
@@ -18,7 +18,7 @@ func listDeviceProcesses(ctx *gin.Context) {
return
}
trigger := utils.GetStrUUID()
- common.SendPackUUID(modules.Packet{Act: `listProcesses`, Event: trigger}, connUUID)
+ common.SendPackByUUID(modules.Packet{Act: `listProcesses`, Event: trigger}, connUUID)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
@@ -42,7 +42,7 @@ func killDeviceProcess(ctx *gin.Context) {
return
}
trigger := utils.GetStrUUID()
- common.SendPackUUID(modules.Packet{Code: 0, Act: `killProcess`, Data: gin.H{`pid`: strconv.FormatInt(int64(form.Pid), 10)}, Event: trigger}, target)
+ common.SendPackByUUID(modules.Packet{Code: 0, Act: `killProcess`, Data: gin.H{`pid`: strconv.FormatInt(int64(form.Pid), 10)}, Event: trigger}, target)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
diff --git a/server/handler/screenshot.go b/server/handler/screenshot.go
index 5bc36fc..fc6c84f 100644
--- a/server/handler/screenshot.go
+++ b/server/handler/screenshot.go
@@ -5,82 +5,47 @@ import (
"Spark/server/common"
"Spark/utils"
"Spark/utils/melody"
- "fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
-// putScreenshot will forward screenshot image from client to browser.
-func putScreenshot(ctx *gin.Context) {
- errMsg := ctx.GetHeader(`Error`)
- trigger := ctx.GetHeader(`Trigger`)
- if len(trigger) == 0 {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
- return
- }
- if len(errMsg) > 0 {
- common.CallEvent(modules.Packet{
- Code: 1,
- Msg: fmt.Sprintf(`${i18n|screenshotFailed}: %v`, errMsg),
- Event: trigger,
- }, nil)
- ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
- return
- }
- data, err := ctx.GetRawData()
- if len(data) == 0 {
- msg := ``
- if err != nil {
- msg = fmt.Sprintf(`${i18n|screenshotObtainFailed}: %v`, err)
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: msg})
- } else {
- msg = `${i18n|screenshotFailed}: ${i18n|unknownError}`
- ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
- }
- common.CallEvent(modules.Packet{
- Code: 1,
- Msg: msg,
- Event: trigger,
- }, nil)
- return
- }
- common.CallEvent(modules.Packet{
- Code: 0,
- Data: map[string]interface{}{
- `screenshot`: data,
- },
- Event: trigger,
- }, nil)
- ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
-}
-
// getScreenshot will call client to screenshot.
func getScreenshot(ctx *gin.Context) {
target, ok := checkForm(ctx, nil)
if !ok {
return
}
+ bridgeID := utils.GetStrUUID()
trigger := utils.GetStrUUID()
- common.SendPackUUID(modules.Packet{Code: 0, Act: `screenshot`, Event: trigger}, target)
- ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
- if p.Code != 0 {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ wait := make(chan bool)
+ called := false
+ common.SendPackByUUID(modules.Packet{Code: 0, Act: `screenshot`, Data: gin.H{`bridge`: bridgeID}, Event: trigger}, target)
+ common.AddEvent(func(p modules.Packet, _ *melody.Session) {
+ wait <- false
+ called = true
+ removeBridge(bridgeID)
+ common.RemoveEvent(trigger)
+ ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ }, target, trigger)
+ instance := addBridgeWithDest(nil, bridgeID, ctx)
+ instance.OnPush = func(bridge *bridge) {
+ called = true
+ common.RemoveEvent(trigger)
+ ctx.Header(`Content-Type`, `image/png`)
+ }
+ instance.OnFinish = func(bridge *bridge) {
+ wait <- false
+ }
+ select {
+ case <-wait:
+ case <-time.After(5 * time.Second):
+ if !called {
+ removeBridge(bridgeID)
+ common.RemoveEvent(trigger)
+ ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else {
- data, ok := p.Data[`screenshot`]
- if !ok {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|screenshotObtainFailed}`})
- return
- }
- screenshot, ok := data.([]byte)
- if !ok {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|screenshotObtainFailed}`})
- return
- }
- ctx.Data(200, `image/png`, screenshot)
+ <-wait
}
- }, target, trigger, 5*time.Second)
- if !ok {
- ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
}
}
diff --git a/server/handler/terminal.go b/server/handler/terminal.go
index f031ec7..5e97127 100644
--- a/server/handler/terminal.go
+++ b/server/handler/terminal.go
@@ -15,18 +15,18 @@ import (
)
type terminal struct {
+ uuid string
+ event string
+ device string
session *melody.Session
deviceConn *melody.Session
- device string
- termUUID string
- eventUUID string
}
var terminals = cmap.New()
-var wsTerminals = melody.New()
+var wsSessions = melody.New()
func init() {
- wsTerminals.HandleConnect(func(session *melody.Session) {
+ wsSessions.HandleConnect(func(session *melody.Session) {
device, ok := session.Get(`Device`)
if !ok {
simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
@@ -59,11 +59,11 @@ func init() {
}
eventUUID := utils.GetStrUUID()
terminal := &terminal{
+ uuid: termUUID,
+ event: eventUUID,
+ device: device.(string),
session: session,
deviceConn: deviceConn,
- device: device.(string),
- termUUID: termUUID,
- eventUUID: eventUUID,
}
terminals.Set(termUUID, terminal)
common.AddEvent(eventWrapper(terminal), connUUID, eventUUID)
@@ -71,9 +71,9 @@ func init() {
`terminal`: termUUID,
}, Event: eventUUID}, deviceConn)
})
- wsTerminals.HandleMessage(onMessage)
- wsTerminals.HandleMessageBinary(onMessage)
- wsTerminals.HandleDisconnect(func(session *melody.Session) {
+ wsSessions.HandleMessage(onMessage)
+ wsSessions.HandleMessageBinary(onMessage)
+ wsSessions.HandleDisconnect(func(session *melody.Session) {
val, ok := session.Get(`Terminal`)
if !ok {
return
@@ -86,17 +86,14 @@ func init() {
if !ok {
return
}
- terminal, ok := val.(*terminal)
- if !ok {
- return
- }
+ terminal := val.(*terminal)
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
- `terminal`: terminal.termUUID,
- }, Event: terminal.eventUUID}, terminal.deviceConn)
+ `terminal`: termUUID,
+ }, Event: terminal.event}, terminal.deviceConn)
terminals.Remove(termUUID)
- common.RemoveEvent(terminal.eventUUID)
+ common.RemoveEvent(terminal.event)
})
- go common.WSHealthCheck(wsTerminals)
+ go common.HealthCheckWS(300, wsSessions)
}
// initTerminal handles terminal websocket handshake event
@@ -125,7 +122,7 @@ func initTerminal(ctx *gin.Context) {
return
}
- wsTerminals.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
+ wsSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
`Secret`: secret,
`Device`: device,
`LastPack`: time.Now().Unix(),
@@ -146,8 +143,8 @@ func eventWrapper(terminal *terminal) common.EventCallback {
msg += `${i18n|unknownError}`
}
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
- terminals.Remove(terminal.termUUID)
- common.RemoveEvent(terminal.eventUUID)
+ terminals.Remove(terminal.uuid)
+ common.RemoveEvent(terminal.event)
terminal.session.Close()
}
return
@@ -158,8 +155,8 @@ func eventWrapper(terminal *terminal) common.EventCallback {
msg = pack.Msg
}
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
- terminals.Remove(terminal.termUUID)
- common.RemoveEvent(terminal.eventUUID)
+ terminals.Remove(terminal.uuid)
+ common.RemoveEvent(terminal.event)
terminal.session.Close()
return
}
@@ -246,18 +243,15 @@ func onMessage(session *melody.Session, data []byte) {
if !ok {
return
}
- terminal, ok := val.(*terminal)
- if !ok {
- return
- }
+ terminal := val.(*terminal)
if pack.Data == nil {
return
}
if input, ok := pack.Data[`input`]; ok {
common.SendPack(modules.Packet{Act: `inputTerminal`, Data: gin.H{
`input`: input,
- `terminal`: terminal.termUUID,
- }, Event: terminal.eventUUID}, terminal.deviceConn)
+ `terminal`: terminal.uuid,
+ }, Event: terminal.event}, terminal.deviceConn)
}
}
if pack.Act == `resizeTerminal` {
@@ -273,10 +267,7 @@ func onMessage(session *melody.Session, data []byte) {
if !ok {
return
}
- terminal, ok := val.(*terminal)
- if !ok {
- return
- }
+ terminal := val.(*terminal)
if pack.Data == nil {
return
}
@@ -285,26 +276,44 @@ func onMessage(session *melody.Session, data []byte) {
common.SendPack(modules.Packet{Act: `resizeTerminal`, Data: gin.H{
`width`: width,
`height`: height,
- `terminal`: terminal.termUUID,
- }, Event: terminal.eventUUID}, terminal.deviceConn)
+ `terminal`: terminal.uuid,
+ }, Event: terminal.event}, terminal.deviceConn)
}
}
}
+ if pack.Act == `killTerminal` {
+ val, ok := session.Get(`Terminal`)
+ if !ok {
+ return
+ }
+ termUUID, ok := val.(string)
+ if !ok {
+ return
+ }
+ val, ok = terminals.Get(termUUID)
+ if !ok {
+ return
+ }
+ terminal := val.(*terminal)
+ if pack.Data == nil {
+ return
+ }
+ common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
+ `terminal`: termUUID,
+ }, Event: terminal.event}, terminal.deviceConn)
+ }
}
func CloseSessionsByDevice(deviceID string) {
var queue []string
terminals.IterCb(func(key string, val interface{}) bool {
- terminal, ok := val.(*terminal)
- if !ok {
- return false
- }
+ terminal := val.(*terminal)
if terminal.device == deviceID {
- common.RemoveEvent(terminal.eventUUID)
+ common.RemoveEvent(terminal.event)
terminal.session.Close()
queue = append(queue, key)
}
- return false
+ return true
})
for _, key := range queue {
diff --git a/server/handler/utility.go b/server/handler/utility.go
new file mode 100644
index 0000000..ea82f34
--- /dev/null
+++ b/server/handler/utility.go
@@ -0,0 +1,200 @@
+package handler
+
+import (
+ "Spark/modules"
+ "Spark/server/common"
+ "Spark/server/config"
+ "Spark/utils"
+ "Spark/utils/melody"
+ "bytes"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/kataras/golog"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+// OnDevicePack handles events about device info.
+// Such as websocket handshake and update device info.
+func OnDevicePack(data []byte, session *melody.Session) error {
+ var pack struct {
+ Code int `json:"code,omitempty"`
+ Act string `json:"act,omitempty"`
+ Msg string `json:"msg,omitempty"`
+ Device modules.Device `json:"data"`
+ }
+ err := utils.JSON.Unmarshal(data, &pack)
+ if err != nil {
+ golog.Error(err)
+ session.Close()
+ return err
+ }
+
+ addr, ok := session.Get(`Address`)
+ if ok {
+ pack.Device.WAN = addr.(string)
+ } else {
+ pack.Device.WAN = `Unknown`
+ }
+
+ if pack.Act == `report` {
+ // Check if this device has already connected.
+ // If so, then find the session and let client quit.
+ // This will keep only one connection remained per device.
+ exSession := ``
+ common.Devices.IterCb(func(uuid string, v interface{}) bool {
+ device := v.(*modules.Device)
+ if device.ID == pack.Device.ID {
+ exSession = uuid
+ target, ok := common.Melody.GetSessionByUUID(uuid)
+ if ok {
+ common.SendPack(modules.Packet{Act: `offline`}, target)
+ target.Close()
+ }
+ return false
+ }
+ return true
+ })
+ if len(exSession) > 0 {
+ common.Devices.Remove(exSession)
+ }
+ }
+ common.SendPack(modules.Packet{Code: 0}, session)
+
+ {
+ val, ok := common.Devices.Get(session.UUID)
+ if ok {
+ deviceInfo := val.(*modules.Device)
+ deviceInfo.CPU = pack.Device.CPU
+ deviceInfo.RAM = pack.Device.RAM
+ deviceInfo.Net = pack.Device.Net
+ if pack.Device.Disk.Total > 0 {
+ deviceInfo.Disk = pack.Device.Disk
+ }
+ deviceInfo.Uptime = pack.Device.Uptime
+ return nil
+ }
+ common.Devices.Set(session.UUID, &pack.Device)
+ }
+ return nil
+}
+
+// checkUpdate will check if client need update and return latest client if so.
+func checkUpdate(ctx *gin.Context) {
+ var form struct {
+ OS string `form:"os" binding:"required"`
+ Arch string `form:"arch" binding:"required"`
+ Commit string `form:"commit" binding:"required"`
+ }
+ if err := ctx.ShouldBind(&form); err != nil {
+ golog.Error(err)
+ ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ return
+ }
+ if form.Commit == config.COMMIT {
+ ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
+ return
+ }
+ tpl, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
+ if err != nil {
+ ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
+ return
+ }
+
+ const MaxBodySize = 384 // This is size of client config buffer.
+ if ctx.Request.ContentLength > MaxBodySize {
+ ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
+ return
+ }
+ body, err := ctx.GetRawData()
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
+ return
+ }
+ session := common.CheckClientReq(ctx)
+ if session == nil {
+ ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
+ return
+ }
+
+ ctx.Header(`Accept-Ranges`, `none`)
+ ctx.Header(`Content-Transfer-Encoding`, `binary`)
+ ctx.Header(`Content-Type`, `application/octet-stream`)
+ if stat, err := tpl.Stat(); err == nil {
+ ctx.Header(`Content-Length`, strconv.FormatInt(stat.Size(), 10))
+ }
+ cfgBuffer := bytes.Repeat([]byte{'\x19'}, 384)
+ prevBuffer := make([]byte, 0)
+ for {
+ thisBuffer := make([]byte, 1024)
+ n, err := tpl.Read(thisBuffer)
+ thisBuffer = thisBuffer[:n]
+ tempBuffer := append(prevBuffer, thisBuffer...)
+ bufIndex := bytes.Index(tempBuffer, cfgBuffer)
+ if bufIndex > -1 {
+ tempBuffer = bytes.Replace(tempBuffer, cfgBuffer, body, -1)
+ }
+ ctx.Writer.Write(tempBuffer[:len(prevBuffer)])
+ prevBuffer = tempBuffer[len(prevBuffer):]
+ if err != nil {
+ break
+ }
+ }
+ if len(prevBuffer) > 0 {
+ ctx.Writer.Write(prevBuffer)
+ prevBuffer = []byte{}
+ }
+}
+
+// getDevices will return all info about all clients.
+func getDevices(ctx *gin.Context) {
+ devices := map[string]interface{}{}
+ common.Devices.IterCb(func(uuid string, v interface{}) bool {
+ device := v.(*modules.Device)
+ devices[uuid] = *device
+ return true
+ })
+ ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: devices})
+}
+
+// callDevice will call client with command from browser.
+func callDevice(ctx *gin.Context) {
+ act := ctx.Param(`act`)
+ if len(act) == 0 {
+ ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ return
+ }
+ {
+ actions := []string{`lock`, `logoff`, `hibernate`, `suspend`, `restart`, `shutdown`, `offline`}
+ ok := false
+ for _, v := range actions {
+ if v == act {
+ ok = true
+ break
+ }
+ }
+ if !ok {
+ ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ return
+ }
+ }
+ connUUID, ok := checkForm(ctx, nil)
+ if !ok {
+ return
+ }
+ trigger := utils.GetStrUUID()
+ common.SendPackByUUID(modules.Packet{Act: act, Event: trigger}, connUUID)
+ ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
+ if p.Code != 0 {
+ ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ } else {
+ ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
+ }
+ }, connUUID, trigger, 5*time.Second)
+ if !ok {
+ //This means the client is offline.
+ //So we take this as a success.
+ ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
+ }
+}
diff --git a/server/main.go b/server/main.go
index d23a333..ab32c06 100644
--- a/server/main.go
+++ b/server/main.go
@@ -7,10 +7,8 @@ import (
"Spark/server/handler"
"bytes"
"context"
- "net"
"os"
"os/signal"
- "strings"
"syscall"
"time"
@@ -61,19 +59,21 @@ func main() {
return
}
app := gin.New()
- auth := gin.BasicAuth(config.Config.Auth)
- handler.APIRouter(app.Group(`/api`), auth)
- app.Any(`/ws`, wsHandshake)
- app.NoRoute(auth, func(ctx *gin.Context) {
- http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request)
- })
+ {
+ auth := gin.BasicAuth(config.Config.Auth)
+ handler.InitRouter(app.Group(`/api`), auth)
+ app.Any(`/ws`, wsHandshake)
+ app.NoRoute(auth, func(ctx *gin.Context) {
+ http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request)
+ })
+ }
common.Melody.Config.MaxMessageSize = 1024
common.Melody.HandleConnect(wsOnConnect)
common.Melody.HandleMessage(wsOnMessage)
common.Melody.HandleMessageBinary(wsOnMessageBinary)
common.Melody.HandleDisconnect(wsOnDisconnect)
- go common.WSHealthCheck(common.Melody)
+ go common.HealthCheckWS(90, common.Melody)
srv := &http.Server{Addr: config.Config.Listen, Handler: app}
go func() {
@@ -114,7 +114,7 @@ func wsHandshake(ctx *gin.Context) {
}, gin.H{
`Secret`: secret,
`LastPack`: time.Now().Unix(),
- `Address`: getRemoteAddr(ctx),
+ `Address`: common.GetRemoteAddr(ctx),
})
if err != nil {
golog.Error(err)
@@ -134,12 +134,12 @@ func wsHandshake(ctx *gin.Context) {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
return
}
- auth := common.CheckClientReq(ctx, func(s *melody.Session) {
- wsOnMessageBinary(s, body)
- })
- if !auth {
+ session := common.CheckClientReq(ctx)
+ if session == nil {
ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
+ return
}
+ wsOnMessageBinary(session, body)
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}
@@ -161,71 +161,22 @@ func wsOnMessageBinary(session *melody.Session, data []byte) {
}
if pack.Act == `report` || pack.Act == `setDevice` {
session.Set(`LastPack`, time.Now().Unix())
- handler.WSDevice(data, session)
+ handler.OnDevicePack(data, session)
return
}
if !common.Devices.Has(session.UUID) {
session.Close()
return
}
- handler.WSRouter(pack, session)
+ common.CallEvent(pack, session)
session.Set(`LastPack`, time.Now().Unix())
}
func wsOnDisconnect(session *melody.Session) {
if val, ok := common.Devices.Get(session.UUID); ok {
- if deviceInfo, ok := val.(*modules.Device); ok {
- handler.CloseSessionsByDevice(deviceInfo.ID)
- }
+ deviceInfo := val.(*modules.Device)
+ handler.CloseSessionsByDevice(deviceInfo.ID)
}
common.Devices.Remove(session.UUID)
}
-
-func getRemoteAddr(ctx *gin.Context) string {
- if remote, ok := ctx.RemoteIP(); ok {
- if remote.IsLoopback() {
- forwarded := ctx.GetHeader(`X-Forwarded-For`)
- if len(forwarded) > 0 {
- return forwarded
- }
- realIP := ctx.GetHeader(`X-Real-IP`)
- if len(realIP) > 0 {
- return realIP
- }
- } else {
- if ip := remote.To4(); ip != nil {
- return ip.String()
- }
- if ip := remote.To16(); ip != nil {
- return ip.String()
- }
- }
- }
-
- remote := net.ParseIP(ctx.Request.RemoteAddr)
- if remote != nil {
- if remote.IsLoopback() {
- forwarded := ctx.GetHeader(`X-Forwarded-For`)
- if len(forwarded) > 0 {
- return forwarded
- }
- realIP := ctx.GetHeader(`X-Real-IP`)
- if len(realIP) > 0 {
- return realIP
- }
- } else {
- if ip := remote.To4(); ip != nil {
- return ip.String()
- }
- if ip := remote.To16(); ip != nil {
- return ip.String()
- }
- }
- }
- addr := ctx.Request.RemoteAddr
- if pos := strings.LastIndex(addr, `:`); pos > -1 {
- return strings.Trim(addr[:pos], `[]`)
- }
- return addr
-}
diff --git a/utils/utils.go b/utils/utils.go
index 0915414..f16d32b 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -36,6 +36,7 @@ func GetMD5(data []byte) ([]byte, string) {
hash := md5.New()
hash.Write(data)
result := hash.Sum(nil)
+ hash.Reset()
return result, hex.EncodeToString(result)
}
@@ -73,9 +74,13 @@ func Decrypt(data []byte, key []byte) ([]byte, error) {
hash, _ := GetMD5(decBuffer)
if !bytes.Equal(hash, data[:16]) {
+ data = nil
+ decBuffer = nil
return nil, ErrFailedVerification
}
+ data = nil
+ decBuffer = decBuffer[:dataLen-16-64]
//fmt.Println(`Recv: `, string(decBuffer[:dataLen-16-64]))
- return decBuffer[:dataLen-16-64], nil
+ return decBuffer, nil
}
diff --git a/web/src/components/explorer.css b/web/src/components/explorer.css
index 7ae2ecf..7a9afca 100644
--- a/web/src/components/explorer.css
+++ b/web/src/components/explorer.css
@@ -6,4 +6,8 @@
.ant-table-body {
max-height: 300px;
min-height: 300px;
+}
+
+.upload-progress-square > .ant-progress-outer > .ant-progress-inner {
+ border-radius: 0 !important;
}
\ No newline at end of file
diff --git a/web/src/components/explorer.js b/web/src/components/explorer.js
index e7271cd..6da8c8c 100644
--- a/web/src/components/explorer.js
+++ b/web/src/components/explorer.js
@@ -1,14 +1,19 @@
import React, {useEffect, useRef, useState} from 'react';
-import {message, Modal, Popconfirm} from "antd";
+import {message, Modal, Popconfirm, Progress} from "antd";
import ProTable from '@ant-design/pro-table';
import {formatSize, post, request, waitTime} from "../utils/utils";
import dayjs from "dayjs";
import i18n from "../locale/locale";
import './explorer.css';
+import {ReloadOutlined, UploadOutlined} from "@ant-design/icons";
+import axios from "axios";
+import Qs from "qs";
+let fileList = [];
function FileBrowser(props) {
const [path, setPath] = useState(`/`);
const [loading, setLoading] = useState(false);
+ const [upload, setUpload] = useState(false);
const columns = [
{
key: 'Name',
@@ -91,7 +96,7 @@ function FileBrowser(props) {
function onRowClick(file) {
let separator = props.isWindows ? '\\' : '/';
if (file.name === '..') {
- listFiles(getLastPath());
+ listFiles(getParentPath());
return;
}
if (file.type !== 0) {
@@ -110,7 +115,7 @@ function FileBrowser(props) {
tableRef.current.reload();
}
- function getLastPath() {
+ function getParentPath() {
let separator = props.isWindows ? '\\' : '/';
// remove the last separator
// or there'll be an empty element after split
@@ -125,6 +130,52 @@ function FileBrowser(props) {
return pathArr.join(separator) + separator;
}
+ function onFileChange(e) {
+ let file = e.target.files[0];
+ if (file === undefined) return;
+ e.target.value = null;
+ {
+ let exists = false;
+ for (let i = 0; i < fileList.length; i++) {
+ if (fileList[i].type === 0 && fileList[i].name === file.name) {
+ exists = true;
+ break;
+ }
+ }
+ if (exists) {
+ Modal.confirm({
+ autoFocusButton: 'cancel',
+ content: i18n.t('fileOverwriteConfirm').replace('{0}', file.name),
+ okText: i18n.t('fileOverwrite'),
+ onOk: () => {
+ setUpload(file);
+ },
+ okButtonProps: {
+ danger: true,
+ },
+ });
+ } else {
+ setUpload(file);
+ }
+ }
+ }
+ function uploadFile() {
+ if (path === '/' || path === '\\' || path.length === 0) {
+ if (props.isWindows) {
+ message.error(i18n.t('uploadInvalidPath'));
+ return;
+ }
+ }
+ document.getElementById('uploader').click();
+ }
+ function onUploadSuccess() {
+ tableRef.current.reload();
+ setUpload(false);
+ }
+ function onUploadCancel() {
+ setUpload(false);
+ }
+
function downloadFile(file) {
post(location.origin + location.pathname + 'api/device/file/get', {
file: path + file,
@@ -150,6 +201,7 @@ function FileBrowser(props) {
if (data.code === 0) {
let addParentShortcut = false;
data.data.files = data.data.files.sort((first, second) => (second.type - first.type));
+ fileList = [].concat(data.data.files);
if (path.length > 0 && path !== '/' && path !== '\\') {
addParentShortcut = true;
data.data.files.unshift({
@@ -165,7 +217,7 @@ function FileBrowser(props) {
total: data.data.files.length - (addParentShortcut ? 1 : 0)
});
}
- setPath(getLastPath());
+ setPath(getParentPath());
return ({data: [], success: false, total: 0});
}
@@ -188,7 +240,22 @@ function FileBrowser(props) {
onDoubleClick: onRowClick.bind(null, file),
})}
toolbar={{
- actions: []
+ settings: [
+ {
+ icon: ,
+ tooltip: i18n.t('upload'),
+ key: 'upload',
+ onClick: uploadFile
+ },
+ {
+ icon: ,
+ tooltip: i18n.t('reload'),
+ key: 'reload',
+ onClick: () => {
+ tableRef.current.reload();
+ }
+ }
+ ]
}}
scroll={{scrollToFirstRowOnChange: true, y: 300}}
search={false}
@@ -203,6 +270,173 @@ function FileBrowser(props) {
actionRef={tableRef}
>
+
+
+
+ )
+}
+
+let abortController = null;
+function UploadModal(props) {
+ const [visible, setVisible] = useState(!!props.file);
+ const [percent, setPercent] = useState(0);
+ const [status, setStatus] = useState(0);
+ // 0: ready, 1: uploading, 2: success, 3: fail, 4: cancel
+
+ useEffect(() => {
+ if (props.file) {
+ setVisible(true);
+ setPercent(0);
+ setStatus(0);
+ }
+ }, [props.file]);
+
+ function onPageUnload(e) {
+ e.preventDefault();
+ e.returnValue = '';
+ return '';
+ }
+
+ function onConfirm() {
+ if (status !== 0) {
+ onCancel();
+ return;
+ }
+ let params = Qs.stringify({
+ device: props.device,
+ path: props.path,
+ file: props.file.name
+ });
+ setStatus(1);
+ window.onbeforeunload = onPageUnload;
+ abortController = new AbortController();
+ axios.post(
+ '/api/device/file/upload?' + params,
+ props.file,
+ {
+ headers: {
+ 'Content-Type': 'application/octet-stream'
+ },
+ timeout: 0,
+ onUploadProgress: (progressEvent) => {
+ let percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+ setPercent(percentCompleted);
+ },
+ signal: abortController.signal
+ }
+ ).then(res => {
+ let data = res.data;
+ if (data.code === 0) {
+ setStatus(2);
+ message.success(i18n.t('uploadSuccess'));
+ } else {
+ setStatus(3);
+ }
+ }).catch((err) => {
+ if (axios.isCancel(err)) {
+ setStatus(4);
+ message.error(i18n.t('uploadAborted'));
+ } else {
+ setStatus(3);
+ message.error(i18n.t('uploadFailed') + i18n.t('colon') + err.message);
+ }
+ }).finally(() => {
+ abortController = null;
+ window.onbeforeunload = null;
+ setTimeout(() => {
+ setVisible(false);
+ if (status === 2) {
+ props.onSuccess();
+ } else {
+ props.onCanel();
+ }
+ }, 1500);
+ });
+ }
+ function onCancel() {
+ if (status === 0) {
+ setVisible(false);
+ setTimeout(props.onCanel, 300);
+ return;
+ }
+ if (status === 1) {
+ Modal.confirm({
+ autoFocusButton: 'cancel',
+ content: i18n.t('uploadCancelConfirm'),
+ onOk: () => {
+ abortController.abort();
+ },
+ okButtonProps: {
+ danger: true,
+ },
+ });
+ return;
+ }
+ setTimeout(() => {
+ setVisible(false);
+ setTimeout(props.onCanel, 300);
+ }, 1500);
+ }
+
+ function getDescription() {
+ switch (status) {
+ case 1:
+ return `${percent}%`;
+ case 2:
+ return i18n.t('uploadSuccess');
+ case 3:
+ return i18n.t('uploadFailed');
+ case 4:
+ return i18n.t('uploadAborted');
+ default:
+ return i18n.t('upload');
+ }
+
+ }
+
+ return (
+ 1}}
+ width={550}
+ >
+ <>
+
+ {getDescription()}
+
+ {props.file.name + ` (${formatSize(props.file.size)})`}
+ >
+
)
}
diff --git a/web/src/components/terminal.js b/web/src/components/terminal.js
index 7632892..7ee4ec2 100644
--- a/web/src/components/terminal.js
+++ b/web/src/components/terminal.js
@@ -8,7 +8,7 @@ import CryptoJS from 'crypto-js';
import wcwidth from 'wcwidth';
import "xterm/css/xterm.css";
import i18n from "../locale/locale";
-import {translate} from "../utils/utils";
+import {getBaseURL, translate} from "../utils/utils";
function hex2buf(hex) {
if (typeof hex !== 'string') {
@@ -59,13 +59,6 @@ function ab2str(buffer) {
return out;
}
-function getBaseURL() {
- if (location.protocol === 'https:') {
- return `wss://${location.host}${location.pathname}api/device/terminal`;
- }
- return `ws://${location.host}${location.pathname}api/device/terminal`;
-}
-
function genRandHex(length) {
return [...Array(length)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
}
@@ -120,7 +113,7 @@ class TerminalModal extends React.Component {
ev?.dispose();
let buffer = '';
let termEv = null;
- // Windows don't support pty, so we still use traditional way.
+ // Windows doesn't support pty, so we still use traditional way.
if (this.props.device.os === 'windows') {
let cmd = '';
termEv = this.term.onData((e) => {
@@ -173,7 +166,7 @@ class TerminalModal extends React.Component {
});
}
- this.ws = new WebSocket(`${getBaseURL()}?device=${this.props.device.id}&secret=${this.secret}`);
+ this.ws = new WebSocket(`${getBaseURL(true)}?device=${this.props.device.id}&secret=${this.secret}`);
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
this.conn = true;
@@ -192,7 +185,6 @@ class TerminalModal extends React.Component {
data = data.substring(buffer.length);
buffer = '';
}
- return;
}
this.term.write(data);
return;
@@ -263,6 +255,9 @@ class TerminalModal extends React.Component {
if (prevProps.visible) {
clearInterval(this.ticker);
if (this.conn) {
+ this.sendData({
+ act: 'killTerminal'
+ });
this.ws.close();
}
this?.termEv?.dispose();
diff --git a/web/src/index.js b/web/src/index.js
index 5eb2c0a..3cc6568 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -27,18 +27,20 @@ axios.interceptors.response.use(async (res) => {
}
return Promise.resolve(res);
}, (err) => {
+ console.error(err);
if (err.code === 'ECONNABORTED') {
- message.warn(i18n.t('requestTimeout'));
- return Promise.resolve(err);
+ message.error(i18n.t('requestTimeout'));
+ return Promise.reject(err);
}
let res = err.response;
- let data = res.data;
+ let data = res?.data ?? {};
if (data.hasOwnProperty('code')) {
if (data.code !== 0){
message.warn(translate(data.msg));
+ return Promise.resolve(res);
}
}
- return Promise.resolve(res);
+ return Promise.reject(err);
});
ReactDOM.render(
diff --git a/web/src/locale/en.json b/web/src/locale/en.json
index a58e7c5..7adc2a6 100644
--- a/web/src/locale/en.json
+++ b/web/src/locale/en.json
@@ -44,12 +44,22 @@
"modifyTime": "Modify Time",
"file": "file",
"folder": "folder",
+ "reload": "Reload",
+ "upload": "Upload",
"delete": "Delete",
"download": "Download",
+ "uploading": "Uploading...",
+ "uploadFailed": "Upload Failed",
+ "uploadAborted": "Upload Aborted",
+ "uploadSuccess": "Upload Success",
+ "uploadInvalidPath": "Cannot upload here",
+ "uploadCancelConfirm": "Are you sure to cancel uploading?",
"deleteConfirm": "Are you sure to delete this {0}?",
"deleteSuccess": "File or folder deleted",
"dateTimeFormat": "MMM D, YYYY h:mm A",
"fileOrDirNotExist": "File or folder does not exist",
+ "fileOverwriteConfirm": "File [ {0} ] already exists, overwrite?",
+ "fileOverwrite": "Overwrite",
"host": "Host",
"port": "Port",
diff --git a/web/src/locale/zh-CN.json b/web/src/locale/zh-CN.json
index b7c7513..bdbad00 100644
--- a/web/src/locale/zh-CN.json
+++ b/web/src/locale/zh-CN.json
@@ -45,12 +45,22 @@
"modifyTime": "修改时间",
"file": "文件",
"folder": "文件夹",
+ "reload": "刷新",
+ "upload": "上传",
"delete": "删除",
"download": "下载",
+ "uploading": "上传中...",
+ "uploadFailed": "上传失败",
+ "uploadAborted": "取消上传",
+ "uploadSuccess": "上传完成",
+ "uploadInvalidPath": "该路径无法上传文件",
+ "uploadCancelConfirm": "确定要取消上传吗?",
"deleteConfirm": "确定要删除该{0}吗?",
"deleteSuccess": "文件或目录已被删除",
"dateTimeFormat": "YYYY/MM/DD HH:mm",
"fileOrDirNotExist": "文件或目录不存在",
+ "fileOverwriteConfirm": "文件 [ {0} ] 已经存在,是否覆盖?",
+ "fileOverwrite": "覆盖",
"registryEditor": "注册表编辑器",
"unknownRegistryKey": "注册表键有误",
diff --git a/web/src/utils/utils.js b/web/src/utils/utils.js
index e1ae24f..a74aac3 100644
--- a/web/src/utils/utils.js
+++ b/web/src/utils/utils.js
@@ -24,11 +24,13 @@ function waitTime(time) {
};
function formatSize(size) {
- if (size === 0) return '0 B';
+ size = isNaN(size) ? 0 : (size??0);
+ size = Math.max(size, 0);
let k = 1024,
- i = Math.floor(Math.log(size) / Math.log(k)),
- units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
- return (size / Math.pow(k, i)).toFixed(2) + ' ' + units[i];
+ i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(k)),
+ units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
+ result = size / Math.pow(k, i);
+ return (Math.round(result * 100) / 100) + ' ' + units[i];
}
function tsToTime(ts) {
@@ -39,6 +41,15 @@ function tsToTime(ts) {
return `${String(hours) + i18n.t('hours') + ' ' + String(minutes) + i18n.t('minutes')}`;
}
+function getBaseURL(ws) {
+ if (location.protocol === 'https:') {
+ let scheme = ws ? 'wss' : 'https';
+ return scheme + `://${location.host}${location.pathname}api/device/terminal`;
+ }
+ let scheme = ws ? 'ws' : 'http';
+ return scheme + `://${location.host}${location.pathname}api/device/terminal`;
+}
+
function post(url, data, ext) {
let form = document.createElement('form');
form.action = url;
@@ -65,4 +76,4 @@ function translate(text) {
});
}
-export {post, request, waitTime, formatSize, tsToTime, translate};
\ No newline at end of file
+export {post, request, waitTime, formatSize, tsToTime, getBaseURL, translate};
\ No newline at end of file