feat(backend): file transfer

This commit is contained in:
pycook
2025-06-08 11:21:29 +08:00
parent cfe1a98c7d
commit 070fccb5db
17 changed files with 4205 additions and 996 deletions

View File

@@ -9,9 +9,10 @@ import (
// Connect handles WebSocket connections for terminal sessions
// @Tags connect
// @Success 200 {object} HttpResponse
// @Param w query int false "width"
// @Param h query int false "height"
// @Param dpi query int false "dpi"
// @Param w query int false "width"
// @Param h query int false "height"
// @Param dpi query int false "dpi"
// @Param session_id query string false "session_id"
// @Success 200 {object} HttpResponse{}
// @Router /connect/:asset_id/:account_id/:protocol [get]
func (c *Controller) Connect(ctx *gin.Context) {

File diff suppressed because it is too large Load Diff

View File

@@ -188,160 +188,6 @@ const docTemplate = `{
}
}
},
"/api/v1/rdp/sessions/{session_id}/files": {
"get": {
"description": "Get file list for RDP session drive",
"tags": [
"RDP File"
],
"summary": "List RDP session files",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Directory path",
"name": "path",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/api/v1/rdp/sessions/{session_id}/files/download": {
"get": {
"description": "Download file from RDP session drive",
"consumes": [
"application/json"
],
"produces": [
"application/octet-stream"
],
"tags": [
"RDP File"
],
"summary": "Download file from RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "File path",
"name": "path",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
}
}
}
},
"/api/v1/rdp/sessions/{session_id}/files/mkdir": {
"post": {
"description": "Create directory in RDP session drive",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"RDP File"
],
"summary": "Create directory in RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"description": "Directory creation request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/controller.RDPMkdirRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/api/v1/rdp/sessions/{session_id}/files/upload": {
"post": {
"description": "Upload file to RDP session drive",
"consumes": [
"multipart/form-data"
],
"tags": [
"RDP File"
],
"summary": "Upload file to RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "file",
"description": "File to upload",
"name": "file",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Target directory path",
"name": "path",
"in": "formData"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/asset": {
"get": {
"tags": [
@@ -906,6 +752,12 @@ const docTemplate = `{
"description": "dpi",
"name": "dpi",
"in": "query"
},
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "query"
}
],
"responses": {
@@ -1181,8 +1033,173 @@ const docTemplate = `{
}
}
},
"/file/session/:session_id/download": {
"get": {
"tags": [
"file"
],
"parameters": [
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "dir",
"name": "dir",
"in": "query",
"required": true
},
{
"type": "string",
"description": "names (comma-separated for multiple files)",
"name": "names",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/file/session/:session_id/ls": {
"get": {
"tags": [
"file"
],
"parameters": [
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "dir",
"name": "dir",
"in": "query",
"required": true
},
{
"type": "boolean",
"description": "show hidden files (default: false)",
"name": "show_hidden",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/file/session/:session_id/mkdir": {
"post": {
"tags": [
"file"
],
"parameters": [
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "dir",
"name": "dir",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/file/session/:session_id/upload": {
"post": {
"description": "Uploads file via server temp storage then transfers to target using optimized SFTP with performance enhancements. HTTP response only after file reaches target machine.",
"consumes": [
"multipart/form-data"
],
"tags": [
"file"
],
"summary": "High-performance file upload using optimized SFTP",
"parameters": [
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "target directory path (default: /tmp)",
"name": "dir",
"in": "query"
},
{
"type": "string",
"description": "Custom transfer ID for progress tracking (frontend generated)",
"name": "transfer_id",
"in": "query"
},
{
"type": "file",
"description": "file to upload",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/file/transfer/progress/id/:transfer_id": {
"get": {
"tags": [
"file"
],
"responses": {}
}
},
"/file/upload/:asset_id/:account_id": {
"post": {
"consumes": [
"multipart/form-data"
],
"tags": [
"file"
],
@@ -1203,9 +1220,21 @@ const docTemplate = `{
},
{
"type": "string",
"description": "path",
"name": "path",
"in": "query",
"description": "target directory path (default: /tmp)",
"name": "dir",
"in": "query"
},
{
"type": "string",
"description": "Custom transfer ID for progress tracking (frontend generated)",
"name": "transfer_id",
"in": "query"
},
{
"type": "file",
"description": "file to upload",
"name": "file",
"in": "formData",
"required": true
}
],
@@ -2090,6 +2119,217 @@ const docTemplate = `{
}
}
},
"/rdp/sessions/{session_id}/files": {
"get": {
"description": "Get file list for RDP session drive",
"tags": [
"RDP File"
],
"summary": "List RDP session files",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Directory path",
"name": "path",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/rdp/sessions/{session_id}/files/download": {
"get": {
"description": "Download files from RDP session drive (supports multiple files via names parameter)",
"consumes": [
"application/json"
],
"produces": [
"application/octet-stream"
],
"tags": [
"RDP File"
],
"summary": "Download files from RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Directory path",
"name": "dir",
"in": "query",
"required": true
},
{
"type": "string",
"description": "File names (comma-separated for multiple files)",
"name": "names",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
}
}
}
},
"/rdp/sessions/{session_id}/files/mkdir": {
"post": {
"description": "Create directory in RDP session drive",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"RDP File"
],
"summary": "Create directory in RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"description": "Directory creation request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/service.RDPMkdirRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/rdp/sessions/{session_id}/files/prepare": {
"post": {
"description": "Create transfer record before RDP upload starts for progress tracking",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"RDP File"
],
"summary": "Create transfer record for RDP upload",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Custom transfer ID",
"name": "transfer_id",
"in": "query"
},
{
"type": "string",
"description": "Filename",
"name": "filename",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/rdp/sessions/{session_id}/files/upload": {
"post": {
"description": "Upload file to RDP session drive",
"consumes": [
"multipart/form-data"
],
"tags": [
"RDP File"
],
"summary": "Upload file to RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Custom transfer ID for progress tracking (frontend generated)",
"name": "transfer_id",
"in": "query"
},
{
"type": "string",
"description": "Target directory path",
"name": "path",
"in": "query"
},
{
"type": "file",
"description": "File to upload",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/session": {
"get": {
"tags": [
@@ -2883,17 +3123,6 @@ const docTemplate = `{
}
}
},
"controller.RDPMkdirRequest": {
"type": "object",
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
}
}
},
"model.AccessAuth": {
"type": "object",
"properties": {
@@ -3737,6 +3966,17 @@ const docTemplate = `{
"type": "boolean"
}
}
},
"service.RDPMkdirRequest": {
"type": "object",
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
}
}
}
}
}`

View File

@@ -177,160 +177,6 @@
}
}
},
"/api/v1/rdp/sessions/{session_id}/files": {
"get": {
"description": "Get file list for RDP session drive",
"tags": [
"RDP File"
],
"summary": "List RDP session files",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Directory path",
"name": "path",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/api/v1/rdp/sessions/{session_id}/files/download": {
"get": {
"description": "Download file from RDP session drive",
"consumes": [
"application/json"
],
"produces": [
"application/octet-stream"
],
"tags": [
"RDP File"
],
"summary": "Download file from RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "File path",
"name": "path",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
}
}
}
},
"/api/v1/rdp/sessions/{session_id}/files/mkdir": {
"post": {
"description": "Create directory in RDP session drive",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"RDP File"
],
"summary": "Create directory in RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"description": "Directory creation request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/controller.RDPMkdirRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/api/v1/rdp/sessions/{session_id}/files/upload": {
"post": {
"description": "Upload file to RDP session drive",
"consumes": [
"multipart/form-data"
],
"tags": [
"RDP File"
],
"summary": "Upload file to RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "file",
"description": "File to upload",
"name": "file",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Target directory path",
"name": "path",
"in": "formData"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/asset": {
"get": {
"tags": [
@@ -895,6 +741,12 @@
"description": "dpi",
"name": "dpi",
"in": "query"
},
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "query"
}
],
"responses": {
@@ -1170,8 +1022,173 @@
}
}
},
"/file/session/:session_id/download": {
"get": {
"tags": [
"file"
],
"parameters": [
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "dir",
"name": "dir",
"in": "query",
"required": true
},
{
"type": "string",
"description": "names (comma-separated for multiple files)",
"name": "names",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/file/session/:session_id/ls": {
"get": {
"tags": [
"file"
],
"parameters": [
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "dir",
"name": "dir",
"in": "query",
"required": true
},
{
"type": "boolean",
"description": "show hidden files (default: false)",
"name": "show_hidden",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/file/session/:session_id/mkdir": {
"post": {
"tags": [
"file"
],
"parameters": [
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "dir",
"name": "dir",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/file/session/:session_id/upload": {
"post": {
"description": "Uploads file via server temp storage then transfers to target using optimized SFTP with performance enhancements. HTTP response only after file reaches target machine.",
"consumes": [
"multipart/form-data"
],
"tags": [
"file"
],
"summary": "High-performance file upload using optimized SFTP",
"parameters": [
{
"type": "string",
"description": "session_id",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "target directory path (default: /tmp)",
"name": "dir",
"in": "query"
},
{
"type": "string",
"description": "Custom transfer ID for progress tracking (frontend generated)",
"name": "transfer_id",
"in": "query"
},
{
"type": "file",
"description": "file to upload",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/file/transfer/progress/id/:transfer_id": {
"get": {
"tags": [
"file"
],
"responses": {}
}
},
"/file/upload/:asset_id/:account_id": {
"post": {
"consumes": [
"multipart/form-data"
],
"tags": [
"file"
],
@@ -1192,9 +1209,21 @@
},
{
"type": "string",
"description": "path",
"name": "path",
"in": "query",
"description": "target directory path (default: /tmp)",
"name": "dir",
"in": "query"
},
{
"type": "string",
"description": "Custom transfer ID for progress tracking (frontend generated)",
"name": "transfer_id",
"in": "query"
},
{
"type": "file",
"description": "file to upload",
"name": "file",
"in": "formData",
"required": true
}
],
@@ -2079,6 +2108,217 @@
}
}
},
"/rdp/sessions/{session_id}/files": {
"get": {
"description": "Get file list for RDP session drive",
"tags": [
"RDP File"
],
"summary": "List RDP session files",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Directory path",
"name": "path",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/rdp/sessions/{session_id}/files/download": {
"get": {
"description": "Download files from RDP session drive (supports multiple files via names parameter)",
"consumes": [
"application/json"
],
"produces": [
"application/octet-stream"
],
"tags": [
"RDP File"
],
"summary": "Download files from RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Directory path",
"name": "dir",
"in": "query",
"required": true
},
{
"type": "string",
"description": "File names (comma-separated for multiple files)",
"name": "names",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
}
}
}
},
"/rdp/sessions/{session_id}/files/mkdir": {
"post": {
"description": "Create directory in RDP session drive",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"RDP File"
],
"summary": "Create directory in RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"description": "Directory creation request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/service.RDPMkdirRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/rdp/sessions/{session_id}/files/prepare": {
"post": {
"description": "Create transfer record before RDP upload starts for progress tracking",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"RDP File"
],
"summary": "Create transfer record for RDP upload",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Custom transfer ID",
"name": "transfer_id",
"in": "query"
},
{
"type": "string",
"description": "Filename",
"name": "filename",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/rdp/sessions/{session_id}/files/upload": {
"post": {
"description": "Upload file to RDP session drive",
"consumes": [
"multipart/form-data"
],
"tags": [
"RDP File"
],
"summary": "Upload file to RDP session",
"parameters": [
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Custom transfer ID for progress tracking (frontend generated)",
"name": "transfer_id",
"in": "query"
},
{
"type": "string",
"description": "Target directory path",
"name": "path",
"in": "query"
},
{
"type": "file",
"description": "File to upload",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controller.HttpResponse"
}
}
}
}
},
"/session": {
"get": {
"tags": [
@@ -2872,17 +3112,6 @@
}
}
},
"controller.RDPMkdirRequest": {
"type": "object",
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
}
}
},
"model.AccessAuth": {
"type": "object",
"properties": {
@@ -3726,6 +3955,17 @@
"type": "boolean"
}
}
},
"service.RDPMkdirRequest": {
"type": "object",
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
}
}
}
}
}

