package ctrl import ( "encoding/base64" "fmt" "hash/fnv" . "github.com/mickael-kerjean/filestash/server/common" "github.com/mickael-kerjean/filestash/server/model" "io" "net/http" "os" "path/filepath" "strings" "strconv" "time" ) type FileInfo struct { Name string `json:"name"` Type string `json:"type"` Size int64 `json:"size"` Time int64 `json:"time"` } var FileCache AppCache func init() { FileCache = NewAppCache() cachePath := filepath.Join(GetCurrentDir(), TMP_PATH) FileCache.OnEvict(func(key string, value interface{}) { os.RemoveAll(filepath.Join(cachePath, key)) }) } func FileLs(ctx App, res http.ResponseWriter, req *http.Request) { if model.CanRead(&ctx) == false { if model.CanUpload(&ctx) == false { SendErrorResult(res, ErrPermissionDenied) return } SendSuccessResults(res, make([]FileInfo, 0)) return } path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { SendErrorResult(res, err) return } entries, err := ctx.Backend.Ls(path) if err != nil { SendErrorResult(res, err) return } go model.SProc.HintLs(&ctx, path) files := make([]FileInfo, len(entries)) etagger := fnv.New32() etagger.Write([]byte(path + strconv.Itoa(len(entries)))) for i:=0; i range request requires a seeker to work, some backend support it, some don't. 2 strategies: // 1. backend support Seek: use what the current backend gives us // 2. backend doesn't support Seek: build up a cache so that subsequent call don't trigger multiple downloads if req.Header.Get("range") != "" && needToCreateCache == true { if obj, ok := file.(io.Seeker); ok == true { if size, err := obj.Seek(0, io.SeekEnd); err == nil { if _, err = obj.Seek(0, io.SeekStart); err == nil { contentLength = size } } } else { tmpPath := filepath.Join(GetCurrentDir(), filepath.Join(GetCurrentDir(), TMP_PATH), "file_" + QuickString(20) + ".dat") f, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE, os.ModePerm); if err != nil { SendErrorResult(res, err) return } if _, err = io.Copy(f, file); err != nil { f.Close() file.Close() SendErrorResult(res, err) return } f.Close() file.Close() if f, err = os.OpenFile(tmpPath, os.O_RDONLY, os.ModePerm); err != nil { SendErrorResult(res, err) return } FileCache.Set(ctx.Session, tmpPath) if fi, err := f.Stat(); err == nil { contentLength = fi.Size() } file = f } } // Range request: find how much data we need to send var ranges [][]int64 if req.Header.Get("range") != "" { ranges = make([][]int64, 0) for _, r := range strings.Split(strings.TrimPrefix(req.Header.Get("range"), "bytes="), ",") { r = strings.TrimSpace(r) if r == "" { continue } var start int64 = -1 var end int64 = -1 sides := strings.Split(r, "-") if len(sides) == 2 { if start, err = strconv.ParseInt(sides[0], 10, 64); err != nil || start < 0 { start = 0 } if end, err = strconv.ParseInt(sides[1], 10, 64); err != nil || end < start { end = contentLength - 1 } } if start != -1 && end != -1 && end - start >= 0 { ranges = append(ranges, []int64{start, end}) } } } // publish headers if contentLength != -1 { header.Set("Content-Length", fmt.Sprintf("%d", contentLength)) } if header.Get("Content-Security-Policy") == "" { header.Set("Content-Security-Policy", "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'") } header.Set("Accept-Ranges", "bytes") // Send data to the client if req.Method != "HEAD" { if f, ok := file.(io.ReadSeeker); ok && len(ranges) > 0 { if _, err = f.Seek(ranges[0][0], io.SeekStart); err == nil { header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ranges[0][0], ranges[0][1], contentLength)) header.Set("Content-Length", fmt.Sprintf("%d", ranges[0][1] - ranges[0][0] + 1)) res.WriteHeader(http.StatusPartialContent) io.CopyN(res, f, ranges[0][1] - ranges[0][0] + 1) } else { res.WriteHeader(http.StatusRequestedRangeNotSatisfiable) } } else { io.Copy(res, file) } } file.Close() } func FileAccess(ctx App, res http.ResponseWriter, req *http.Request) { allowed := []string{} if model.CanRead(&ctx){ allowed = append(allowed, "GET") } if model.CanEdit(&ctx){ allowed = append(allowed, "PUT") } if model.CanUpload(&ctx){ allowed = append(allowed, "POST") } header := res.Header() header.Set("Allow", strings.Join(allowed, ", ")) SendSuccessResult(res, nil) } func FileSave(ctx App, res http.ResponseWriter, req *http.Request) { if model.CanEdit(&ctx) == false { SendErrorResult(res, NewError("Permission denied", 403)) return } path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { SendErrorResult(res, err) return } file, _, err := req.FormFile("file") if err != nil { SendErrorResult(res, err) return } defer file.Close() err = ctx.Backend.Save(path, file) file.Close() if err != nil { SendErrorResult(res, NewError(err.Error(), 403)) return } go model.SProc.HintLs(&ctx, filepath.Dir(path) + "/") go model.SProc.HintFile(&ctx, path) SendSuccessResult(res, nil) } func FileMv(ctx App, res http.ResponseWriter, req *http.Request) { if model.CanEdit(&ctx) == false { SendErrorResult(res, NewError("Permission denied", 403)) return } from, err := pathBuilder(ctx, req.URL.Query().Get("from")) if err != nil { SendErrorResult(res, err) return } to, err := pathBuilder(ctx, req.URL.Query().Get("to")) if err != nil { SendErrorResult(res, err) return } if from == "" || to == "" { SendErrorResult(res, NewError("missing path parameter", 400)) return } err = ctx.Backend.Mv(from, to) if err != nil { SendErrorResult(res, err) return } go model.SProc.HintRm(&ctx, filepath.Dir(from) + "/") go model.SProc.HintLs(&ctx, filepath.Dir(to) + "/") SendSuccessResult(res, nil) } func FileRm(ctx App, res http.ResponseWriter, req *http.Request) { if model.CanEdit(&ctx) == false { SendErrorResult(res, NewError("Permission denied", 403)) return } path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { SendErrorResult(res, err) return } err = ctx.Backend.Rm(path) if err != nil { SendErrorResult(res, err) return } model.SProc.HintRm(&ctx, path) SendSuccessResult(res, nil) } func FileMkdir(ctx App, res http.ResponseWriter, req *http.Request) { if model.CanUpload(&ctx) == false { SendErrorResult(res, NewError("Permission denied", 403)) return } path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { SendErrorResult(res, err) return } err = ctx.Backend.Mkdir(path) if err != nil { SendErrorResult(res, err) return } go model.SProc.HintLs(&ctx, filepath.Dir(path) + "/") SendSuccessResult(res, nil) } func FileTouch(ctx App, res http.ResponseWriter, req *http.Request) { if model.CanUpload(&ctx) == false { SendErrorResult(res, NewError("Permission denied", 403)) return } path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { SendErrorResult(res, err) return } err = ctx.Backend.Touch(path) if err != nil { SendErrorResult(res, err) return } go model.SProc.HintLs(&ctx, filepath.Dir(path) + "/") SendSuccessResult(res, nil) } func pathBuilder(ctx App, path string) (string, error) { if path == "" { return "", NewError("No path available", 400) } sessionPath := ctx.Session["path"] basePath := filepath.Join(sessionPath, path) if path[len(path)-1:] == "/" && basePath != "/" { basePath += "/" } if strings.HasPrefix(basePath, ctx.Session["path"]) == false { return "", NewError("There's nothing here", 403) } return basePath, nil }