diff --git a/fsv2/README.md b/fsv2/README.md index 968ca08..0c65713 100644 --- a/fsv2/README.md +++ b/fsv2/README.md @@ -1,2 +1,7 @@ ## 文件系统 第二版本 + +软件用于客户端,用于处理更新文件 + +被动更新模式 +接收文件 --》查找文件并备份 --》 文件覆盖 \ No newline at end of file diff --git a/fsv2/config/config.go b/fsv2/config/config.go new file mode 100644 index 0000000..77214ab --- /dev/null +++ b/fsv2/config/config.go @@ -0,0 +1,73 @@ +package config + +import ( + "fmt" + "net" + "os" +) + +type Config struct { + DeviceName string + Port string + LocalIP string + MulticastAddress string + WildcardAddress string + FilePath string + Version string +} + +var G Config + +func (c *Config) SetConf(port string) error { + Hostname, err := os.Hostname() + if err != nil { + Hostname = "unknow device" + } + // 设备名称 + c.DeviceName = Hostname + c.Port = port + c.LocalIP, err = getLocalIP() + if err != nil { + return err + } + // 多播地址 + c.MulticastAddress = fmt.Sprintf("224.0.0.169:%s", port) + // 广播地址 + c.WildcardAddress = fmt.Sprintf("0.0.0.0:%s", port) + c.FilePath = "./" + c.Version = "0.3.0" + return nil +} + +// 本地ip +func getLocalIP() (string, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "", err + } + var ips []string + for _, address := range addrs { + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + ips = append(ips, ipnet.IP.String()) + } + } + } + if len(ips) == 0 { + return "", fmt.Errorf("get local ip failed") + } else if len(ips) == 1 { + return ips[0], nil + } else { + // Select the one connected to the network + // when there are multiple network interfaces + + // Is there a better way? + c, err := net.Dial("udp", "8.8.8.8:80") + if err != nil { + return ips[0], nil + } + defer c.Close() + return c.LocalAddr().(*net.UDPAddr).IP.String(), nil + } + +} diff --git a/fsv2/go.mod b/fsv2/go.mod index 07bbebe..0cfa67a 100644 --- a/fsv2/go.mod +++ b/fsv2/go.mod @@ -3,6 +3,17 @@ module xtcfs go 1.22.1 require ( - github.com/arl/statsviz v0.6.0 - github.com/gorilla/websocket v1.5.3 + github.com/chyok/st v0.0.0-20240414083746-2b61e9e205ae + github.com/schollz/progressbar/v3 v3.14.2 + github.com/urfave/cli/v2 v2.27.2 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect ) diff --git a/fsv2/go.sum b/fsv2/go.sum index 7d3dee2..19b970e 100644 --- a/fsv2/go.sum +++ b/fsv2/go.sum @@ -1,10 +1,33 @@ -github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE= -github.com/arl/statsviz v0.6.0/go.mod h1:0toboo+YGSUXDaS4g1D5TVS4dXs7S7YYT5J/qnW2h8s= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +github.com/chyok/st v0.0.0-20240414083746-2b61e9e205ae h1:h52yw9f/+Q4Mdyz79m7cnl4HTqY2VEo93eXkqSmiB7Q= +github.com/chyok/st v0.0.0-20240414083746-2b61e9e205ae/go.mod h1:HPBtM/h5CwXveOvIpc3nWE0yO+KNsI/2wqw4iSYOZ/A= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/schollz/progressbar/v3 v3.14.2 h1:EducH6uNLIWsr560zSV1KrTeUb/wZGAHqyMFIEa99ks= +github.com/schollz/progressbar/v3 v3.14.2/go.mod h1:aQAZQnhF4JGFtRJiw/eobaXpsqpVQAftEQ+hLGXaRc4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= diff --git a/fsv2/handler/handler.go b/fsv2/handler/handler.go new file mode 100644 index 0000000..3dfab0e --- /dev/null +++ b/fsv2/handler/handler.go @@ -0,0 +1,387 @@ +package handler + +import ( + "encoding/json" + "fmt" + "html/template" + "io" + "mime" + "net" + "path" + "strings" + "time" + + "net/http" + "net/rpc" + "os" + "path/filepath" + + "xtcfs/config" + "xtcfs/transfer" + "xtcfs/util" + + "github.com/chyok/st/web" +) + +// rpc功能 压缩包的结构体 +// 文件路径 文件名 +type Args struct { + Zpfile, Fname string +} + +// 获取文件大小的接口 +type Size interface { + Size() int64 +} + +// 获取文件信息的接口 +type Stat interface { + Stat() (os.FileInfo, error) +} + +// 显示状态 +func ReceiveStatus(w http.ResponseWriter, r *http.Request) { + switch r.Method { + // 显示当前传送文件的大小 + case http.MethodPost: + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + if statInterface, ok := file.(Stat); ok { + fileInfo, _ := statInterface.Stat() + fmt.Fprintf(w, "上传文件的大小为: %d", fileInfo.Size()) + } + + if sizeInterface, ok := file.(Size); ok { + fmt.Fprintf(w, "上传文件的大小为: %d", sizeInterface.Size()) + } + return + } + + // fmt.Fprintf(w, r.Method) +} + +// 接收 上传 +func ReceiveHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + // serve upload page for receive + // tmpl, err := template.New("index").Parse(web.UploadPage) + // if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + // } + // // 获取当前设备名称 + // err = tmpl.Execute(w, config.G.DeviceName) + // if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // } + // + fmt.Fprintf(w, "%s:接收文件中...", r.Host) + case http.MethodPost: + // receive file and save + file, header, err := r.FormFile("file") + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer file.Close() + + _, params, err := mime.ParseMediaType(header.Header.Get("Content-Disposition")) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + filename := filepath.FromSlash(params["filename"]) + + fmt.Printf("Downloading [%s]...\n", filename) + dirPath := filepath.Dir(filename) + err = os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + out, err := os.Create(filename) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer out.Close() + + _, err = io.Copy(out, file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // 如果收到的是zip文件,自动给解压缩 + suf := strings.Split(filename, ".") + if suf[1] == "zip" { + go util.DecompressZip(filename) + } + + fmt.Printf("[√] Download [%s] Success.\n", filename) + // + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + + // 输出接收结果 + fmt.Fprintf(w, "接收成功,并已经完成解压缩") +} + +// 文件服务 +func FileServerHandler(w http.ResponseWriter, r *http.Request) { + currentPath := config.G.FilePath + + fileInfo, err := os.Stat(currentPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + basePath := filepath.Base(currentPath) + if fileInfo.IsDir() { + path := r.URL.Path[len("/download/"+basePath):] + fullPath := filepath.Join(currentPath, path) + http.ServeFile(w, r, fullPath) + } else { + http.ServeFile(w, r, currentPath) + } +} + +// udp 方式发送zip文件 +func SendZip(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + // 选择文件,并生成zip包 + // 文件 + zipfarr := r.Form["zipfiles"] + // 服务器ip地址 + serip := r.Form["serverip"] + if serip[0] == "" { + http.Error(w, "remote server ip is blank!", http.StatusInternalServerError) + return + } + // 选中的路径 + wtculpath := r.Form["curpath"] + + // 实际路径 + realFilePath := filepath.Join(config.G.FilePath, wtculpath[0]) + + // zip 文件名 + zpFileName := "BIU_" + time.Now().Format("20060102_150405") + ".zip" + + // 创建zip 异步? + taskId := make(chan string) + go func() { + util.CompressToZip(zpFileName, realFilePath, zipfarr) + taskId <- "arcok" + // fmt.Fprintln(w, "create archive:", err) + }() + // go util.CompressToZip(zpFileName, realFilePath, zipfarr) + fmt.Println("archive is createding...") + + // ZIP 文件的实际路径 + ziprl := path.Join("./files/", zpFileName) + + // zip 创建成功后 + rest := <-taskId + // 有压缩包 才可以操作 + if strings.EqualFold(strings.ToLower(rest), "arcok") { + fmt.Println("archive is sending...") + // 创建udp 渠道发送数据 + // 1、获取udp addr + remoteAddr, err := net.ResolveUDPAddr("udp", serip[0]+":9099") + if err != nil { + fmt.Printf("Failed to resolve %s: %v\n", serip[0], err) + return + } + // 2、 监听端口 + conn, err := net.DialUDP("udp", nil, remoteAddr) + if err != nil { + fmt.Printf("Failed to dial %s: %v\n", serip[0], err) + return + } + defer conn.Close() + // 3、在端口发送数据 + message := fmt.Sprintf("%s%s%s", config.G.DeviceName, "|", "sender") + // 向链接通道发送数据 数据包头 + conn.Write([]byte(message)) + // 发送文件 + go func() { + err := transfer.SendFiles(ziprl, fmt.Sprintf("http://%s", remoteAddr)) + if err != nil { + fmt.Printf("Send file to %s error: %s\n", remoteAddr, err) + } + }() + // 页面上显示 + fmt.Fprintf(w, "File:%s,been sent successfully.", zpFileName) + } else { + fmt.Println("archive is not exist!!!") + } + +} + +// 下载zip +func Downzip(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + // 选择文件,并生成zip包 + // 文件 + zipfarr := r.Form["zipfiles"] + // 服务器ip地址 + serip := r.Form["serverip"] + if serip[0] == "" { + http.Error(w, "remote server ip is blank!", http.StatusInternalServerError) + return + } + // fmt.Println(r.Form) + // 选中的路径 + wtculpath := r.Form["curpath"] + + // 实际路径 + realFilePath := filepath.Join(config.G.FilePath, wtculpath[0]) + + // zip 文件名 + zpFileName := "BIU_" + time.Now().Format("20060102_150405") + ".zip" + + // 创建zip + err := util.CompressToZip(zpFileName, realFilePath, zipfarr) + if err != nil { + fmt.Println("create zip error", err) + } + // 停止下,检查zip是否创建成功 + time.Sleep(1200) + _, err = os.Stat(realFilePath + "/" + zpFileName) + if err != nil { + + // 调用httpd rpc + fmt.Println("RPC Server IP:", serip[0]) + rpcServer := serip[0] + ":9080" + client, err := rpc.DialHTTP("tcp", rpcServer) + if err != nil { + fmt.Println("Http rpc error :", err) + } + // 组装url 静态文件 + // url := "http://" + config.G.LocalIP + ":" + config.G.Port + "/files/" + zpFileName + // 动态 + url := "http://" + config.G.LocalIP + ":" + config.G.Port + "/files?zp=" + zpFileName + args := Args{url, zpFileName} + var reply int + err = client.Call("Rbup.DecompressZip", args, &reply) + if err != nil { + fmt.Println("rpc call error :", err) + // 输出执行结果 + fmt.Fprintln(w, "rpc call error:", err) + } else { + // 输出下载链接 + fmt.Fprintf(w, "%s
\n", url, url) + fmt.Fprintf(w, "RPC execute result :%d
\n", reply) + } + } else { + fmt.Fprintf(w, "archive is created faild") + } + +} + +// 文件下载 +func Dfiles(w http.ResponseWriter, r *http.Request) { + // url中获取参数 + query := r.URL.Query() + zpfile := query.Get("zp") + + // 实际路径 + zpFileName := filepath.Join(config.G.FilePath, "/files/", zpfile) + // fileInfo, err := os.Stat(zpFileName) + // if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + // } + // fmt.Println(fileInfo) + http.ServeFile(w, r, zpFileName) +} + +// 发送拦截 +func SendHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + // serve download page for send + realFilePath := filepath.Join(config.G.FilePath, r.URL.Path[1:]) + downloadPath := filepath.Join(filepath.Base(config.G.FilePath), r.URL.Path[1:]) + fileInfo, err := os.Stat(realFilePath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := struct { + DeviceName string + IsDir bool + FileName string + DownloadPath string + UrlPath string + Files []os.DirEntry + }{ + DeviceName: config.G.DeviceName, + DownloadPath: downloadPath, + UrlPath: strings.TrimSuffix(r.URL.Path, "/"), + } + + if fileInfo.IsDir() { + data.IsDir = true + // 遍历目录 + files, err := os.ReadDir(realFilePath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + data.Files = files + } else { + data.FileName = filepath.Base(realFilePath) + } + + // 文件列表模板 + tmpl, err := template.New("download").Parse(web.DownloadPage) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = tmpl.Execute(w, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + case http.MethodPost: + // return file or folder information in JSON format for convenient send to the recipient + currentPath := config.G.FilePath + fileInfo, err := os.Stat(currentPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var pathInfo struct { + Type string `json:"type"` + Paths []string `json:"paths"` + } + + if fileInfo.IsDir() { + files, err := util.GetDirFilePaths(currentPath, true) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + pathInfo.Paths = files + pathInfo.Type = "dir" + } else { + pathInfo.Paths = []string{filepath.Base(currentPath)} + pathInfo.Type = "file" + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(pathInfo) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/fsv2/main.go b/fsv2/main.go index 4ae5e5f..8cfdce4 100644 --- a/fsv2/main.go +++ b/fsv2/main.go @@ -1,73 +1,77 @@ package main import ( - "math/rand" + "fmt" "net/http" - "strconv" - "time" + "os" - "github.com/arl/statsviz" - "github.com/gorilla/websocket" -) - -var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true // 允许跨域请求 - }, -} + "xtcfs/config" + "xtcfs/handler" -func handleConnections(w http.ResponseWriter, r *http.Request) { - // 将HTTP连接升级为WebSocket连接 - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - println("Failed to set up WebSocket:", err) - return - } - defer conn.Close() + "github.com/urfave/cli/v2" +) - // 这里可以添加代码来处理实时状态更新 - // 例如,可以循环接收客户端发送的消息,并发送给所有连接的客户端 - for { - // 接收客户端消息 - messageType, p, err := conn.ReadMessage() - if err != nil { - println("Error reading message:", err) - return - } - println("Received message:", string(p)) +var ( + port string +) - // 将接收到的消息发送给所有客户端 - err = conn.WriteMessage(messageType, p) - if err != nil { - println("Error sending message:", err) - return - } - } +// 初始化配置 +func initConfig(c *cli.Context) error { + return config.G.SetConf(port) } -func work() { - // Generate some allocations - m := map[string][]byte{} - - for { - b := make([]byte, 512+rand.Intn(16*1024)) - m[strconv.Itoa(len(m)%(10*100))] = b +// 接收端 管理上传 +func receiveClient() error { + //接收者角色 - if len(m)%(10*100) == 0 { - m = make(map[string][]byte) - } + // 注释上面的局域网广播 + fmt.Println("xtfs run as receive role...") + // 显示状态等 + // http.HandleFunc("/", handler.ReceiveStatus) + http.HandleFunc("/", handler.ReceiveHandler) + // http.Handle("/static/", http.StripPrefix("/static/", + // http.FileServer(http.FS(web.StaticFs)))) - time.Sleep(10 * time.Millisecond) - } + fmt.Println("Waiting for receive...") + return http.ListenAndServe(config.G.WildcardAddress, nil) } +// 入口函数 +/** +* 保留接收的功能,用来做接收端 +**/ func main() { - statsviz.RegisterDefault() - go work() - // http.ListenAndServe(":8080", nil) - http.HandleFunc("/ws", handleConnections) - err := http.ListenAndServe(":8080", nil) + app := &cli.App{ + Name: "xtfs", + Usage: "simple file transfer tool", + UsageText: "xtfs [global options] [filename|foldername]", + Description: "xtfs is a simple command-line tool for fast local file/folder sharing", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Value: "9099", + Usage: "server port", + Aliases: []string{"p"}, + Destination: &port, + }, + &cli.BoolFlag{ + Name: "version", + Aliases: []string{"v"}, + Usage: "print the version", + }, + }, + Action: func(c *cli.Context) error { + // if c.NArg() > 0 { + // currentPath := filepath.ToSlash(c.Args().Get(0)) + // config.G.FilePath = currentPath + // return sendClient() + // } + return receiveClient() + }, + Before: initConfig, + } + err := app.Run(os.Args) if err != nil { - println("ListenAndServe:", err) + fmt.Println(err.Error()) } } diff --git a/fsv2/fscore/receive.go b/fsv2/transfer/receive.go similarity index 100% rename from fsv2/fscore/receive.go rename to fsv2/transfer/receive.go diff --git a/fsv2/transfer/send.go b/fsv2/transfer/send.go new file mode 100644 index 0000000..c14b3e3 --- /dev/null +++ b/fsv2/transfer/send.go @@ -0,0 +1,119 @@ +package transfer + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "xtcfs/util" + + "github.com/schollz/progressbar/v3" +) + +// 发送文件 +func SendFiles(filePath string, url string) error { + filePath = filepath.ToSlash(filePath) + fileInfo, err := os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("file [%s] not exist", filePath) + } + return fmt.Errorf("file [%s] error: %w", filePath, err) + } + + if fileInfo.IsDir() { + return postDirectory(filePath, url) + } + + return postFile(filePath, path.Base(filePath), url) +} + +// 传送文件夹 +func postDirectory(dirPath string, url string) error { + files, err := util.GetDirFilePaths(dirPath, false) + if err != nil { + return err + } + + fmt.Println("\nAll files in folder:") + for _, file := range files { + fmt.Println(file) + } + + var confirm string + fmt.Print("\nTransfer all files? [Y/N] ") + fmt.Scanln(&confirm) + if strings.ToLower(confirm) != "y" { + fmt.Print("\nCancel send all files ") + return nil + } + + for _, file := range files { + fileName, _ := filepath.Rel(dirPath, file) + fileName = filepath.Join(filepath.Base(dirPath), fileName) + err := postFile(file, fileName, url) + if err != nil { + return err + } + } + fmt.Printf("Send folder %s success.\n", dirPath) + return nil +} + +// 传送文件 +func postFile(filePath string, filename string, url string) error { + payload := &bytes.Buffer{} + writer := multipart.NewWriter(payload) + + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + part, err := writer.CreateFormFile("file", filepath.ToSlash(filename)) + if err != nil { + return err + } + + fileInfo, _ := file.Stat() + bar := progressbar.DefaultBytes( + fileInfo.Size(), + fmt.Sprintf("Uploading [%s]", filename), + ) + + _, err = io.Copy(io.MultiWriter(part, bar), file) + if err != nil { + return err + } + + err = writer.Close() + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPost, url, payload) + if err != nil { + return err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("upload failed with status code: %d", resp.StatusCode) + } + + return nil +} diff --git a/fsv2/util/util.go b/fsv2/util/util.go new file mode 100644 index 0000000..2ab5699 --- /dev/null +++ b/fsv2/util/util.go @@ -0,0 +1,233 @@ +package util + +import ( + "archive/zip" + "fmt" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "strings" +) + +// 遍历目录下的文件 +func GetDirFilePaths(dirPath string, relativeOnly bool) ([]string, error) { + var filePaths []string + err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + if relativeOnly { + fileName := filepath.Base(path) + relativePath := filepath.ToSlash(filepath.Join(filepath.Base(dirPath), fileName)) + filePaths = append(filePaths, relativePath) + } else { + filePaths = append(filePaths, filepath.ToSlash(path)) + } + } + return nil + }) + if err != nil { + return nil, err + } + return filePaths, nil +} + +// dest:目的地址以及压缩文件名 eg:/data/logZip/2022-12-12-log.zip +// paths:需要压缩的所有文件所组成的集合 +func CompressToZip(dest string, currentPath string, paths []string) error { + // fmt.Println("real path", currentPath) + // 打开files 目录 + filesPath := "" + if dir, err := os.Getwd(); err == nil { + filesPath = dir + "/files/" + } + if err := os.MkdirAll(filesPath, 0755); err != nil { + fmt.Println(err.Error()) + } + // ToSlash 过滤windows的斜杠引起的bug + zfile, err := os.Create(path.Join("./files", "/", dest)) + if err != nil { + return err + } + defer zfile.Close() + zipWriter := zip.NewWriter(zfile) + defer zipWriter.Close() + + // 遍历带压缩的目录信息 + for _, src := range paths { + // 替换文件名,并且去除前后 "\" 或 "/" + // path = strings.Trim(path, string(filepath.Separator)) + // 从配置中读取到源目录 + src = path.Join(currentPath, "/", src) + // 删除尾随路径(如果它是目录) + src := strings.TrimSuffix(src, string(os.PathSeparator)) + err = filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // 创建本地文件头 + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + // 将压缩方法设置为deflate + header.Method = zip.Deflate + + // 在zip存档中设置文件的相对路径 + header.Name, err = filepath.Rel(filepath.Dir(src), path) + if err != nil { + return err + } + + // 目录需要拼上一个 "/" ,否则会出现一个和目录一样的文件在压缩包中 + if info.IsDir() { + // header.Name += string(os.PathSeparator) + header.Name += "/" + } else { + // 替换一下分隔符,zip不支持 "\\" + header.Name = strings.ReplaceAll(header.Name, "\\", "/") + } + // 创建写入头的writer + headerWriter, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + if info.IsDir() { + return nil + } + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(headerWriter, f) + return err + }) + if err != nil { + return err + } + } + // 存到指定位置 + os.Rename(dest, path.Join("./files/", dest)) + return nil +} + +// 解压缩zip文件 +// 这个方法必须是首字母大写的,才能被注册 +// src: 需要解压的zip文件 +func DecompressZip(zpFname string) error { + // 下载文件 + archive, err := zip.OpenReader(zpFname) + if err != nil { + return err + } + dir := filepath.Dir(zpFname) + defer archive.Close() + // 遍历目录 + for _, f := range archive.File { + filePath := filepath.Join(dir, f.Name) + if f.FileInfo().IsDir() { + os.MkdirAll(filePath, os.ModePerm) + continue + } + // 父文件夹开始闯将目录 + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return fmt.Errorf("failed to make directory (%v)", err) + } + // 文件原有模式 + dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return fmt.Errorf("failed to create file (%v)", err) + } + fileInArchive, err := f.Open() + if err != nil { + return fmt.Errorf("failed to open file in zip (%v)", err) + } + if _, err := io.Copy(dstFile, fileInArchive); err != nil { + return fmt.Errorf("failed to copy file in zip (%v)", err) + } + dstFile.Close() + fileInArchive.Close() + } + return nil +} + +// Zip 压缩文件或目录 +// @params dst io.Writer 压缩文件可写流 +// @params src string 待压缩源文件/目录路径 +func Zip(dst io.Writer, src string) error { + // 强转一下路径 + src = filepath.Clean(src) + // 提取最后一个文件或目录的名称 + baseFile := filepath.Base(src) + // 判断src是否存在 + _, err := os.Stat(src) + if err != nil { + return err + } + + // 通文件流句柄创建一个ZIP压缩包 + zw := zip.NewWriter(dst) + // 延迟关闭这个压缩包 + defer zw.Close() + + // 通过filepath封装的Walk来递归处理源路径到压缩文件中 + return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error { + // 是否存在异常 + if err != nil { + return err + } + + // 通过原始文件头信息,创建zip文件头信息 + zfh, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + // 赋值默认的压缩方法,否则不压缩 + zfh.Method = zip.Deflate + + // 移除绝对路径 + tmpPath := path + index := strings.Index(tmpPath, baseFile) + if index > -1 { + tmpPath = tmpPath[index:] + } + // 替换文件名,并且去除前后 "\" 或 "/" + tmpPath = strings.Trim(tmpPath, string(filepath.Separator)) + // 替换一下分隔符,zip不支持 "\\" + zfh.Name = strings.ReplaceAll(tmpPath, "\\", "/") + // 目录需要拼上一个 "/" ,否则会出现一个和目录一样的文件在压缩包中 + if info.IsDir() { + zfh.Name += "/" + } + + // 写入文件头信息,并返回一个ZIP文件写入句柄 + zfw, err := zw.CreateHeader(zfh) + if err != nil { + return err + } + + // 仅在他是标准文件时进行文件内容写入 + if zfh.Mode().IsRegular() { + // 打开要压缩的文件 + sfr, err := os.Open(path) + if err != nil { + return err + } + defer sfr.Close() + + // 将srcFileReader拷贝到zipFilWrite中 + _, err = io.Copy(zfw, sfr) + if err != nil { + return err + } + } + + // 搞定 + return nil + }) +}