欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Go实战--golang实现静态文件服务器(文件查看,文件上传,文件下载)

程序员文章站 2022-07-09 21:08:03
...

生命不止,继续 go go go !!!

之前写过博客介绍net/http包:
Go语言学习之net/http包(The way to go)

package net/http

再次温习几个方法:
http.FileServer
FileServer returns a handler that serves HTTP requests with the contents of the file system rooted at root.

func StripPrefix(prefix string, h Handler)
Handler将请求url.path中移出指定的前缀,然后将省下的请求交给handler h来处理,对于那些不是以指定前缀开始的路径请求,该函数返回一个http 404 not found 的错误.

type Dir
A Dir implements FileSystem using the native file system restricted to a specific directory tree.

type Handler
A Handler responds to an HTTP request.

func (*Request) ParseMultipartForm

func (r *Request) ParseMultipartForm(maxMemory int64) error

ParseMultipartForm parses a request body as multipart/form-data. The whole request body is parsed and up to a total of maxMemory bytes of its file parts are stored in memory, with the remainder stored on disk in temporary files. ParseMultipartForm calls ParseForm if necessary. After one call to ParseMultipartForm, subsequent calls have no effect.
ParseMultipartForm将请求的主体作为multipart/form-data解析。请求的整个主体都会被解析,得到的文件记录最多 maxMemery字节保存在内存,其余部分保存在硬盘的temp文件里。如果必要,ParseMultipartForm会自行调用 ParseForm。重复调用本方法是无意义的。

type Server

type Server struct {
        Addr      string      // TCP address to listen on, ":http" if empty
        Handler   Handler     // handler to invoke, http.DefaultServeMux if nil
        TLSConfig *tls.Config // optional TLS config, used by ListenAndServeTLS

        // ReadTimeout is the maximum duration for reading the entire
        // request, including the body.
        //
        // Because ReadTimeout does not let Handlers make per-request
        // decisions on each request body's acceptable deadline or
        // upload rate, most users will prefer to use
        // ReadHeaderTimeout. It is valid to use them both.
        ReadTimeout time.Duration

        // ReadHeaderTimeout is the amount of time allowed to read
        // request headers. The connection's read deadline is reset
        // after reading the headers and the Handler can decide what
        // is considered too slow for the body.
        ReadHeaderTimeout time.Duration

        // WriteTimeout is the maximum duration before timing out
        // writes of the response. It is reset whenever a new
        // request's header is read. Like ReadTimeout, it does not
        // let Handlers make decisions on a per-request basis.
        WriteTimeout time.Duration

        // IdleTimeout is the maximum amount of time to wait for the
        // next request when keep-alives are enabled. If IdleTimeout
        // is zero, the value of ReadTimeout is used. If both are
        // zero, there is no timeout.
        IdleTimeout time.Duration

        // MaxHeaderBytes controls the maximum number of bytes the
        // server will read parsing the request header's keys and
        // values, including the request line. It does not limit the
        // size of the request body.
        // If zero, DefaultMaxHeaderBytes is used.
        MaxHeaderBytes int

        // TLSNextProto optionally specifies a function to take over
        // ownership of the provided TLS connection when an NPN/ALPN
        // protocol upgrade has occurred. The map key is the protocol
        // name negotiated. The Handler argument should be used to
        // handle HTTP requests and will initialize the Request's TLS
        // and RemoteAddr if not already set. The connection is
        // automatically closed when the function returns.
        // If TLSNextProto is not nil, HTTP/2 support is not enabled
        // automatically.
        TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

        // ConnState specifies an optional callback function that is
        // called when a client connection changes state. See the
        // ConnState type and associated constants for details.
        ConnState func(net.Conn, ConnState)

        // ErrorLog specifies an optional logger for errors accepting
        // connections and unexpected behavior from handlers.
        // If nil, logging goes to os.Stderr via the log package's
        // standard logger.
        ErrorLog *log.Logger
        // contains filtered or unexported fields
}

简单的文件服务器

功能:
1 上传文件
2 显示文件列表

用到的包:
1 package io
io.Copy方法

