当前位置 博文首页 > golang实现的文件上传下载小工具

    golang实现的文件上传下载小工具

    作者:janbar 时间:2021-02-04 12:17

    前言

    虽然现在文件上传下载工具多如牛毛,比如http、ftp、sftp、scp等方案都可以用于文件传输,但都是需要安装服务器甚至客户端。
    有一种场景是我只需要临时上传或下载一个文件,完了就不用服务器运行了,如果使用那些文件传输工具,不光安装麻烦,开启关闭也恼火额。
    因此才想搞小工具,不过Python爱好者可以用python -m http.server 8080 --bind 192.168.1.100开启文件服务器,对我来说还是麻烦。
    已经上传到【Github】,随意鉴赏。

    源码鉴赏

    模拟一个http服务器,通过curl和wget命令作为客户端实现文件的上传下载功能。
    只是实现一个小工具,所以没必要使用http库了,我也试过用http库来完成相同的功能,发现很多东西都用不上。
    上传和下载文件加入了进度显示,方便知道上传和下载进度。本来想实现断点续传功能,但比较懒,不想弄,原理很简单。
    想想还是把认证授权加上去,不然太不安全了,命令行更新了使用用户名和密码方式的上传和下载。

    package main
     
    import (
      "bufio"
      "encoding/base64"
      "errors"
      "fmt"
      "io"
      "net"
      "net/url"
      "os"
      "path/filepath"
      "strconv"
      "strings"
      "unsafe"
    )
     
    func main() {
      if len(os.Args) != 3 {
        fmt.Printf(`usage: %s ip:port user:pass
     
    get file:
     wget --auth-no-challenge --user=user --password=pass --content-disposition "http://ip:port?/home/tmp.txt"
     curl -u user:pass -OJ "http://ip:port?/home/tmp.txt"
    post file:
     wget -qO - --auth-no-challenge --user=user --password=pass --post-file=C:\tmp.txt "http://ip:port?/home/tmp.txt"
     curl -u user:pass --data-binary @C:\tmp.txt "http://ip:port?/home/tmp.txt"
    `, os.Args[0])
        return
      }
      addr, err := net.ResolveTCPAddr("tcp", os.Args[1])
      if err != nil {
        panic(err)
      }
      ser, err := net.ListenTCP("tcp", addr)
      if err != nil {
        panic(err)
      }
     
      fmt.Printf("Listen: [%s]\n", addr)
      authStr = "Basic " + base64.StdEncoding.EncodeToString([]byte(os.Args[2]))
      for {
        ln, err := ser.AcceptTCP()
        if err != nil {
          panic(err)
        }
        go func(l *net.TCPConn) {
          err := handleFile(l)
          if err != nil {
            respData(l, err.Error())
          }
          l.Close()
        }(ln)
      }
    }
     
    const (
      maxMemory = 10 << 20 // 缓存10MB
      respMsg  = "HTTP/1.1 200 OK\r\nContent-Type:text/plain;charset=utf-8\r\nContent-Disposition:attachment;filename=resp.txt\r\nContent-Length:%d\r\n\r\n%s"
      getHeader = "HTTP/1.1 200 OK\r\nContent-Type:application/octet-stream\r\nContent-Disposition:attachment;filename=%s\r\nContent-Length:%d\r\nContent-Transfer-Encoding:binary\r\n\r\n"
    )
     
    var authStr string // 授权信息
     
    func respData(w io.Writer, data string) {
      msg := data + "\r\n"
      fmt.Fprintf(w, respMsg, len(msg), msg)
    }
     
    func handleFile(l *net.TCPConn) error {
      br := bufio.NewReaderSize(l, maxMemory)
      method, path, length, err := getHeaderMsg(br)
      if err != nil {
        return err
      }
      fmt.Printf("[%s - %s - %d]\n", method, path, length)
     
      if method == "GET" {
        return httpGetFile(path, l, length)
      }
      err = httpPostFile(path, br, length)
      if err != nil {
        return err
      }
      respData(l, "post ok")
      return nil
    }
     
    // 内存复用,更快速,省内存
    func bytesToString(b []byte) string {
      return *(*string)(unsafe.Pointer(&b))
    }
     
    func getHeaderMsg(r *bufio.Reader) (string, string, int64, error) {
      // 读取第一行,提取有用信息
      line, _, err := r.ReadLine()
      if err != nil {
        return "", "", 0, err
      }
      header := strings.Fields(bytesToString(line))
      if len(header) < 3 { // 首行至少3列数据
        return "", "", 0, errors.New("header error")
      }
      method, path := header[0], ""
     
      s := strings.Index(header[1], "?")
      if s >= 0 {
        path, _ = url.QueryUnescape(header[1][s+1:])
      }
      if path == "" { // ?号后面就是文件路径,需要解码url一下
        return "", "", 0, errors.New("path error")
      }
     
      var length int64
      if method == "GET" {
        fi, err := os.Stat(path)
        if err != nil {
          return "", "", 0, err
        }
        length = fi.Size() // GET请求提前得到文件大小
      } else if method != "POST" {
        return "", "", 0, errors.New(method + " not support")
      }
     
      var authCheck string
      for {
        line, _, err = r.ReadLine()
        if err != nil {
          return "", "", 0, err
        }
        if len(line) == 0 {
          break // 遇到空行,之后为请求体
        }
        header = strings.Split(bytesToString(line), ":")
        if len(header) == 2 { // 头部[key: val]解析
          header[0] = strings.ToLower(strings.TrimSpace(header[0]))
          header[1] = strings.TrimSpace(header[1])
          if method == "POST" && header[0] == "content-length" {
            length, _ = strconv.ParseInt(header[1], 10, 64)
          } else if header[0] == "authorization" {
            authCheck = header[1]
          }
        }
      }
      if authCheck != authStr {
        return "", "", 0, errors.New("authorization error")
      }
      return method, path, length, nil
    }
     
    func httpPostFile(path string, r io.Reader, length int64) error {
      fw, err := os.Create(path)
      if err != nil {
        return err
      }
      defer fw.Close()
      pr := newProgress(r, length)
      _, err = io.CopyN(fw, pr, length)
      pr.Close()
      return err
    }
     
    func httpGetFile(path string, w io.Writer, size int64) error {
      fr, err := os.Open(path)
      if err != nil {
        return err
      }
      defer fr.Close()
      fmt.Fprintf(w, getHeader, filepath.Base(path), size)
      pr := newProgress(fr, size)
      _, err = io.Copy(w, pr)
      pr.Close()
      return err
    }
     
    type progress struct {
      r  io.Reader
      cnt int64
      rate chan int64
    }
     
    func newProgress(r io.Reader, size int64) io.ReadCloser {
      p := &progress{r: r, rate: make(chan int64)}
      // 之所以这样做进度,是因为打印耗性能,因此在协程中打印进度
      // 在处理数据中用非阻塞方式往chan中传处理字节数
      go func(rate <-chan int64, all int64) {
        for cur := range rate {
          fmt.Printf("\rhandle:%4d%%", cur*100/all)
        }
        fmt.Printf("\rhandle: 100%%\r\n\r\n")
      }(p.rate, size)
      return p
    }
     
    func (p *progress) Read(b []byte) (int, error) {
      n, err := p.r.Read(b)
      p.cnt += int64(n)
      select { // 非阻塞方式往chan中写数据
      case p.rate <- p.cnt:
      default:
      }
      return n, err
    }
     
    func (p *progress) Close() error {
      close(p.rate) // 关闭chan,通知打印协程退出
      return nil
    }
    

    食用方法

    执行UpDownFile-h可以查看帮助文档,里面有wget和curl上传和下载文件的命令,方便忘记命令的时候copy一下下。
    工具虽小,但确实解决了我个人的临时上传下载文件需求,再也不用到处安装各种服务器咯,爽爽哒。

    usage: UpDownFile ip:port
     
    get file:
     wget --auth-no-challenge --user=user --password=pass --content-disposition "http://ip:port?/home/tmp.txt"
     curl -u user:pass -OJ "http://ip:port?/home/tmp.txt"
    post file:
     wget -qO - --auth-no-challenge --user=user --password=pass --post-file=C:\tmp.txt "http://ip:port?/home/tmp.txt"
     curl -u user:pass --data-binary @C:\tmp.txt "http://ip:port?/home/tmp.txt"
    
    js
    下一篇:没有了