Files
remote-core/file/file_http.go
T
w11 872f843c6f refactor(file,shell,process): 去 gin 化——标准库 http.HandlerFunc + webhttp.Router 适配层
- 新增 webhttp 包: Router 最小路由接口(5 动词) + ServeMuxRouter 适配器 + WriteJSON 统一响应
- file/shell/process 三个被控能力包改以标准库 handler 形态注册, 不再绑死 gin
- 由此 gin 及其连带重依赖(quic-go/http3、mongo bson、ugorji codec、goccy、validator)退出被控端二进制
- shell: recDir 死参数修复——原实现硬编码 ./rec, 传入的录制目录不生效; 现录制写入与记录列表同源
- shell: 终端创建失败由 panic 改为记日志后关连接(net/http 按连接兜底恢复, 不再依赖框架 Recovery)
- file: 上传落盘不再沿用 SaveUploadedFile 的 MkdirAll+Chmod(Windows 盘根 Chmod 报参数不正确), 保留 MkdirAll 去掉 Chmod
- 消费方: remote-client 纯 ServeMux 承载; remote-service 保留 gin 栈经 gin.WrapH 桥接(middle 链照常生效)

破坏性变更: RegisterRouters* 形参 gin.RouterGroup → webhttp.Router, 升级 tag v0.5.0
2026-09-29 08:26:23 +08:00

316 lines
7.6 KiB
Go