func Copy(dst Writer, src Reader) (written int64, err error)

Copy 将 src 复制到 dst,直到在 src 上到达 EOF 或发生错误。它返回复制的字节数,如果有的话,还会返回在复制时遇到的第一个错误。
成功的 Copy 返回 err == nil,而非 err == EOF。由于 Copy 被定义为从 src 读取直到 EOF 为止,因此它不会将来自 Read 的 EOF 当做错误来报告。

2 package os
可以参考博客:Go语言学习之os包中文件相关的操作(The way to go)

os.Create方法:

func Create(name string) (*File, error)

Create creates the named file with mode 0666 (before umask), truncating it if it already exists.

完整代码:

package main

import (
    "io"
    "net/http"
    "os"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    //POST takes the uploaded file(s) and saves it to disk.
    case "POST":
        //parse the multipart form in the request
        err := r.ParseMultipartForm(100000)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        //get a ref to the parsed multipart form
        m := r.MultipartForm

        //get the *fileheaders
        files := m.File["uploadfile"]
        for i, _ := range files {
            //for each fileheader, get a handle to the actual file
            file, err := files[i].Open()
            defer file.Close()
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            //create destination file making sure the path is writeable.
            dst, err := os.Create("./upload/" + files[i].Filename)
            defer dst.Close()
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            //copy the uploaded file to the destination file
            if _, err := io.Copy(dst, file); err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }

        }

    default:
        w.WriteHeader(http.StatusMethodNotAllowed)
    }
}

func main() {
    http.HandleFunc("/upload", uploadHandler)

    //static file handler.
    http.Handle("/staticfile/", http.StripPrefix("/staticfile/", http.FileServer(http.Dir("./staticfile"))))

    //Listen on port 8080
    http.ListenAndServe(":8080", nil)
}

通过curl上传文件:

curl -F "aaa@qq.com:\Users\wangs\Desktop\xueba_license.txt" localhost:8080/upload

通过postman上传文件:
记得设置header: Content-Type multipart/form-data****
Go实战--golang实现静态文件服务器(文件查看,文件上传,文件下载)

查看静态文件:
浏览器访问localhost:8080/staticfile

可以浏览器上传的简单文件服务器

引用自:http://github.com/widuu/staticserver
功能:
1 上传文件
2 显示文件列表

用到的包:
1 html/template
参考博客:Go语言学习之html/template包(The way to go)

func (t *Template) ParseFiles(filenames ...string) (*Template, error)

2 path/filepath
参考博客:Go语言学习之path/filepath包(the way to go)

Ext(path string) string

获取路径字符串中的文件扩展名

func Abs(path string) (string, error)

获取 path 的绝对路径

3 strconv
参考博客:Go语言学习之strconv包(The way to go)

func FormatInt(i int64, base int) string

将整数转换为字符串

4 time
参考博客:Go语言学习之time包(获取当前时间戳等)(the way to go)

完整代码:
view文件夹下:
file.html:

<html>
<head>
    <title>{{.}}</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://127.0.0.1:9090/upload" method="post">
  <input type="file" name="uploadfile" />
  <input type="submit" value="upload" />
</form>
</body>
</html>

index.html:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<link href="./css/css.css" rel="stylesheet" type="text/css" />
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <title>{{.Title}}</title>
</head>
<body>
    <div>
        <a href="/upload">上传文件</a></p>
        <a href="/file">查看文件</a></p>
    </div>
</body>
</html>

main.go:

package main

import (
    "fmt"
    "html/template"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "strconv"
    "time"
)

var mux map[string]func(http.ResponseWriter, *http.Request)

type Myhandler struct{}
type home struct {
    Title string
}

const (
    Template_Dir = "./view/"
    Upload_Dir   = "./upload/"
)

func main() {
    server := http.Server{
        Addr:        ":9090",
        Handler:     &Myhandler{},
        ReadTimeout: 10 * time.Second,
    }
    mux = make(map[string]func(http.ResponseWriter, *http.Request))
    mux["/"] = index
    mux["/upload"] = upload
    mux["/file"] = StaticServer
    server.ListenAndServe()
}