View File

@@ -15,13 +15,6 @@ definitions:
items: {}
type: array
type: object
controller.RDPMkdirRequest:
properties:
path:
type: string
required:
- path
type: object
model.AccessAuth:
properties:
allow:
@@ -580,6 +573,13 @@ definitions:
paste:
type: boolean
type: object
service.RDPMkdirRequest:
properties:
path:
type: string
required:
- path
type: object
info:
contact: {}
paths:
@@ -689,108 +689,6 @@ paths:
$ref: '#/definitions/controller.HttpResponse'
tags:
- account
/api/v1/rdp/sessions/{session_id}/files:
get:
description: Get file list for RDP session drive
parameters:
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: Directory path
in: query
name: path
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
summary: List RDP session files
tags:
- RDP File
/api/v1/rdp/sessions/{session_id}/files/download:
get:
consumes:
- application/json
description: Download file from RDP session drive
parameters:
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: File path
in: query
name: path
required: true
type: string
produces:
- application/octet-stream
responses:
"200":
description: OK
schema:
type: file
summary: Download file from RDP session
tags:
- RDP File
/api/v1/rdp/sessions/{session_id}/files/mkdir:
post:
consumes:
- application/json
description: Create directory in RDP session drive
parameters:
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: Directory creation request
in: body
name: request
required: true
schema:
$ref: '#/definitions/controller.RDPMkdirRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
summary: Create directory in RDP session
tags:
- RDP File
/api/v1/rdp/sessions/{session_id}/files/upload:
post:
consumes:
- multipart/form-data
description: Upload file to RDP session drive
parameters:
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: File to upload
in: formData
name: file
required: true
type: file
- description: Target directory path
in: formData
name: path
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
summary: Upload file to RDP session
tags:
- RDP File
/asset:
get:
parameters:
@@ -1134,6 +1032,10 @@ paths:
in: query
name: dpi
type: integer
- description: session_id
in: query
name: session_id
type: string
responses:
"200":
description: OK
@@ -1307,8 +1209,118 @@ paths:
$ref: '#/definitions/controller.HttpResponse'
tags:
- file
/file/session/:session_id/download:
get:
parameters:
- description: session_id
in: path
name: session_id
required: true
type: string
- description: dir
in: query
name: dir
required: true
type: string
- description: names (comma-separated for multiple files)
in: query
name: names
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
tags:
- file
/file/session/:session_id/ls:
get:
parameters:
- description: session_id
in: path
name: session_id
required: true
type: string
- description: dir
in: query
name: dir
required: true
type: string
- description: 'show hidden files (default: false)'
in: query
name: show_hidden
type: boolean
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
tags:
- file
/file/session/:session_id/mkdir:
post:
parameters:
- description: session_id
in: path
name: session_id
required: true
type: string
- description: dir
in: query
name: dir
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
tags:
- file
/file/session/:session_id/upload:
post:
consumes:
- multipart/form-data
description: Uploads file via server temp storage then transfers to target using
optimized SFTP with performance enhancements. HTTP response only after file
reaches target machine.
parameters:
- description: session_id
in: path
name: session_id
required: true
type: string
- description: 'target directory path (default: /tmp)'
in: query
name: dir
type: string
- description: Custom transfer ID for progress tracking (frontend generated)
in: query
name: transfer_id
type: string
- description: file to upload
in: formData
name: file
required: true
type: file
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
summary: High-performance file upload using optimized SFTP
tags:
- file
/file/transfer/progress/id/:transfer_id:
get:
responses: {}
tags:
- file
/file/upload/:asset_id/:account_id:
post:
consumes:
- multipart/form-data
parameters:
- description: asset_id
in: path
@@ -1320,11 +1332,19 @@ paths:
name: account_id
required: true
type: integer
- description: path
- description: 'target directory path (default: /tmp)'
in: query
name: path
required: true
name: dir
type: string
- description: Custom transfer ID for progress tracking (frontend generated)
in: query
name: transfer_id
type: string
- description: file to upload
in: formData
name: file
required: true
type: file
responses:
"200":
description: OK
@@ -1861,6 +1881,147 @@ paths:
$ref: '#/definitions/controller.HttpResponse'
tags:
- QuickCommand
/rdp/sessions/{session_id}/files:
get:
description: Get file list for RDP session drive
parameters:
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: Directory path
in: query
name: path
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
summary: List RDP session files
tags:
- RDP File
/rdp/sessions/{session_id}/files/download:
get:
consumes:
- application/json
description: Download files from RDP session drive (supports multiple files
via names parameter)
parameters:
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: Directory path
in: query
name: dir
required: true
type: string
- description: File names (comma-separated for multiple files)
in: query
name: names
required: true
type: string
produces:
- application/octet-stream
responses:
"200":
description: OK
schema:
type: file
summary: Download files from RDP session
tags:
- RDP File
/rdp/sessions/{session_id}/files/mkdir:
post:
consumes:
- application/json
description: Create directory in RDP session drive
parameters:
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: Directory creation request
in: body
name: request
required: true
schema:
$ref: '#/definitions/service.RDPMkdirRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
summary: Create directory in RDP session
tags:
- RDP File
/rdp/sessions/{session_id}/files/prepare:
post:
consumes:
- application/json
description: Create transfer record before RDP upload starts for progress tracking
parameters:
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: Custom transfer ID
in: query
name: transfer_id
type: string
- description: Filename
in: query
name: filename
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
summary: Create transfer record for RDP upload
tags:
- RDP File
/rdp/sessions/{session_id}/files/upload:
post:
consumes:
- multipart/form-data
description: Upload file to RDP session drive
parameters:
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: Custom transfer ID for progress tracking (frontend generated)
in: query
name: transfer_id
type: string
- description: Target directory path
in: query
name: path
type: string
- description: File to upload
in: formData
name: file
required: true
type: file
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controller.HttpResponse'
summary: Upload file to RDP session
tags:
- RDP File
/session:
get:
parameters:

