mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2024-11-24 19:19:11 +08:00
5ce6bbf076
glide has its own requirements. My previous workaround caused me some code checkin errors. Need to fix this.
302 lines
8.5 KiB
Go
302 lines
8.5 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"mime"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"net/url"
|
|
|
|
"github.com/chrislusf/seaweedfs/weed/glog"
|
|
"github.com/chrislusf/seaweedfs/weed/images"
|
|
"github.com/chrislusf/seaweedfs/weed/operation"
|
|
"github.com/chrislusf/seaweedfs/weed/storage"
|
|
"github.com/chrislusf/seaweedfs/weed/util"
|
|
)
|
|
|
|
var fileNameEscaper = strings.NewReplacer("\\", "\\\\", "\"", "\\\"")
|
|
|
|
func (vs *VolumeServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request) {
|
|
n := new(storage.Needle)
|
|
vid, fid, filename, ext, _ := parseURLPath(r.URL.Path)
|
|
volumeId, err := storage.NewVolumeId(vid)
|
|
if err != nil {
|
|
glog.V(2).Infoln("parsing error:", err, r.URL.Path)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
err = n.ParsePath(fid)
|
|
if err != nil {
|
|
glog.V(2).Infoln("parsing fid error:", err, r.URL.Path)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
glog.V(4).Infoln("volume", volumeId, "reading", n)
|
|
if !vs.store.HasVolume(volumeId) {
|
|
if !vs.ReadRedirect {
|
|
glog.V(2).Infoln("volume is not local:", err, r.URL.Path)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
lookupResult, err := operation.Lookup(vs.GetMasterNode(), volumeId.String())
|
|
glog.V(2).Infoln("volume", volumeId, "found on", lookupResult, "error", err)
|
|
if err == nil && len(lookupResult.Locations) > 0 {
|
|
u, _ := url.Parse(util.NormalizeUrl(lookupResult.Locations[0].PublicUrl))
|
|
u.Path = r.URL.Path
|
|
arg := url.Values{}
|
|
if c := r.FormValue("collection"); c != "" {
|
|
arg.Set("collection", c)
|
|
}
|
|
u.RawQuery = arg.Encode()
|
|
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
|
|
|
|
} else {
|
|
glog.V(2).Infoln("lookup error:", err, r.URL.Path)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
return
|
|
}
|
|
cookie := n.Cookie
|
|
count, e := vs.store.ReadVolumeNeedle(volumeId, n)
|
|
glog.V(4).Infoln("read bytes", count, "error", e)
|
|
if e != nil || count <= 0 {
|
|
glog.V(0).Infoln("read error:", e, r.URL.Path)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
defer n.ReleaseMemory()
|
|
if n.Cookie != cookie {
|
|
glog.V(0).Infoln("request", r.URL.Path, "with unmaching cookie seen:", cookie, "expected:", n.Cookie, "from", r.RemoteAddr, "agent", r.UserAgent())
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
if n.LastModified != 0 {
|
|
w.Header().Set("Last-Modified", time.Unix(int64(n.LastModified), 0).UTC().Format(http.TimeFormat))
|
|
if r.Header.Get("If-Modified-Since") != "" {
|
|
if t, parseError := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); parseError == nil {
|
|
if t.Unix() >= int64(n.LastModified) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
etag := n.Etag()
|
|
if inm := r.Header.Get("If-None-Match"); inm == etag {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
w.Header().Set("Etag", etag)
|
|
|
|
if vs.tryHandleChunkedFile(n, filename, w, r) {
|
|
return
|
|
}
|
|
|
|
if n.NameSize > 0 && filename == "" {
|
|
filename = string(n.Name)
|
|
if ext == "" {
|
|
ext = path.Ext(filename)
|
|
}
|
|
}
|
|
mtype := ""
|
|
if n.MimeSize > 0 {
|
|
mt := string(n.Mime)
|
|
if !strings.HasPrefix(mt, "application/octet-stream") {
|
|
mtype = mt
|
|
}
|
|
}
|
|
|
|
if ext != ".gz" {
|
|
if n.IsGzipped() {
|
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
} else {
|
|
if n.Data, err = operation.UnGzipData(n.Data); err != nil {
|
|
glog.V(0).Infoln("ungzip error:", err, r.URL.Path)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ext == ".png" || ext == ".jpg" || ext == ".gif" {
|
|
width, height := 0, 0
|
|
if r.FormValue("width") != "" {
|
|
width, _ = strconv.Atoi(r.FormValue("width"))
|
|
}
|
|
if r.FormValue("height") != "" {
|
|
height, _ = strconv.Atoi(r.FormValue("height"))
|
|
}
|
|
n.Data, _, _ = images.Resized(ext, n.Data, width, height)
|
|
}
|
|
|
|
if e := writeResponseContent(filename, mtype, bytes.NewReader(n.Data), w, r); e != nil {
|
|
glog.V(2).Infoln("response write error:", e)
|
|
}
|
|
}
|
|
|
|
func (vs *VolumeServer) FaviconHandler(w http.ResponseWriter, r *http.Request) {
|
|
data, err := images.Asset("favicon/favicon.ico")
|
|
if err != nil {
|
|
glog.V(2).Infoln("favicon read error:", err)
|
|
return
|
|
}
|
|
|
|
if e := writeResponseContent("favicon.ico", "image/x-icon", bytes.NewReader(data), w, r); e != nil {
|
|
glog.V(2).Infoln("response write error:", e)
|
|
}
|
|
}
|
|
|
|
func (vs *VolumeServer) tryHandleChunkedFile(n *storage.Needle, fileName string, w http.ResponseWriter, r *http.Request) (processed bool) {
|
|
if !n.IsChunkedManifest() {
|
|
return false
|
|
}
|
|
|
|
chunkManifest, e := operation.LoadChunkManifest(n.Data, n.IsGzipped())
|
|
if e != nil {
|
|
glog.V(0).Infof("load chunked manifest (%s) error: %v", r.URL.Path, e)
|
|
return false
|
|
}
|
|
if fileName == "" && chunkManifest.Name != "" {
|
|
fileName = chunkManifest.Name
|
|
}
|
|
mType := ""
|
|
if chunkManifest.Mime != "" {
|
|
mt := chunkManifest.Mime
|
|
if !strings.HasPrefix(mt, "application/octet-stream") {
|
|
mType = mt
|
|
}
|
|
}
|
|
|
|
w.Header().Set("X-File-Store", "chunked")
|
|
|
|
chunkedFileReader := &operation.ChunkedFileReader{
|
|
Manifest: chunkManifest,
|
|
Master: vs.GetMasterNode(),
|
|
}
|
|
defer chunkedFileReader.Close()
|
|
if e := writeResponseContent(fileName, mType, chunkedFileReader, w, r); e != nil {
|
|
glog.V(2).Infoln("response write error:", e)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func writeResponseContent(filename, mimeType string, rs io.ReadSeeker, w http.ResponseWriter, r *http.Request) error {
|
|
totalSize, e := rs.Seek(0, 2)
|
|
if mimeType == "" {
|
|
if ext := path.Ext(filename); ext != "" {
|
|
mimeType = mime.TypeByExtension(ext)
|
|
}
|
|
}
|
|
if mimeType != "" {
|
|
w.Header().Set("Content-Type", mimeType)
|
|
}
|
|
if filename != "" {
|
|
contentDisposition := "inline"
|
|
if r.FormValue("dl") != "" {
|
|
if dl, _ := strconv.ParseBool(r.FormValue("dl")); dl {
|
|
contentDisposition = "attachment"
|
|
}
|
|
}
|
|
w.Header().Set("Content-Disposition", contentDisposition+`; filename="`+fileNameEscaper.Replace(filename)+`"`)
|
|
}
|
|
w.Header().Set("Accept-Ranges", "bytes")
|
|
if r.Method == "HEAD" {
|
|
w.Header().Set("Content-Length", strconv.FormatInt(totalSize, 10))
|
|
return nil
|
|
}
|
|
rangeReq := r.Header.Get("Range")
|
|
if rangeReq == "" {
|
|
w.Header().Set("Content-Length", strconv.FormatInt(totalSize, 10))
|
|
if _, e = rs.Seek(0, 0); e != nil {
|
|
return e
|
|
}
|
|
_, e = io.Copy(w, rs)
|
|
return e
|
|
}
|
|
|
|
//the rest is dealing with partial content request
|
|
//mostly copy from src/pkg/net/http/fs.go
|
|
ranges, err := parseRange(rangeReq, totalSize)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
|
|
return nil
|
|
}
|
|
if sumRangesSize(ranges) > totalSize {
|
|
// The total number of bytes in all the ranges
|
|
// is larger than the size of the file by
|
|
// itself, so this is probably an attack, or a
|
|
// dumb client. Ignore the range request.
|
|
return nil
|
|
}
|
|
if len(ranges) == 0 {
|
|
return nil
|
|
}
|
|
if len(ranges) == 1 {
|
|
// RFC 2616, Section 14.16:
|
|
// "When an HTTP message includes the content of a single
|
|
// range (for example, a response to a request for a
|
|
// single range, or to a request for a set of ranges
|
|
// that overlap without any holes), this content is
|
|
// transmitted with a Content-Range header, and a
|
|
// Content-Length header showing the number of bytes
|
|
// actually transferred.
|
|
// ...
|
|
// A response to a request for a single range MUST NOT
|
|
// be sent using the multipart/byteranges media type."
|
|
ra := ranges[0]
|
|
w.Header().Set("Content-Length", strconv.FormatInt(ra.length, 10))
|
|
w.Header().Set("Content-Range", ra.contentRange(totalSize))
|
|
w.WriteHeader(http.StatusPartialContent)
|
|
if _, e = rs.Seek(ra.start, 0); e != nil {
|
|
return e
|
|
}
|
|
|
|
_, e = io.CopyN(w, rs, ra.length)
|
|
return e
|
|
}
|
|
// process multiple ranges
|
|
for _, ra := range ranges {
|
|
if ra.start > totalSize {
|
|
http.Error(w, "Out of Range", http.StatusRequestedRangeNotSatisfiable)
|
|
return nil
|
|
}
|
|
}
|
|
sendSize := rangesMIMESize(ranges, mimeType, totalSize)
|
|
pr, pw := io.Pipe()
|
|
mw := multipart.NewWriter(pw)
|
|
w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
|
|
sendContent := pr
|
|
defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
|
|
go func() {
|
|
for _, ra := range ranges {
|
|
part, e := mw.CreatePart(ra.mimeHeader(mimeType, totalSize))
|
|
if e != nil {
|
|
pw.CloseWithError(e)
|
|
return
|
|
}
|
|
if _, e = rs.Seek(ra.start, 0); e != nil {
|
|
pw.CloseWithError(e)
|
|
return
|
|
}
|
|
if _, e = io.CopyN(part, rs, ra.length); e != nil {
|
|
pw.CloseWithError(e)
|
|
return
|
|
}
|
|
}
|
|
mw.Close()
|
|
pw.Close()
|
|
}()
|
|
if w.Header().Get("Content-Encoding") == "" {
|
|
w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
|
|
}
|
|
w.WriteHeader(http.StatusPartialContent)
|
|
_, e = io.CopyN(w, sendContent, sendSize)
|
|
return e
|
|
}
|