package file
// 移植自 remote-backend-server internal/handlers/file(去 DB 依赖)。
// 标准库 http.HandlerFunc 形态, 路由注册经 webhttp.Router 适配(去 gin 化)。
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/jaypipes/ghw"
"github.com/shirou/gopsutil/v3/disk"
"git.zeroonesoft.cn/golib/remote-core/common"
"git.zeroonesoft.cn/golib/remote-core/webhttp"
)
// RegisterRoutersGatewayFile 注册路由。
// 下载注册的是 GET, ServeMux 模式下 HEAD 自动兼容(响应头一致、无响应体);
// 原 gin 独立 HEAD handler 在文件缺失时会因 fi 空指针 panic, 一并消失。
func RegisterRoutersGatewayFile(r webhttp.Router) {
r.GET("/file/list", getGatewayFileList)
r.POST("/file/list", postGatewayFileList)
r.DELETE("/file", deleteGatewayFile)
r.PUT("/file/name", putGatewayFileName)
r.GET("/file/download", getGatewayFileDownload)
r.HEAD("/file/download", headGatewayFileDownload)
r.POST("/file/upload", postGatewayFileUpload)
r.POST("/file/dir", postGatewayFileDir)
}
type PathData struct {
Path string `json:"path"`
}
func fail(w http.ResponseWriter, msg string) {
webhttp.WriteJSON(w, http.StatusOK, map[string]any{
"code": 1,
"msg": msg,
"data": "OK",
})
}
func ok(w http.ResponseWriter, data any) {
webhttp.WriteJSON(w, http.StatusOK, map[string]any{
"code": 0,
"msg": "操作成功",
"data": data,
})
}
// 传递文件路径不方便 比如 路径中包含 #
func getGatewayFileList(w http.ResponseWriter, r *http.Request) {
dir := r.URL.Query().Get("path")
data, err := getFileList(dir)
if err != nil {
fail(w, "error:"+err.Error())
return
}
ok(w, map[string]any{
"list": data,
"count": len(data),
"path": dir,
})
}
func deleteGatewayFile(w http.ResponseWriter, r *http.Request) {
path := r.URL.Query().Get("path")
exists, err := common.PathExists(path)
if err != nil {
fail(w, "error:"+err.Error())
return
}
if !exists {
fail(w, "error:文件不存在")
return
}
if err = os.Remove(path); err != nil {
fail(w, "error:"+err.Error())
return
}
ok(w, nil)
}
func putGatewayFileName(w http.ResponseWriter, r *http.Request) {
oldPath := r.URL.Query().Get("old")
newFile := r.URL.Query().Get("new")
if _, err := os.Stat(oldPath); err != nil {
if os.IsNotExist(err) {
fail(w, "error:文件不存在")
return
}
fail(w, "error:"+err.Error())
return
}
dir := filepath.Dir(oldPath)
newPath := filepath.Join(dir, newFile)
if err := os.Rename(oldPath, newPath); err != nil {
fail(w, "error:"+err.Error())
return
}
ok(w, nil)
}
func postGatewayFileDir(w http.ResponseWriter, r *http.Request) {
dir := r.URL.Query().Get("dir")
exists, err := common.PathExists(dir)
if err != nil {
fail(w, "error:"+err.Error())
return
}
if exists {
fail(w, "error:文件不存在")
return
}
if err = os.MkdirAll(dir, 0750); err != nil {
fail(w, "error:"+err.Error())
return
}
ok(w, nil)
}
func postGatewayFileList(w http.ResponseWriter, r *http.Request) {
dir := PathData{}
// body 解析失败按空路径处理(列盘符), 与原 gin BindJSON 忽略错误的语义一致
_ = json.NewDecoder(r.Body).Decode(&dir)
data, err := getFileList(dir.Path)
if err != nil {
fail(w, "error:"+err.Error())
return
}
ok(w, map[string]any{
"list": data,
"count": len(data),
"path": dir.Path,
})
}
type FileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
Size int64 `json:"size"`
Mode string `json:"mode"`
ModTime string `json:"modTime"`
IsDir bool `json:"isDir"`
IsDrive bool `json:"isDrive"`
Used int64 `json:"used"`
DriveType string `json:"driveType"`
}
func getFileList(dir string) ([]FileInfo, error) {
list := make([]FileInfo, 0)
if dir == "" {
partitions, err := disk.Partitions(false)
//fmt.Printf("disk partitions:%v\n\n", partitions)
if err != nil {
// return nil, err
}
block, err := ghw.Block()
for i, partition := range partitions {
usage, err := disk.Usage(partitions[i].Mountpoint)
if err != nil {
break
}
label, err := getVolumeName(partition.Device)
name := label
if label != partition.Device {
name = name + " (" + partition.Device + ")"
}
driveType := ""
for _, d := range block.Disks {
// fmt.Printf(" %v\n", disk.StorageController)
for _, part := range d.Partitions {
if part.Name == partition.Device {
driveType = d.DriveType.String()
break
}
}
}
list = append(list, FileInfo{
Name: name,
Path: partition.Mountpoint + "/",
Size: int64(usage.Total),
Used: int64(usage.Used),
Mode: "",
ModTime: "",
IsDir: false,
IsDrive: true,
DriveType: driveType,
})
}
return list, nil
}
f, err := os.Open(dir)
if err != nil {
fmt.Println(err)
}
files, err := f.Readdir(-1)
f.Close()
if err != nil {
fmt.Println(err)
}
for _, file := range files {
list = append(list, FileInfo{
Name: file.Name(),
Path: filepath.Join(dir, file.Name()),
Size: file.Size(),
Mode: file.Mode().String(),
ModTime: file.ModTime().Format("2006-01-02 15:04:05"),
IsDir: file.IsDir(),
})
}
return list, nil
}
func getGatewayFileDownload(w http.ResponseWriter, r *http.Request) {
filePath := r.URL.Query().Get("filename")
fileName := filepath.Base(filePath)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+fileName)
w.Header().Set("Content-Transfer-Encoding", "binary")
http.ServeFile(w, r, filePath)
}
func headGatewayFileDownload(w http.ResponseWriter, r *http.Request) {
filePath := r.URL.Query().Get("filename")
fileName := filepath.Base(filePath)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+fileName)
w.Header().Set("Content-Transfer-Encoding", "binary")
if fi, err := os.Stat(filePath); err == nil {
w.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10))
}
w.WriteHeader(http.StatusOK)
}
type Win32_Volume struct {
Name string
Label string
}
type FileData struct {
OriginalFileName string `json:"originalFileName"`
FileName string `json:"fileName"`
FilePath string `json:"filePath"`
Size int64 `json:"size"`
Md5 string `json:"md5"`
}
func postGatewayFileUpload(w http.ResponseWriter, r *http.Request) {
dir := r.URL.Query().Get("dir")
file, err := HttpUploadFile(r, dir)
if err != nil {
webhttp.WriteJSON(w, http.StatusOK, map[string]any{
"code": 1,
"msg": "上传失败:" + err.Error(),
})
return
}
webhttp.WriteJSON(w, http.StatusOK, map[string]any{
"code": 0,
"msg": "success",
"status": "done",
"file": file,
})
}
// HttpUploadFile 接收 multipart 上传并落盘。
// 不沿用 gin SaveUploadedFile 的 MkdirAll+Chmod: Windows 盘根上 Chmod 报
// "参数不正确" 导致盘根上传必败(remote-service 已用流式接管绕开此坑, 这里
// 是非 multipart 请求的兜底路径), 目录创建保留、Chmod 去掉。
func HttpUploadFile(r *http.Request, saveDir string) (*FileData, error) {
fh, head, err := r.FormFile("file")
if err != nil {
return nil, err
}
defer fh.Close()
savePath := filepath.Join(saveDir, head.Filename)
if err = os.MkdirAll(filepath.Dir(savePath), 0755); err != nil {
return nil, err
}
out, err := os.Create(savePath)
if err != nil {
return nil, err
}
defer out.Close()
if _, err = io.Copy(out, fh); err != nil {
return nil, err
}
return &FileData{
OriginalFileName: head.Filename,
FileName: head.Filename,
FilePath: savePath,
Size: head.Size,
}, nil
}