View File

@@ -2,6 +2,7 @@ package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
@@ -24,7 +25,11 @@ func AuthMiddleware() gin.HandlerFunc {
)
m := make(map[string]any)
ctx.ShouldBindBodyWithJSON(&m)
contentType := ctx.GetHeader("Content-Type")
if !strings.Contains(contentType, "multipart/form-data") {
ctx.ShouldBindBodyWithJSON(&m)
}
if ctx.Request.Method == "GET" {
if _, ok := ctx.GetQuery("_key"); ok {
m["_key"] = ctx.Query("_key")

View File

@@ -26,7 +26,7 @@ func Error2RespMiddleware() gin.HandlerFunc {
// Skip middleware for session replay and file download endpoints
urlPath := ctx.Request.URL.String()
if strings.Contains(urlPath, "session/replay") ||
strings.Contains(urlPath, "/file/download/") {
strings.Contains(urlPath, "/download") {
ctx.Next()
return
}

View File

@@ -12,7 +12,7 @@ import (
func SetupRouter(r *gin.Engine) {
r.SetTrustedProxies([]string{"0.0.0.0/0", "::/0"})
r.MaxMultipartMemory = 128 << 20
r.MaxMultipartMemory = 32 << 20 // 32MB, match with controller constant
r.Use(gin.Recovery(), middleware.LoggerMiddleware())
docs.SwaggerInfo.Title = "ONETERM API"
@@ -101,10 +101,23 @@ func SetupRouter(r *gin.Engine) {
file := v1.Group("file")
{
file.GET("/history", c.GetFileHistory)
// Legacy asset-based file operations (for backward compatibility)
file.GET("/ls/:asset_id/:account_id", c.FileLS)
file.POST("/mkdir/:asset_id/:account_id", c.FileMkdir)
file.POST("/upload/:asset_id/:account_id", c.FileUpload)
file.GET("/download/:asset_id/:account_id", c.FileDownload)
sftpFile := file.Group("/session/:session_id")
{
sftpFile.GET("/ls", c.SftpFileLS)
sftpFile.POST("/mkdir", c.SftpFileMkdir)
sftpFile.POST("/upload", c.SftpFileUpload)
sftpFile.GET("/download", c.SftpFileDownload)
}
// File transfer progress tracking
file.GET("/transfer/progress/id/:transfer_id", c.TransferProgressById)
}
config := v1.Group("config")

View File

@@ -168,11 +168,16 @@ func DoConnect(ctx *gin.Context, ws *websocket.Conn) (sess *gsession.Session, er
return
}
sessionId := ctx.Query("session_id")
if sessionId == "" {
sessionId = uuid.New().String()
}
sess = gsession.NewSession(ctx)
sess.Ws = ws
sess.Session = &model.Session{
SessionType: ctx.GetInt("sessionType"),
SessionId: uuid.New().String(),
SessionId: sessionId,
Uid: currentUser.GetUid(),
UserName: currentUser.GetUserName(),
AssetId: assetId,
@@ -256,6 +261,30 @@ func DoConnect(ctx *gin.Context, ws *websocket.Conn) (sess *gsession.Session, er
gsession.GetOnlineSession().Store(sess.SessionId, sess)
gsession.UpsertSession(sess)
// Initialize session-based file client for high-performance file operations
// Only for SSH-based protocols that support SFTP
protocol := strings.Split(sess.Protocol, ":")[0]
if protocol == "ssh" {
if err := service.DefaultFileService.InitSessionFileClient(sess.SessionId, sess.AssetId, sess.AccountId); err != nil {
logger.L().Warn("Failed to initialize session file client",
zap.String("sessionId", sess.SessionId),
zap.Int("assetId", sess.AssetId),
zap.Int("accountId", sess.AccountId),
zap.Error(err))
// Don't fail the session creation for file service initialization failure
} else {
logger.L().Info("Session file client initialized successfully",
zap.String("sessionId", sess.SessionId),
zap.Int("assetId", sess.AssetId),
zap.Int("accountId", sess.AccountId))
}
} else if protocol == "rdp" || protocol == "vnc" {
logger.L().Debug("Skipping session file client initialization for Guacamole protocol",
zap.String("protocol", protocol),
zap.String("sessionId", sess.SessionId))
// RDP and VNC use Guacamole protocol for file transfer, not SSH/SFTP
}
return
}
@@ -263,12 +292,20 @@ func DoConnect(ctx *gin.Context, ws *websocket.Conn) (sess *gsession.Session, er
func HandleTerm(sess *gsession.Session, ctx *gin.Context) (err error) {
defer func() {
logger.L().Debug("defer HandleTerm", zap.String("sessionId", sess.SessionId))
// Clean up session-based file client (only for SSH-based protocols)
protocol := strings.Split(sess.Protocol, ":")[0]
if protocol == "ssh" {
service.DefaultFileService.CloseSessionFileClient(sess.SessionId)
// Clear SSH client from session to ensure proper cleanup
sess.ClearSSHClient()
}
sess.SshParser.Close(sess.Prompt)
sess.Status = model.SESSIONSTATUS_OFFLINE
sess.ClosedAt = lo.ToPtr(time.Now())
if err = gsession.UpsertSession(sess); err != nil {
logger.L().Error("offline session failed", zap.String("sessionId", sess.SessionId), zap.Error(err))
return
logger.L().Error("upsert session failed", zap.Error(err))
}
}()
chs := sess.Chans

View File

@@ -49,6 +49,10 @@ func ConnectSsh(ctx *gin.Context, sess *gsession.Session, asset *model.Asset, ac
return
}
// CRITICAL: Store SSH client in session for file transfer reuse
sess.SetSSHClient(sshCli)
logger.L().Info("SSH client stored in session for reuse", zap.String("sessionId", sess.SessionId))
sshSess, err := sshCli.NewSession()
if err != nil {
logger.L().Error("ssh session create failed", zap.Error(err))

View File

@@ -16,6 +16,7 @@ import (
myi18n "github.com/veops/oneterm/internal/i18n"
"github.com/veops/oneterm/internal/model"
"github.com/veops/oneterm/internal/service"
gsession "github.com/veops/oneterm/internal/session"
myErrors "github.com/veops/oneterm/pkg/errors"
"github.com/veops/oneterm/pkg/logger"
@@ -224,6 +225,10 @@ func IsActive(message []byte) bool {
func OfflineSession(ctx *gin.Context, sessionId string, closer string) {
logger.L().Debug("offline", zap.String("session_id", sessionId), zap.String("closer", closer))
defer gsession.GetOnlineSession().Delete(sessionId)
// Clean up session-based file client
service.DefaultFileService.CloseSessionFileClient(sessionId)
session := gsession.GetOnlineSessionById(sessionId)
if session == nil {
return

View File

@@ -5,13 +5,11 @@ import (
"fmt"
"io"
"net"
"os"
"strings"
"time"
"github.com/samber/lo"
"github.com/spf13/cast"
"go.uber.org/zap"
"github.com/veops/oneterm/internal/model"
"github.com/veops/oneterm/internal/tunneling"
@@ -33,6 +31,7 @@ const (
DRIVE_CREATE_PATH = "create-drive-path"
DRIVE_DISABLE_UPLOAD = "disable-upload"
DRIVE_DISABLE_DOWNLOAD = "disable-download"
DRIVE_NAME = "drive-name"
)
type Configuration struct {
@@ -87,6 +86,7 @@ func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, a
func() map[string]string {
return map[string]string{
"version": VERSION,
"client-name": "OneTerm",
"recording-path": RECORDING_PATH,
"create-recording-path": CREATE_RECORDING,
"ignore-cert": IGNORE_CERT,
@@ -100,12 +100,19 @@ func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, a
"password": account.Password,
"disable-copy": cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), !cfg.RdpConfig.Copy, !cfg.VncConfig.Copy)),
"disable-paste": cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), !cfg.RdpConfig.Paste, !cfg.VncConfig.Paste)),
"resize-method": "display-update",
// Set file transfer related parameters from config
DRIVE_ENABLE: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.EnableDrive, false)),
DRIVE_PATH: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.DrivePath, "")),
DRIVE_CREATE_PATH: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.CreateDrivePath, false)),
DRIVE_DISABLE_UPLOAD: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.DisableUpload, false)),
DRIVE_DISABLE_DOWNLOAD: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.DisableDownload, false)),
// DRIVE_ENABLE: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.EnableDrive, false)),
// DRIVE_PATH: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.DrivePath, "")),
// DRIVE_CREATE_PATH: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.CreateDrivePath, false)),
// DRIVE_DISABLE_UPLOAD: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.DisableUpload, false)),
// DRIVE_DISABLE_DOWNLOAD: cast.ToString(lo.Ternary(strings.Contains(protocol, "rdp"), cfg.RdpConfig.DisableDownload, false)),
DRIVE_ENABLE: "true",
DRIVE_PATH: fmt.Sprintf("/rdp/asset_%d", asset.Id),
DRIVE_CREATE_PATH: "true",
DRIVE_DISABLE_UPLOAD: "false",
DRIVE_DISABLE_DOWNLOAD: "false",
DRIVE_NAME: "Drive",
}
}, func() map[string]string {
return map[string]string{
@@ -130,21 +137,6 @@ func NewTunnel(connectionId, sessionId string, w, h, dpi int, protocol string, a
t.Config.Parameters["port"] = cast.ToString(t.gw.LocalPort)
}
// If RDP protocol and file transfer is enabled
if strings.Contains(protocol, "rdp") && t.Config.Parameters[DRIVE_ENABLE] == "true" {
// Get drive path
t.drivePath = t.Config.Parameters[DRIVE_PATH]
// Create drive path if needed
if t.Config.Parameters[DRIVE_CREATE_PATH] == "true" && t.drivePath != "" {
if err := os.MkdirAll(t.drivePath, 0755); err != nil {
logger.L().Error("Failed to create RDP drive path", zap.Error(err))
// Don't terminate the connection, just disable file transfer
t.drivePath = ""
}
}
}
err = t.handshake()
return
@@ -284,7 +276,7 @@ func (t *Tunnel) HandleFileUpload(filename string, size int64) (string, error) {
return "", fmt.Errorf("file upload is disabled")
}
transfer, err := t.transferManager.CreateUpload(filename, t.drivePath)
transfer, err := t.transferManager.CreateUpload(t.SessionId, filename, t.drivePath)
if err != nil {
return "", err
}
@@ -298,7 +290,7 @@ func (t *Tunnel) HandleFileDownload(filename string) (string, int64, error) {
return "", 0, fmt.Errorf("file download is disabled")
}
transfer, err := t.transferManager.CreateDownload(filename, t.drivePath)
transfer, err := t.transferManager.CreateDownload(t.SessionId, filename, t.drivePath)
if err != nil {
return "", 0, err
}

View File

@@ -15,14 +15,25 @@ const (
INSTRUCTION_FILE_ERROR = "file-error"
)
// RDP file transfer related parameters
// Object instruction constants for filesystem operations
const (
RDP_ENABLE_DRIVE = "enable-drive"
RDP_DRIVE_PATH = "drive-path"
RDP_DRIVE_NAME = "drive-name"
RDP_DISABLE_DOWNLOAD = "disable-download"
RDP_DISABLE_UPLOAD = "disable-upload"
RDP_CREATE_DRIVE_PATH = "create-drive-path"
INSTRUCTION_FILESYSTEM = "filesystem"
INSTRUCTION_GET = "get"
INSTRUCTION_PUT = "put"
INSTRUCTION_BODY = "body"
INSTRUCTION_UNDEFINE = "undefine"
)
// Stream instruction constants
const (
INSTRUCTION_BLOB = "blob"
INSTRUCTION_END = "end"
)
// Filesystem mimetypes
const (
MIMETYPE_STREAM_INDEX = "application/vnd.glyptodon.guacamole.stream-index+json"
MIMETYPE_TEXT_PLAIN = "text/plain"
)
// HandleFileInstruction processes file transfer related instructions

View File

@@ -22,18 +22,39 @@ type FileTransferManager struct {
// FileTransfer represents a single file transfer
type FileTransfer struct {
ID string
Filename string
Path string
Size int64
Offset int64
Created time.Time
Completed bool
IsUpload bool
ID string `json:"id"`
SessionID string `json:"session_id"`
Filename string `json:"filename"`
Path string `json:"path"`
Size int64 `json:"size"`
Offset int64 `json:"offset"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Completed bool `json:"completed"`
IsUpload bool `json:"is_upload"`
Status string `json:"status"` // "pending", "uploading", "completed", "failed"
Error string `json:"error,omitempty"`
file *os.File
mutex sync.Mutex
}
// FileTransferProgress represents transfer progress information
type FileTransferProgress struct {
ID string `json:"id"`
SessionID string `json:"session_id"`
Filename string `json:"filename"`
Size int64 `json:"size"`
Offset int64 `json:"offset"`
Percentage float64 `json:"percentage"`
Status string `json:"status"`
IsUpload bool `json:"is_upload"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Error string `json:"error,omitempty"`
Speed int64 `json:"speed"` // bytes per second
ETA int64 `json:"eta"` // estimated time to completion in seconds
}
// Global file transfer manager instance
var (
DefaultFileTransferManager = NewFileTransferManager()
@@ -47,72 +68,89 @@ func NewFileTransferManager() *FileTransferManager {
}
// CreateUpload creates an upload file transfer
func (m *FileTransferManager) CreateUpload(filename, drivePath string) (*FileTransfer, error) {
func (m *FileTransferManager) CreateUpload(sessionID, filename, drivePath string) (*FileTransfer, error) {
return m.CreateUploadWithID("", sessionID, filename, drivePath)
}
// CreateUploadWithID creates an upload file transfer with custom ID
func (m *FileTransferManager) CreateUploadWithID(transferID, sessionID, filename, drivePath string) (*FileTransfer, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
id := uuid.New().String()
var id string
if transferID != "" {
if _, exists := m.transfers[transferID]; exists {
return nil, fmt.Errorf("transfer ID already exists: %s", transferID)
}
id = transferID
} else {
id = uuid.New().String()
}
fullPath := filepath.Join(drivePath, filename)
// Ensure directory exists
dir := filepath.Dir(fullPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
// Create file
file, err := os.Create(fullPath)
if err != nil {
return nil, fmt.Errorf("failed to create file: %w", err)
}
now := time.Now()
transfer := &FileTransfer{
ID: id,
Filename: filename,
Path: fullPath,
Created: time.Now(),
IsUpload: true,
file: file,
ID: id,
SessionID: sessionID,
Filename: filename,
Path: fullPath,
Created: now,
Updated: now,
IsUpload: true,
Status: "pending",
file: file,
}
m.transfers[id] = transfer
logger.L().Debug("Created file upload", zap.String("id", id), zap.String("filename", filename))
logger.L().Debug("Created file upload", zap.String("id", id), zap.String("sessionId", sessionID), zap.String("filename", filename))
return transfer, nil
}
// CreateDownload creates a download file transfer
func (m *FileTransferManager) CreateDownload(filename, drivePath string) (*FileTransfer, error) {
// CreateDownload creates a download file transfer (no progress tracking)
func (m *FileTransferManager) CreateDownload(sessionID, filename, drivePath string) (*FileTransfer, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
id := uuid.New().String()
fullPath := filepath.Join(drivePath, filename)
// Open file
file, err := os.Open(fullPath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
// Get file info
stat, err := file.Stat()
if err != nil {
file.Close()
return nil, fmt.Errorf("failed to get file info: %w", err)
}
now := time.Now()
transfer := &FileTransfer{
ID: id,
Filename: filename,
Path: fullPath,
Size: stat.Size(),
Created: time.Now(),
IsUpload: false,
file: file,
ID: id,
SessionID: sessionID,
Filename: filename,
Path: fullPath,
Size: stat.Size(),
Created: now,
Updated: now,
IsUpload: false,
Status: "completed",
file: file,
}
m.transfers[id] = transfer
logger.L().Debug("Created file download", zap.String("id", id), zap.String("filename", filename))
logger.L().Debug("Created file download", zap.String("id", id), zap.String("sessionId", sessionID), zap.String("filename", filename))
return transfer, nil
}
@@ -123,6 +161,54 @@ func (m *FileTransferManager) GetTransfer(id string) *FileTransfer {
return m.transfers[id]
}
// GetTransfersBySession gets all transfers for a session
func (m *FileTransferManager) GetTransfersBySession(sessionID string) []*FileTransfer {
m.mutex.Lock()
defer m.mutex.Unlock()
var transfers []*FileTransfer
for _, transfer := range m.transfers {
if transfer.SessionID == sessionID {
transfers = append(transfers, transfer)
}
}
return transfers
}
// GetAllTransfers gets all active transfers
func (m *FileTransferManager) GetAllTransfers() []*FileTransfer {
m.mutex.Lock()
defer m.mutex.Unlock()
var transfers []*FileTransfer
for _, transfer := range m.transfers {
transfers = append(transfers, transfer)
}
return transfers
}
// GetTransferProgress gets progress information for a transfer
func (m *FileTransferManager) GetTransferProgress(id string) (*FileTransferProgress, error) {
transfer := m.GetTransfer(id)
if transfer == nil {
return nil, fmt.Errorf("transfer not found")
}
return transfer.GetProgress(), nil
}
// GetSessionProgress gets progress information for all transfers in a session
func (m *FileTransferManager) GetSessionProgress(sessionID string) ([]*FileTransferProgress, error) {
transfers := m.GetTransfersBySession(sessionID)
var progresses []*FileTransferProgress
for _, transfer := range transfers {
progresses = append(progresses, transfer.GetProgress())
}
return progresses, nil
}
// RemoveTransfer removes a transfer by ID
func (m *FileTransferManager) RemoveTransfer(id string) {
m.mutex.Lock()
@@ -135,6 +221,23 @@ func (m *FileTransferManager) RemoveTransfer(id string) {
}
}
// CleanupCompletedTransfers removes completed transfers older than specified duration
func (m *FileTransferManager) CleanupCompletedTransfers(maxAge time.Duration) {
m.mutex.Lock()
defer m.mutex.Unlock()
cutoff := time.Now().Add(-maxAge)
for id, transfer := range m.transfers {
if transfer.Completed && transfer.Updated.Before(cutoff) {
if transfer.file != nil {
transfer.file.Close()
}
delete(m.transfers, id)
logger.L().Debug("Cleaned up completed transfer", zap.String("id", id), zap.String("filename", transfer.Filename))
}
}
}
// Write writes data to an upload file
func (t *FileTransfer) Write(data []byte) (int, error) {
t.mutex.Lock()
@@ -148,16 +251,30 @@ func (t *FileTransfer) Write(data []byte) (int, error) {
return 0, fmt.Errorf("cannot write to download transfer")
}
if t.Status == "pending" {
t.Status = "uploading"
}
n, err := t.file.Write(data)
if err != nil {
t.Status = "failed"
t.Error = err.Error()
t.Updated = time.Now()
return n, err
}
t.Offset += int64(n)
t.Updated = time.Now()
if t.Size > 0 && t.Offset >= t.Size {
t.Completed = true
t.Status = "completed"
}
return n, nil
}
// Read reads data from a download file
// Read reads data from a download file (no progress tracking)
func (t *FileTransfer) Read(p []byte) (int, error) {
t.mutex.Lock()
defer t.mutex.Unlock()
@@ -167,19 +284,56 @@ func (t *FileTransfer) Read(p []byte) (int, error) {
}
n, err := t.file.Read(p)
if err != nil {
if err == io.EOF {
t.Completed = true
}
if err != nil && err != io.EOF {
return n, err
}
t.Offset += int64(n)
if t.Offset >= t.Size {
t.Completed = true
return n, err
}
// SetSize sets the total size for the transfer (useful for uploads where size is known)
func (t *FileTransfer) SetSize(size int64) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.Size = size
}
// GetProgress returns the current progress information
func (t *FileTransfer) GetProgress() *FileTransferProgress {
t.mutex.Lock()
defer t.mutex.Unlock()
var percentage float64
if t.Size > 0 {
percentage = float64(t.Offset) / float64(t.Size) * 100
}
return n, nil
var speed int64
var eta int64
if !t.Created.Equal(t.Updated) && t.Offset > 0 {
duration := t.Updated.Sub(t.Created).Seconds()
speed = int64(float64(t.Offset) / duration)
if speed > 0 && t.Size > t.Offset {
eta = (t.Size - t.Offset) / speed
}
}
return &FileTransferProgress{
ID: t.ID,
SessionID: t.SessionID,
Filename: t.Filename,
Size: t.Size,
Offset: t.Offset,
Percentage: percentage,
Status: t.Status,
IsUpload: t.IsUpload,
Created: t.Created,
Updated: t.Updated,
Error: t.Error,
Speed: speed,
ETA: eta,
}
}
// Close closes the file transfer
@@ -187,7 +341,14 @@ func (t *FileTransfer) Close() error {
t.mutex.Lock()
defer t.mutex.Unlock()
t.Completed = true
if !t.Completed {
if t.Status != "failed" {
t.Status = "completed"
}
t.Completed = true
t.Updated = time.Now()
}
if t.file != nil {
return t.file.Close()
}

View File

@@ -11,7 +11,6 @@ import (
// IFileRepository file history repository interface
type IFileRepository interface {
AddFileHistory(ctx context.Context, history *model.FileHistory) error
GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error)
}
// FileRepository file history repository implementation
@@ -30,29 +29,3 @@ func NewFileRepository(db *gorm.DB) IFileRepository {
func (r *FileRepository) AddFileHistory(ctx context.Context, history *model.FileHistory) error {
return r.db.Create(history).Error
}
// GetFileHistory gets file history records
func (r *FileRepository) GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error) {
db := r.db.Model(&model.FileHistory{})
// Apply filter conditions
for key, value := range filters {
if value != nil && value != "" {
db = db.Where(key, value)
}
}
// Count total records
var count int64
if err := db.Count(&count).Error; err != nil {
return nil, 0, err
}
// Query records
var histories []*model.FileHistory
if err := db.Find(&histories).Error; err != nil {
return nil, 0, err
}
return histories, count, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import (
"github.com/gliderlabs/ssh"
"github.com/gorilla/websocket"
"go.uber.org/zap"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/sync/errgroup"
"gorm.io/gorm/clause"
@@ -125,6 +126,10 @@ type Session struct {
ShareEnd time.Time `json:"-" gorm:"-"`
Once sync.Once `json:"-" gorm:"-"`
Prompt string `json:"-" gorm:"-"`
// SSH connection reuse for file transfers
SSHClient *gossh.Client `json:"-" gorm:"-"`
sshMutex sync.RWMutex `json:"-" gorm:"-"`
}
func (m *Session) HasMonitors() (has bool) {
@@ -166,3 +171,36 @@ func UpsertSession(data *Session) (err error) {
Create(data).
Error
}
// SetSSHClient stores SSH client for connection reuse
func (s *Session) SetSSHClient(client *gossh.Client) {
s.sshMutex.Lock()
defer s.sshMutex.Unlock()
s.SSHClient = client
logger.L().Debug("SSH client stored for session", zap.String("sessionId", s.SessionId))
}
// GetSSHClient gets stored SSH client for connection reuse
func (s *Session) GetSSHClient() *gossh.Client {
s.sshMutex.RLock()
defer s.sshMutex.RUnlock()
return s.SSHClient
}
// ClearSSHClient clears stored SSH client
func (s *Session) ClearSSHClient() {
s.sshMutex.Lock()
defer s.sshMutex.Unlock()
if s.SSHClient != nil {
s.SSHClient.Close()
s.SSHClient = nil
logger.L().Debug("SSH client cleared for session", zap.String("sessionId", s.SessionId))
}
}
// HasSSHClient checks if session has an active SSH client
func (s *Session) HasSSHClient() bool {
s.sshMutex.RLock()
defer s.sshMutex.RUnlock()
return s.SSHClient != nil
}