前言
雖然現在檔案上傳下載工具多如牛毛,比如
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"
"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一下下。
工具雖小,但確實解決了我個人的臨時上傳下載檔案需求,再也不用到處安裝各種伺服器咯,爽爽噠。
usage: UpDownFile 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