func (*Myhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if h, ok := mux[r.URL.String()]; ok {
        h(w, r)
        return
    }
    http.StripPrefix("/", http.FileServer(http.Dir("./upload/"))).ServeHTTP(w, r)
}

func upload(w http.ResponseWriter, r *http.Request) {

    if r.Method == "GET" {
        t, _ := template.ParseFiles(Template_Dir + "file.html")
        t.Execute(w, "上传文件")
    } else {
        r.ParseMultipartForm(32 << 20)
        file, handler, err := r.FormFile("uploadfile")
        if err != nil {
            fmt.Fprintf(w, "%v", "上传错误")
            return
        }
        fileext := filepath.Ext(handler.Filename)
        if check(fileext) == false {
            fmt.Fprintf(w, "%v", "不允许的上传类型")
            return
        }
        filename := strconv.FormatInt(time.Now().Unix(), 10) + fileext
        f, _ := os.OpenFile(Upload_Dir+filename, os.O_CREATE|os.O_WRONLY, 0660)
        _, err = io.Copy(f, file)
        if err != nil {
            fmt.Fprintf(w, "%v", "上传失败")
            return
        }
        filedir, _ := filepath.Abs(Upload_Dir + filename)
        fmt.Fprintf(w, "%v", filename+"上传完成,服务器地址:"+filedir)
    }
}

func index(w http.ResponseWriter, r *http.Request) {
    title := home{Title: "首页"}
    t, _ := template.ParseFiles(Template_Dir + "index.html")
    t.Execute(w, title)
}

func StaticServer(w http.ResponseWriter, r *http.Request) {
    http.StripPrefix("/file", http.FileServer(http.Dir("./upload/"))).ServeHTTP(w, r)
}

func check(name string) bool {
    ext := []string{".exe", ".js", ".png"}

    for _, v := range ext {
        if v == name {
            return false
        }
    }
    return true
}

浏览器访问:
http://localhost:9090/

实现一个共享文件夹服务

出自:Alexis ROBERT [email protected]的代码.

完整代码:

/* Tiny web server in Golang for sharing a folder
Copyright (c) 2010-2014 Alexis ROBERT <aaa@qq.com>

Contains some code from Golang's http.ServeFile method, and
uses lighttpd's directory listing HTML template. */

package main

import "net/http"
import "net/url"
import "io"
import "os"
import "mime"
import "path"
import "fmt"
import "flag"
import "strings"
import "strconv"
import "text/template"
import "container/list"
import "compress/gzip"
import "compress/zlib"
import "time"

var root_folder *string // TODO: Find a way to be cleaner !
var uses_gzip *bool

const serverUA = "Alexis/0.2"
const fs_maxbufsize = 4096 // 4096 bits = default page size on OSX

/* Go is the first programming language with a templating engine embeddeed
 * but with no min function. */
func min(x int64, y int64) int64 {
    if x < y {
        return x
    }
    return y
}

func main() {
    // Get current working directory to get the file from it
    cwd, err := os.Getwd()
    if err != nil {
        fmt.Printf("Error while getting current directory.")
        return
    }

    // Command line parsing
    bind := flag.String("bind", ":1718", "Bind address")
    root_folder = flag.String("root", cwd, "Root folder")
    uses_gzip = flag.Bool("gzip", true, "Enables gzip/zlib compression")

    flag.Parse()

    http.Handle("/", http.HandlerFunc(handleFile))

    fmt.Printf("Sharing %s on %s ...\n", *root_folder, *bind)
    http.ListenAndServe((*bind), nil)
}

// Manages directory listings
type dirlisting struct {
    Name           string
    Children_dir   []string
    Children_files []string
    ServerUA       string
}

func copyToArray(src *list.List) []string {
    dst := make([]string, src.Len())

    i := 0
    for e := src.Front(); e != nil; e = e.Next() {
        dst[i] = e.Value.(string)
        i = i + 1
    }

    return dst
}

