package files import ( "bytes" "context" "crypto/sha1" "crypto/sha256" "encoding/hex" "fmt" "hash" "hash/crc32" "io" "net/http" "path/filepath" "strconv" "strings" . "github.com/mickael-kerjean/filestash/server/pkg/core" . "github.com/mickael-kerjean/filestash/server/pkg/kernel" . "github.com/mickael-kerjean/filestash/server/pkg/permissions" . "github.com/mickael-kerjean/filestash/server/pkg/utils" ) func FileSave(ctx *App, res http.ResponseWriter, req *http.Request) { h := res.Header() h.Set("Cache-Control", "no-store") h.Set("Pragma", "no-cache") h.Set("Connection", "Close") if req.Method == http.MethodOptions { h.Set("Accept-Post", "application/offset+octet-stream, application/vnd.filestash.delta.rdiff") return } path, err := PathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { Log.Debug("files::save action=path_builder err=%s", err.Error()) SendErrorResult(res, err) return } if CanEdit(ctx) == false { if CanUpload(ctx) == false { Log.Debug("files::save action=permission_upload err=permission_denied") SendErrorResult(res, ErrPermissionDenied) return } // for user who cannot edit but can upload => we want to ensure there // won't be any overwritten data root, filename := SplitPath(path) entries, err := ctx.Backend.Ls(root) if err != nil { Log.Debug("files::save action=permission_ls err=%s", err.Error()) SendErrorResult(res, ErrPermissionDenied) return } for i := 0; i < len(entries); i++ { if entries[i].Name() == filename { Log.Debug("files::save action=permission_ls err=already_exist") SendErrorResult(res, ErrConflict) return } } } for _, auth := range Hooks.Get.AuthorisationMiddleware() { if err = auth.Save(ctx, path); err != nil { Log.Info("files::save action=middleware err=%s", err.Error()) SendErrorResult(res, ErrNotAuthorized) return } } proto := "" if strings.HasPrefix(req.Header.Get("Content-Type"), "application/vnd.filestash.delta.rdiff") { proto = "rdiff" } else if _, ok := req.Header["Tus-Resumable"]; ok { proto = "tus" } switch proto { case "": handlerClassic(ctx, res, req, path, h) case "tus": handlerTUS(ctx, res, req, path, h) case "rdiff": handlerRDIFF(ctx, res, req, path, h) default: SendErrorResult(res, ErrNotImplemented) } } func handlerClassic(ctx *App, res http.ResponseWriter, req *http.Request, path string, h http.Header) { if req.Method != http.MethodPost { SendErrorResult(res, ErrNotFound) return } since := req.Header.Get("If-Unmodified-Since") if since != "" { expected, err := http.ParseTime(since) if err != nil { Log.Debug("files::save action=precondition err=%s", err.Error()) SendErrorResult(res, ErrNotValid) return } else if finfo, err := ctx.Backend.Stat(path); err == nil && finfo.ModTime().Unix() != expected.Unix() { SendErrorResult(res, NewError("Modified since", http.StatusPreconditionFailed)) return } } err := ctx.Backend.Save(path, req.Body) req.Body.Close() if err != nil { Log.Debug("files::save action=backend_save err=%s", err.Error()) SendErrorResult(res, NewError(err.Error(), 403)) return } if since != "" { if finfo, err := ctx.Backend.Stat(path); err == nil && finfo.ModTime().Unix() > 0 { h.Set("Last-Modified", finfo.ModTime().UTC().Format(http.TimeFormat)) } } SendSuccessResult(res, nil) return } func handlerTUS(ctx *App, res http.ResponseWriter, req *http.Request, path string, h http.Header) { cacheKey := map[string]string{ "path": path, "session": GenerateID(ctx.Session), } if req.Method == http.MethodHead { c := chunkedUploadCache.Get(cacheKey) if c == nil { SendErrorResult(res, ErrNotFound) return } offset, length := c.(*chunkedUpload).Meta() h.Set("Tus-Resumable", "1.0.0") h.Set("Upload-Offset", fmt.Sprintf("%d", offset)) h.Set("Upload-Length", fmt.Sprintf("%d", length)) h.Set("Cache-Control", "no-store") res.WriteHeader(http.StatusNoContent) return } if req.Method == http.MethodPost { if c := chunkedUploadCache.Get(cacheKey); c != nil { chunkedUploadCache.Del(cacheKey) } size, err := strconv.ParseUint(req.Header.Get("Upload-Length"), 10, 0) if err != nil { Log.Debug("files::save::tus action=backend_save step=header_check_post err=%s", err.Error()) SendErrorResult(res, ErrNotValid) return } ctx.Context = context.Background() b, err := ctx.Backend.Init(ctx.Session, ctx) if err != nil { Log.Debug("files::save::tus action=backend_save step=backend_init err=%s", err.Error()) SendErrorResult(res, ErrNotValid) return } uploader := createChunkedUploader(b.Save, path, size) chunkedUploadCache.Set(cacheKey, uploader) h.Set("Tus-Resumable", "1.0.0") h.Set("Content-Length", "0") h.Set("Location", req.URL.String()) res.WriteHeader(http.StatusCreated) return } if req.Method == http.MethodPatch { if req.Header.Get("Content-Type") != "application/offset+octet-stream" { SendErrorResult(res, NewError("Unsupported Media Type", 415)) return } var ( hash hash.Hash expectedChecksum string ) if checksumHeader := req.Header.Get("Upload-Checksum"); checksumHeader != "" { parts := strings.SplitN(checksumHeader, " ", 2) if len(parts) != 2 { SendErrorResult(res, NewError("Bad Request", 400)) return } else if parts[0] == "sha1" { hash = sha1.New() } else if parts[0] == "crc32" { hash = crc32.NewIEEE() } else { SendErrorResult(res, NewError("Bad Request", 400)) return } expectedChecksum = parts[1] } requestOffset, err := strconv.ParseUint(req.Header.Get("Upload-Offset"), 10, 0) if err != nil { Log.Debug("files::save::tus action=backend_save step=header_check_patch err=%s", err.Error()) SendErrorResult(res, ErrNotValid) return } c := chunkedUploadCache.Get(cacheKey) if c == nil { Log.Debug("files::save::tus action=backend_save step=cache_fetch_patch") SendErrorResult(res, NewError("Conflict", 409)) return } uploader := c.(*chunkedUpload) initialOffset, totalSize := uploader.Meta() if initialOffset != requestOffset { Log.Debug("files::save::tus action=uploader.next path=%s err=offset_missmatch", path) SendErrorResult(res, ErrNotValid) return } reader := req.Body if hash != nil { reader = io.NopCloser(io.TeeReader(req.Body, hash)) } if err := uploader.Next(reader); err != nil { Log.Debug("files::save::tus action=uploader.next path=%s err=%s", path, err.Error()) SendErrorResult(res, NewError(err.Error(), 403)) return } if hash != nil && expectedChecksum != hex.EncodeToString(hash.Sum(nil)) { SendErrorResult(res, NewError("Checksum Mismatch", 460)) return } newOffset, _ := uploader.Meta() if newOffset > totalSize { uploader.Close() chunkedUploadCache.Del(cacheKey) Log.Warning("files::save::tus path=%s err=assert_offset size=%d old_offset=%d new_offset=%d", path, totalSize, initialOffset, newOffset) SendErrorResult(res, NewError("aborted - offset larger than total size", 403)) return } else if newOffset == totalSize { if err := uploader.Close(); err != nil { Log.Debug("files::save::tus action=uploader.close err=%s", err.Error()) SendErrorResult(res, ErrNotValid) return } chunkedUploadCache.Del(cacheKey) } h.Set("Tus-Resumable", "1.0.0") h.Set("Upload-Offset", fmt.Sprintf("%d", newOffset)) res.WriteHeader(http.StatusNoContent) return } SendErrorResult(res, ErrNotFound) } func handlerRDIFF(ctx *App, res http.ResponseWriter, req *http.Request, path string, h http.Header) { if req.Method != http.MethodPost { SendErrorResult(res, ErrNotFound) return } basePath := path if hdr := req.Header.Get("X-Copy-Source"); hdr != "" { root, _ := SplitPath(path) basePath = JoinPath(root, filepath.Base(hdr)) } since := req.Header.Get("If-Unmodified-Since") if since != "" { expected, err := http.ParseTime(since) if err != nil { Log.Debug("files::save::rdiff action=precondition err=%s", err.Error()) SendErrorResult(res, ErrNotValid) return } else if finfo, err := ctx.Backend.Stat(basePath); err == nil && finfo.ModTime().Unix() != expected.Unix() { SendErrorResult(res, NewError("Modified since", http.StatusPreconditionFailed)) return } } remote, err := ctx.Backend.Cat(basePath) if err != nil { Log.Debug("files::save::rdiff action=backend_cat err=%s", err.Error()) SendErrorResult(res, ErrNotFound) return } defer remote.Close() root, filename := SplitPath(path) part := root + "." + filename + ".part_" + QuickString(8) hasher := sha256.New() reader, writer := io.Pipe() saved := make(chan error, 1) go func() { saved <- ctx.Backend.Save(part, reader) }() abort := func(err error, status int) { writer.CloseWithError(err) <-saved if err := ctx.Backend.Rm(part); err != nil { Log.Debug("files::save::rdiff action=part_cleanup err=%s", err.Error()) } Log.Debug("files::save::rdiff action=patch err=%s", err.Error()) SendErrorResult(res, NewError(err.Error(), status)) } if err = rdiffPatch(remote, req.Body, io.MultiWriter(writer, hasher)); err != nil { abort(err, 403) return } expected := make([]byte, sha256.Size) if _, err = io.ReadFull(req.Body, expected); err != nil { abort(err, 403) return } writer.Close() if err = <-saved; err != nil { abort(err, 403) return } if bytes.Equal(hasher.Sum(nil), expected) == false { if err := ctx.Backend.Rm(part); err != nil { Log.Debug("files::save::rdiff action=part_cleanup err=%s", err.Error()) } SendErrorResult(res, NewError("Checksum Mismatch", 460)) return } if err = ctx.Backend.Mv(part, path); err != nil { if ctx.Backend.Rm(path) == nil { err = ctx.Backend.Mv(part, path) } } if err != nil { Log.Debug("files::save::rdiff action=commit err=%s", err.Error()) if err := ctx.Backend.Rm(part); err != nil { Log.Debug("files::save::rdiff action=part_cleanup err=%s", err.Error()) } SendErrorResult(res, NewError(err.Error(), 403)) return } if finfo, err := ctx.Backend.Stat(path); err == nil && finfo.ModTime().Unix() > 0 { h.Set("Last-Modified", finfo.ModTime().UTC().Format(http.TimeFormat)) } SendSuccessResult(res, nil) }