Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix 0 byte uploads #2998

Merged
merged 6 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/fix-0-byte-uploads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Bugfix: Fix 0-byte-uploads

We fixed a problem with 0-byte uploads by using TouchFile instead of going through TUS (decomposedfs and owncloudsql storage drivers only for now).

https://github.com/cs3org/reva/pull/2998
8 changes: 8 additions & 0 deletions internal/grpc/interceptors/eventsmiddleware/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ func LinkRemoved(r *link.RemovePublicShareResponse, req *link.RemovePublicShareR
}
}

// FileTouched converts the response to an event
func FileTouched(r *provider.TouchFileResponse, req *provider.TouchFileRequest, executant *user.UserId) events.FileTouched {
return events.FileTouched{
Executant: executant,
Ref: req.Ref,
}
}

// FileUploaded converts the response to an event
func FileUploaded(r *provider.InitiateFileUploadResponse, req *provider.InitiateFileUploadRequest, executant *user.UserId) events.FileUploaded {
return events.FileUploaded{
Expand Down
4 changes: 4 additions & 0 deletions internal/grpc/interceptors/eventsmiddleware/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ func NewUnary(m map[string]interface{}) (grpc.UnaryServerInterceptor, int, error
ev = SpaceDisabled(v, r, executantID)
}
}
case *provider.TouchFileResponse:
if isSuccess(v) {
ev = FileTouched(v, req.(*provider.TouchFileRequest), executantID)
}
}

