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 }