文件上傳下載小工具

前言

雖然如今文件上傳下載工具多如牛毛,好比http、ftp、sftp、scp等方案均可以用於文件傳輸,但都是須要安裝服務器甚至客戶端。
有一種場景是我只須要臨時上傳或下載一個文件,完了就不用服務器運行了,若是使用那些文件傳輸工具,不光安裝麻煩,開啓關閉也惱火額。
所以纔想搞小工具,不過Python愛好者能夠用python -m http.server 8080 --bind 192.168.1.100開啓文件服務器,對我來講仍是麻煩。
已經上傳到【Github】,隨意鑑賞。python

源碼鑑賞

模擬一個http服務器,經過curl和wget命令做爲客戶端實現文件的上傳下載功能。
只是實現一個小工具,因此不必使用http庫了,我也試過用http庫來完成相同的功能,發現不少東西都用不上。
上傳和下載文件加入了進度顯示,方便知道上傳和下載進度。原本想實現斷點續傳功能,但比較懶,不想弄,原理很簡單。git

package main

import (
    "bufio"
    "errors"
    "fmt"
    "io"
    "net"
    "net/url"
    "os"
    "path/filepath"
    "strconv"
    "strings"
    "unsafe"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Printf(`usage: %s ip:port

get file:
  wget --content-disposition "http://ip:port?/root/tmp.txt"
  curl -OJ "http://ip:port?/root/tmp.txt"
post file:
  wget -q -O - --post-file=d:\tmp.txt "http://ip:port?/root/tmp.txt"
  curl "http://ip:port?/root/tmp.txt" --data-binary @d:\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)
    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"
)

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 getHeaderMsg(r *bufio.Reader) (string, string, int64, error) {
    // 內存複用,更快速,省內存
    bytesToString := func(b []byte) string {
        return *(*string)(unsafe.Pointer(&b))
    }

    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")
    }

    for {
        line, _, err = r.ReadLine()
        if err != nil {
            return "", "", 0, err
        }
        if len(line) == 0 {
            break // 遇到空行,則以後爲請求體
        }
        if method == "POST" { // POST請求才須要經過header找到消息體長度
            header = strings.Split(bytesToString(line), ":")
            if len(header) == 2 && strings.ToLower(header[0]) == "content-length" {
                // 獲取消息體長度字節數
                length, _ = strconv.ParseInt(strings.TrimSpace(header[1]), 10, 64)
            }
        }
    }
    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一下下。
工具雖小,但確實解決了我我的的臨時上傳下載文件需求,不再用處處安裝各類服務器咯,爽爽噠。github

usage: hfile ip:port

get file:
  wget --content-disposition "http://ip:port?/root/tmp.txt"
  curl -OJ "http://ip:port?/root/tmp.txt"
post file:
  wget -q -O - --post-file=d:\tmp.txt "http://ip:port?/root/tmp.txt"
  curl "http://ip:port?/root/tmp.txt" --data-binary @d:\tmp.txt
相關文章
相關標籤/搜索