if ev != nil {
Expand Down
4 changes: 3 additions & 1 deletion internal/grpc/services/gateway/storageprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,9 @@ func (s *svc) CreateContainer(ctx context.Context, req *provider.CreateContainer
}

func (s *svc) TouchFile(ctx context.Context, req *provider.TouchFileRequest) (*provider.TouchFileResponse, error) {
c, _, err := s.find(ctx, req.Ref)
var c provider.ProviderAPIClient
var err error
c, _, req.Ref, err = s.findAndUnwrap(ctx, req.Ref)
if err != nil {
return &provider.TouchFileResponse{
Status: status.NewStatusFromErrType(ctx, "TouchFile ref="+req.Ref.String(), err),
Expand Down
24 changes: 22 additions & 2 deletions internal/http/services/owncloud/ocdav/tus.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,28 @@ func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http.
}
}

uploadLength, err := strconv.ParseInt(r.Header.Get(net.HeaderUploadLength), 10, 64)
if err != nil {
log.Debug().Err(err).Msg("wrong request")
w.WriteHeader(http.StatusBadRequest)
return
}
if uploadLength == 0 {
tfRes, err := client.TouchFile(ctx, &provider.TouchFileRequest{
Ref: ref,
})
if err != nil {
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if tfRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Interface("status", tfRes.Status).Msg("error touching file")
w.WriteHeader(http.StatusInternalServerError)
return
}
}

opaqueMap := map[string]*typespb.OpaqueEntry{
net.HeaderUploadLength: {
Decoder: "plain",
Expand Down Expand Up @@ -228,14 +250,12 @@ func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http.
// for creation-with-upload extension forward bytes to dataprovider
// TODO check this really streams
if r.Header.Get(net.HeaderContentType) == "application/offset+octet-stream" {

length, err := strconv.ParseInt(r.Header.Get(net.HeaderContentLength), 10, 64)
if err != nil {
log.Debug().Err(err).Msg("wrong request")
w.WriteHeader(http.StatusBadRequest)
return
}

var httpRes *http.Response

httpReq, err := rhttp.NewRequest(ctx, http.MethodPatch, ep, r.Body)
Expand Down
13 changes: 13 additions & 0 deletions pkg/events/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ func (FileUploaded) Unmarshal(v []byte) (interface{}, error) {
return e, err
}

// FileTouched is emitted when a file is uploaded
type FileTouched struct {
Executant *user.UserId
Ref *provider.Reference
}

// Unmarshal to fulfill umarshaller interface
func (FileTouched) Unmarshal(v []byte) (interface{}, error) {
e := FileTouched{}
err := json.Unmarshal(v, &e)
return e, err
}

// FileDownloaded is emitted when a file is downloaded
type FileDownloaded struct {
Executant *user.UserId
Expand Down
56 changes: 55 additions & 1 deletion pkg/storage/fs/owncloudsql/owncloudsql.go
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,61 @@ func (fs *owncloudsqlfs) CreateDir(ctx context.Context, ref *provider.Reference)

// TouchFile as defined in the storage.FS interface
func (fs *owncloudsqlfs) TouchFile(ctx context.Context, ref *provider.Reference) error {
return fmt.Errorf("unimplemented: TouchFile")
ip, err := fs.resolve(ctx, ref)
if err != nil {
return err
}

// check permissions of parent dir
parentPerms, err := fs.readPermissions(ctx, filepath.Dir(ip))
if err == nil {
if !parentPerms.InitiateFileUpload {
return errtypes.PermissionDenied("")
}
} else {
if isNotFound(err) {
return errtypes.NotFound(ref.Path)
}
return errors.Wrap(err, "owncloudsql: error reading permissions")
}

_, err = os.Create(ip)
if err != nil {
if os.IsNotExist(err) {
return errtypes.NotFound(ref.Path)
}
// FIXME we also need already exists error, webdav expects 405 MethodNotAllowed
return errors.Wrap(err, "owncloudsql: error creating file "+fs.toStoragePath(ctx, filepath.Dir(ip)))
}

if err = os.Chmod(ip, 0700); err != nil {
return errors.Wrap(err, "owncloudsql: error setting file permissions on "+fs.toStoragePath(ctx, filepath.Dir(ip)))
}

fi, err := os.Stat(ip)
if err != nil {
return err
}
mtime := time.Now().Unix()

data := map[string]interface{}{
"path": fs.toDatabasePath(ip),
"etag": calcEtag(ctx, fi),
"mimetype": mime.Detect(false, ip),
"permissions": int(conversions.RoleFromResourcePermissions(parentPerms).OCSPermissions()), // inherit permissions of parent
"mtime": mtime,
"storage_mtime": mtime,
}
storageID, err := fs.getStorage(ctx, ip)
if err != nil {
return err
}
_, err = fs.filecache.InsertOrUpdate(ctx, storageID, data, false)
if err != nil {
return err
}

return fs.propagate(ctx, filepath.Dir(ip))
}

func (fs *owncloudsqlfs) CreateReference(ctx context.Context, sp string, targetURI *url.URL) error {
Expand Down
36 changes: 34 additions & 2 deletions pkg/storage/utils/decomposedfs/decomposedfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ package decomposedfs

import (
"context"
"fmt"
"io"
"net/url"
"os"
Expand Down Expand Up @@ -78,6 +77,7 @@ type Tree interface {
ListFolder(ctx context.Context, node *node.Node) ([]*node.Node, error)
// CreateHome(owner *userpb.UserId) (n *node.Node, err error)
CreateDir(ctx context.Context, node *node.Node) (err error)
TouchFile(ctx context.Context, node *node.Node) error
// CreateReference(ctx context.Context, node *node.Node, targetURI *url.URL) error
Move(ctx context.Context, oldNode *node.Node, newNode *node.Node) (err error)
Delete(ctx context.Context, node *node.Node) (err error)
Expand Down Expand Up @@ -337,7 +337,39 @@ func (fs *Decomposedfs) CreateDir(ctx context.Context, ref *provider.Reference)

// TouchFile as defined in the storage.FS interface
func (fs *Decomposedfs) TouchFile(ctx context.Context, ref *provider.Reference) error {
return fmt.Errorf("unimplemented: TouchFile")
parentRef := &provider.Reference{
ResourceId: ref.ResourceId,
Path: path.Dir(ref.Path),
}

// verify parent exists
parent, err := fs.lu.NodeFromResource(ctx, parentRef)
if err != nil {
return errtypes.InternalError(err.Error())
}
if !parent.Exists {
return errtypes.NotFound(parentRef.Path)
}

n, err := fs.lu.NodeFromResource(ctx, ref)
if err != nil {
return errtypes.InternalError(err.Error())
}
ok, err := fs.p.HasPermission(ctx, n, func(rp *provider.ResourcePermissions) bool {
return rp.InitiateFileUpload
})
switch {
case err != nil:
return errtypes.InternalError(err.Error())
case !ok:
return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name))
}

// check lock
if err := n.CheckLock(ctx); err != nil {
return err
}
return fs.tp.TouchFile(ctx, n)
}

// CreateReference creates a reference as a node folder with the target stored in extended attributes
Expand Down
14 changes: 14 additions & 0 deletions pkg/storage/utils/decomposedfs/mocks/Tree.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions pkg/storage/utils/decomposedfs/tree/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"io"
"io/fs"
iofs "io/fs"
"os"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -228,6 +229,49 @@ func (t *Tree) GetMD(ctx context.Context, n *node.Node) (os.FileInfo, error) {
return md, nil
}

// TouchFile creates a new empty file
func (t *Tree) TouchFile(ctx context.Context, n *node.Node) error {
if n.Exists {
return errtypes.AlreadyExists(n.ID)
}

if n.ID == "" {
n.ID = uuid.New().String()
}

nodePath := n.InternalPath()
if err := os.MkdirAll(filepath.Dir(nodePath), 0700); err != nil {
return errors.Wrap(err, "Decomposedfs: error creating node")
}
_, err := os.Create(nodePath)
if err != nil {
return errors.Wrap(err, "Decomposedfs: error creating node")
}

err = n.WriteAllNodeMetadata()
if err != nil {
return err
}

// link child name to parent if it is new
childNameLink := filepath.Join(n.ParentInternalPath(), n.Name)
var link string
link, err = os.Readlink(childNameLink)
if err == nil && link != "../"+n.ID {
if err = os.Remove(childNameLink); err != nil {
return errors.Wrap(err, "Decomposedfs: could not remove symlink child entry")
}
}
if errors.Is(err, iofs.ErrNotExist) || link != "../"+n.ID {
relativeNodePath := filepath.Join("../../../../../", lookup.Pathify(n.ID, 4, 2))
if err = os.Symlink(relativeNodePath, childNameLink); err != nil {
return errors.Wrap(err, "Decomposedfs: could not symlink child entry")
}
}

return t.Propagate(ctx, n)
}

// CreateDir creates a new directory entry in the tree
func (t *Tree) CreateDir(ctx context.Context, n *node.Node) (err error) {

Expand Down
19 changes: 19 additions & 0 deletions pkg/storage/utils/decomposedfs/tree/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,25 @@ var _ = Describe("Tree", func() {
Expect(err).ToNot(HaveOccurred())
})

Describe("TouchFile", func() {
It("creates a file inside", func() {
ref := &provider.Reference{
ResourceId: env.SpaceRootRes,
Path: "emptydir/newFile",
}
fileToBeCreated, err := env.Lookup.NodeFromResource(env.Ctx, ref)
Expect(err).ToNot(HaveOccurred())
Expect(fileToBeCreated.Exists).To(BeFalse())

err = t.TouchFile(env.Ctx, fileToBeCreated)
Expect(err).ToNot(HaveOccurred())

existingFile, err := env.Lookup.NodeFromResource(env.Ctx, ref)
Expect(err).ToNot(HaveOccurred())
Expect(existingFile.Exists).To(BeTrue())
})
})

Context("that was deleted", func() {
var (
trashPath string
Expand Down