func handleDirectory(f *os.File, w http.ResponseWriter, req *http.Request) {
    names, _ := f.Readdir(-1)

    // First, check if there is any index in this folder.
    for _, val := range names {
        if val.Name() == "index.html" {
            serveFile(path.Join(f.Name(), "index.html"), w, req)
            return
        }
    }

    // Otherwise, generate folder content.
    children_dir_tmp := list.New()
    children_files_tmp := list.New()

    for _, val := range names {
        if val.Name()[0] == '.' {
            continue
        } // Remove hidden files from listing

        if val.IsDir() {
            children_dir_tmp.PushBack(val.Name())
        } else {
            children_files_tmp.PushBack(val.Name())
        }
    }

    // And transfer the content to the final array structure
    children_dir := copyToArray(children_dir_tmp)
    children_files := copyToArray(children_files_tmp)

    tpl, err := template.New("tpl").Parse(dirlisting_tpl)
    if err != nil {
        http.Error(w, "500 Internal Error : Error while generating directory listing.", 500)
        fmt.Println(err)
        return
    }

    data := dirlisting{Name: req.URL.Path, ServerUA: serverUA,
        Children_dir: children_dir, Children_files: children_files}

    err = tpl.Execute(w, data)
    if err != nil {
        fmt.Println(err)
    }
}

func serveFile(filepath string, w http.ResponseWriter, req *http.Request) {
    // Opening the file handle
    f, err := os.Open(filepath)
    if err != nil {
        http.Error(w, "404 Not Found : Error while opening the file.", 404)
        return
    }

    defer f.Close()

    // Checking if the opened handle is really a file
    statinfo, err := f.Stat()
    if err != nil {
        http.Error(w, "500 Internal Error : stat() failure.", 500)
        return
    }

    if statinfo.IsDir() { // If it's a directory, open it !
        handleDirectory(f, w, req)
        return
    }

    if (statinfo.Mode() &^ 07777) == os.ModeSocket { // If it's a socket, forbid it !
        http.Error(w, "403 Forbidden : you can't access this resource.", 403)
        return
    }

    // Manages If-Modified-Since and add Last-Modified (taken from Golang code)
    if t, err := time.Parse(http.TimeFormat, req.Header.Get("If-Modified-Since")); err == nil && statinfo.ModTime().Unix() <= t.Unix() {
        w.WriteHeader(http.StatusNotModified)
        return
    }
    w.Header().Set("Last-Modified", statinfo.ModTime().Format(http.TimeFormat))

    // Content-Type handling
    query, err := url.ParseQuery(req.URL.RawQuery)

    if err == nil && len(query["dl"]) > 0 { // The user explicitedly wanted to download the file (Dropbox style!)
        w.Header().Set("Content-Type", "application/octet-stream")
    } else {
        // Fetching file's mimetype and giving it to the browser
        if mimetype := mime.TypeByExtension(path.Ext(filepath)); mimetype != "" {
            w.Header().Set("Content-Type", mimetype)
        } else {
            w.Header().Set("Content-Type", "application/octet-stream")
        }
    }

    // Manage Content-Range (TODO: Manage end byte and multiple Content-Range)
    if req.Header.Get("Range") != "" {
        start_byte := parseRange(req.Header.Get("Range"))

        if start_byte < statinfo.Size() {
            f.Seek(start_byte, 0)
        } else {
            start_byte = 0
        }

        w.Header().Set("Content-Range",
            fmt.Sprintf("bytes %d-%d/%d", start_byte, statinfo.Size()-1, statinfo.Size()))
    }

    // Manage gzip/zlib compression
    output_writer := w.(io.Writer)

    is_compressed_reply := false

    if (*uses_gzip) == true && req.Header.Get("Accept-Encoding") != "" {
        encodings := parseCSV(req.Header.Get("Accept-Encoding"))

        for _, val := range encodings {
            if val == "gzip" {
                w.Header().Set("Content-Encoding", "gzip")
                output_writer = gzip.NewWriter(w)

                is_compressed_reply = true

                break
            } else if val == "deflate" {
                w.Header().Set("Content-Encoding", "deflate")
                output_writer = zlib.NewWriter(w)

                is_compressed_reply = true

                break
            }
        }
    }

    if !is_compressed_reply {
        // Add Content-Length
        w.Header().Set("Content-Length", strconv.FormatInt(statinfo.Size(), 10))
    }

    // Stream data out !
    buf := make([]byte, min(fs_maxbufsize, statinfo.Size()))
    n := 0
    for err == nil {
        n, err = f.Read(buf)
        output_writer.Write(buf[0:n])
    }

    // Closes current compressors
    switch output_writer.(type) {
    case *gzip.Writer:
        output_writer.(*gzip.Writer).Close()
    case *zlib.Writer:
        output_writer.(*zlib.Writer).Close()
    }

    f.Close()
}

