From ba2e0240c60873679ebfce6fc7c961b0328f706b Mon Sep 17 00:00:00 2001
From: Lauris BH <lauris@nix.lv>
Date: Wed, 8 Nov 2017 15:04:19 +0200
Subject: [PATCH] Add LFS object verification step after upload (#2868)

* Add LFS object verification step after upload

* Fix file verification condition and small refactor

* Fix URLs

* Remove newline and return status 422 on failed verification

* Better error hadling
---
 modules/lfs/content_store.go | 17 ++++++++++++-
 modules/lfs/server.go        | 48 +++++++++++++++++++++++++++++++++++-
 routers/routes/routes.go     |  1 +
 3 files changed, 64 insertions(+), 2 deletions(-)

diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go
index 3e1b2f345b..895b51b8ba 100644
--- a/modules/lfs/content_store.go
+++ b/modules/lfs/content_store.go
@@ -1,13 +1,14 @@
 package lfs
 
 import (
-	"code.gitea.io/gitea/models"
 	"crypto/sha256"
 	"encoding/hex"
 	"errors"
 	"io"
 	"os"
 	"path/filepath"
+
+	"code.gitea.io/gitea/models"
 )
 
 var (
@@ -82,6 +83,20 @@ func (s *ContentStore) Exists(meta *models.LFSMetaObject) bool {
 	return true
 }
 
+// Verify returns true if the object exists in the content store and size is correct.
+func (s *ContentStore) Verify(meta *models.LFSMetaObject) (bool, error) {
+	path := filepath.Join(s.BasePath, transformKey(meta.Oid))
+
+	fi, err := os.Stat(path)
+	if os.IsNotExist(err) || err == nil && fi.Size() != meta.Size {
+		return false, nil
+	} else if err != nil {
+		return false, err
+	}
+
+	return true, nil
+}
+
 func transformKey(key string) string {
 	if len(key) < 5 {
 		return key
diff --git a/modules/lfs/server.go b/modules/lfs/server.go
index d618d61853..68f2af1519 100644
--- a/modules/lfs/server.go
+++ b/modules/lfs/server.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"path"
 	"regexp"
 	"strconv"
 	"strings"
@@ -15,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+
 	"github.com/dgrijalva/jwt-go"
 	"gopkg.in/macaron.v1"
 )
@@ -66,7 +68,12 @@ type ObjectError struct {
 
 // ObjectLink builds a URL linking to the object.
 func (v *RequestVars) ObjectLink() string {
-	return fmt.Sprintf("%s%s/%s/info/lfs/objects/%s", setting.AppURL, v.User, v.Repo, v.Oid)
+	return setting.AppURL + path.Join(v.User, v.Repo, "info/lfs/objects", v.Oid)
+}
+
+// VerifyLink builds a URL for verifying the object.
+func (v *RequestVars) VerifyLink() string {
+	return setting.AppURL + path.Join(v.User, v.Repo, "info/lfs/verify")
 }
 
 // link provides a structure used to build a hypermedia representation of an HTTP link.
@@ -320,6 +327,40 @@ func PutHandler(ctx *context.Context) {
 	logRequest(ctx.Req, 200)
 }
 
+// VerifyHandler verify oid and its size from the content store
+func VerifyHandler(ctx *context.Context) {
+	if !setting.LFS.StartServer {
+		writeStatus(ctx, 404)
+		return
+	}
+
+	if !ContentMatcher(ctx.Req) {
+		writeStatus(ctx, 400)
+		return
+	}
+
+	rv := unpack(ctx)
+
+	meta, _ := getAuthenticatedRepoAndMeta(ctx, rv, true)
+	if meta == nil {
+		return
+	}
+
+	contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
+	ok, err := contentStore.Verify(meta)
+	if err != nil {
+		ctx.Resp.WriteHeader(500)
+		fmt.Fprintf(ctx.Resp, `{"message":"%s"}`, err)
+		return
+	}
+	if !ok {
+		writeStatus(ctx, 422)
+		return
+	}
+
+	logRequest(ctx.Req, 200)
+}
+
 // Represent takes a RequestVars and Meta and turns it into a Representation suitable
 // for json encoding
 func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload bool) *Representation {
@@ -347,6 +388,11 @@ func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload boo
 		rep.Actions["upload"] = &link{Href: rv.ObjectLink(), Header: header}
 	}
 
+	if upload && !download {
+		// Force client side verify action while gitea lacks proper server side verification
+		rep.Actions["verify"] = &link{Href: rv.VerifyLink(), Header: header}
+	}
+
 	return rep
 }
 
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index f1c9f18489..5a76dddb66 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -681,6 +681,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 				m.Get("/objects/:oid/:filename", lfs.ObjectOidHandler)
 				m.Any("/objects/:oid", lfs.ObjectOidHandler)
 				m.Post("/objects", lfs.PostHandler)
+				m.Post("/verify", lfs.VerifyHandler)
 				m.Any("/*", func(ctx *context.Context) {
 					ctx.Handle(404, "", nil)
 				})