func handleFile(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Server", serverUA)

    filepath := path.Join((*root_folder), path.Clean(req.URL.Path))
    serveFile(filepath, w, req)

    fmt.Printf("\"%s %s %s\" \"%s\" \"%s\"\n",
        req.Method,
        req.URL.String(),
        req.Proto,
        req.Referer(),
        req.UserAgent()) // TODO: Improve this crappy logging
}

func parseCSV(data string) []string {
    splitted := strings.SplitN(data, ",", -1)

    data_tmp := make([]string, len(splitted))

    for i, val := range splitted {
        data_tmp[i] = strings.TrimSpace(val)
    }

    return data_tmp
}

func parseRange(data string) int64 {
    stop := (int64)(0)
    part := 0
    for i := 0; i < len(data) && part < 2; i = i + 1 {
        if part == 0 { // part = 0 <=> equal isn't met.
            if data[i] == '=' {
                part = 1
            }

            continue
        }

        if part == 1 { // part = 1 <=> we've met the equal, parse beginning
            if data[i] == ',' || data[i] == '-' {
                part = 2 // part = 2 <=> OK DUDE.
            } else {
                if 48 <= data[i] && data[i] <= 57 { // If it's a digit ...
                    // ... convert the char to integer and add it!
                    stop = (stop * 10) + (((int64)(data[i])) - 48)
                } else {
                    part = 2 // Parsing error! No error needed : 0 = from start.
                }
            }
        }
    }

    return stop
}

const dirlisting_tpl = `<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<!-- Modified from lighttpd directory listing -->
<head>
<title>Index of {{.Name}}</title>
<style type="text/css">
a, a:active {text-decoration: none; color: blue;}
a:visited {color: #48468F;}
a:hover, a:focus {text-decoration: underline; color: red;}
body {background-color: #F5F5F5;}
h2 {margin-bottom: 12px;}
table {margin-left: 12px;}
th, td { font: 90% monospace; text-align: left;}
th { font-weight: bold; padding-right: 14px; padding-bottom: 3px;}
td {padding-right: 14px;}
td.s, th.s {text-align: right;}
div.list { background-color: white; border-top: 1px solid #646464; border-bottom: 1px solid #646464; padding-top: 10px; padding-bottom: 14px;}
div.foot { font: 90% monospace; color: #787878; padding-top: 4px;}
</style>
</head>
<body>
<h2>Index of {{.Name}}</h2>
<div class="list">
<table summary="Directory Listing" cellpadding="0" cellspacing="0">
<thead><tr><th class="n">Name</th><th class="t">Type</th><th class="dl">Options</th></tr></thead>
<tbody>
<tr><td class="n"><a href="../">Parent Directory</a>/</td><td class="t">Directory</td><td class="dl"></td></tr>
{{range .Children_dir}}
<tr><td class="n"><a href="{{.}}/">{{.}}/</a></td><td class="t">Directory</td><td class="dl"></td></tr>
{{end}}
{{range .Children_files}}
<tr><td class="n"><a href="{{.}}">{{.}}</a></td><td class="t">&nbsp;</td><td class="dl"><a href="{{.}}?dl">Download</a></td></tr>
{{end}}
</tbody>
</table>
</div>
<div class="foot">{{.ServerUA}}</div>
</body>
</html>`

Go实战--golang实现静态文件服务器(文件查看,文件上传,文件下载)