From 064f61cc8921b4a652d38ef47601abfec06ab366 Mon Sep 17 00:00:00 2001 From: Damillora Date: Wed, 12 May 2021 01:22:46 +0700 Subject: [PATCH] feat: similarity search --- go.mod | 4 +- go.sum | 9 ++ pkg/app/blob_routes.go | 174 ++++++++++++++++++++----------- pkg/database/blob.go | 4 + pkg/models/item.go | 5 + pkg/models/responses.go | 6 ++ pkg/services/auth.go | 3 +- pkg/services/blob.go | 48 +++++++++ pkg/services/post.go | 4 +- web/app/src/api.js | 12 +++ web/app/src/routes/Post.svelte | 44 +++++++- web/app/src/routes/Upload.svelte | 22 +++- 12 files changed, 264 insertions(+), 71 deletions(-) create mode 100644 pkg/services/blob.go diff --git a/go.mod b/go.mod index bd24f76..be84218 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,16 @@ module github.com/Damillora/Shioriko go 1.16 require ( + github.com/corona10/goimagehash v1.0.3 github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/disintegration/imaging v1.6.2 github.com/gin-contrib/cors v1.3.1 github.com/gin-contrib/static v0.0.1 github.com/gin-gonic/gin v1.7.1 github.com/go-playground/validator/v10 v10.6.0 github.com/google/uuid v1.2.0 - github.com/h2non/bimg v1.1.5 golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf + golang.org/x/image v0.0.0-20210504121937-7319ad40d33e gorm.io/driver/postgres v1.1.0 gorm.io/gorm v1.21.9 ) diff --git a/go.sum b/go.sum index 428b405..d9de515 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/corona10/goimagehash v1.0.3 h1:NZM518aKLmoNluluhfHGxT3LGOnrojrxhGn63DR/CZA= +github.com/corona10/goimagehash v1.0.3/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -47,6 +49,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= @@ -273,6 +277,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -392,6 +398,9 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o= golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk= +golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/pkg/app/blob_routes.go b/pkg/app/blob_routes.go index 9ef39e1..5410776 100644 --- a/pkg/app/blob_routes.go +++ b/pkg/app/blob_routes.go @@ -1,17 +1,27 @@ package app import ( + "encoding/binary" + "image" + _ "image/gif" + "image/jpeg" + _ "image/jpeg" + _ "image/png" "net/http" "os" "path/filepath" + _ "golang.org/x/image/webp" + "github.com/Damillora/Shioriko/pkg/config" "github.com/Damillora/Shioriko/pkg/database" "github.com/Damillora/Shioriko/pkg/middleware" "github.com/Damillora/Shioriko/pkg/models" + "github.com/Damillora/Shioriko/pkg/services" + "github.com/corona10/goimagehash" + "github.com/disintegration/imaging" "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/h2non/bimg" ) func InitializeBlobRoutes(g *gin.Engine) { @@ -67,6 +77,87 @@ func uploadBlob(c *gin.Context) { os.Mkdir(filepath.Join(dataDir, "thumbnail", folder1, folder2), 0755) } + previewFilename := id + ".jpg" + previewFilePath := filepath.Join(dataDir, "preview", folder1, folder2, previewFilename) + thumbnailFilePath := filepath.Join(dataDir, "thumbnail", folder1, folder2, previewFilename) + + fileObj, _ := file.Open() + originalImage, _, err := image.Decode(fileObj) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + } + width := originalImage.Bounds().Dx() + height := originalImage.Bounds().Dy() + + hash, err := goimagehash.PerceptionHash(originalImage) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + return + } + hashInt := hash.GetHash() + + similarPosts, err := services.SimilaritySearch(hashInt) + + hashSlice := make([]byte, 8) + binary.LittleEndian.PutUint64(hashSlice, hashInt) + + previewImage := imaging.Resize(originalImage, 1000, 0, imaging.Lanczos) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + return + } + + thumbnailImage := imaging.Resize(originalImage, 300, 0, imaging.Lanczos) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + return + } + + previewFile, err := os.Create(previewFilePath) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + return + } + thumbnailFile, err := os.Create(thumbnailFilePath) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + return + } + + err = jpeg.Encode(previewFile, previewImage, nil) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + } + err = jpeg.Encode(thumbnailFile, thumbnailImage, nil) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Message: err.Error(), + }) + return + } + filename := id + filepath.Ext(file.Filename) filePath := filepath.Join(dataDir, folder1, folder2, filename) err = c.SaveUploadedFile(file, filePath) @@ -75,60 +166,7 @@ func uploadBlob(c *gin.Context) { Code: http.StatusBadRequest, Message: err.Error(), }) - } - - previewFilename := id + ".webp" - previewFilePath := filepath.Join(dataDir, "preview", folder1, folder2, previewFilename) - thumbnailFilePath := filepath.Join(dataDir, "thumbnail", folder1, folder2, previewFilename) - - buffer, err := bimg.Read(filePath) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) - } - - image := bimg.NewImage(buffer) - metadata, err := image.Metadata() - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) - } - width := metadata.Size.Width - height := metadata.Size.Height - - previewImage, err := image.Resize(1000, 0) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) - } - - thumbnailImage, err := image.Resize(300, 0) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) - } - - err = bimg.Write(previewFilePath, previewImage) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) - } - err = bimg.Write(thumbnailFilePath, thumbnailImage) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse{ - Code: http.StatusBadRequest, - Message: err.Error(), - }) + return } blob := database.Blob{ @@ -138,13 +176,29 @@ func uploadBlob(c *gin.Context) { ThumbnailFilePath: filepath.Join("thumbnail", folder1, folder2, previewFilename), Width: width, Height: height, + Hash1: hashSlice[0:2], + Hash2: hashSlice[2:4], + Hash3: hashSlice[4:6], + Hash4: hashSlice[6:8], } database.DB.Create(&blob) - c.JSON(http.StatusOK, models.BlobResponse{ - ID: id, - Width: width, - Height: height, - }) + if len(similarPosts) > 0 { + c.JSON(http.StatusOK, + models.BlobSimilarResponse{ + ID: id, + Width: width, + Height: height, + Similar: similarPosts, + }) + return + } else { + c.JSON(http.StatusOK, models.BlobResponse{ + ID: id, + Width: width, + Height: height, + }) + return + } } diff --git a/pkg/database/blob.go b/pkg/database/blob.go index a870a28..8387ae3 100644 --- a/pkg/database/blob.go +++ b/pkg/database/blob.go @@ -11,6 +11,10 @@ type Blob struct { ThumbnailFilePath string Width int Height int + Hash1 []byte + Hash2 []byte + Hash3 []byte + Hash4 []byte CreatedAt time.Time UpdatedAt time.Time } diff --git a/pkg/models/item.go b/pkg/models/item.go index 673b199..6c5796d 100644 --- a/pkg/models/item.go +++ b/pkg/models/item.go @@ -17,3 +17,8 @@ type PostListItem struct { ImageThumbnailPath string `json:"thumbnail_path"` Tags []string `json:"tags"` } + +type PostSimilarityListItem struct { + ID string `json:"id"` + Distance int `json:"distance"` +} diff --git a/pkg/models/responses.go b/pkg/models/responses.go index 13ffe12..0c02492 100644 --- a/pkg/models/responses.go +++ b/pkg/models/responses.go @@ -20,6 +20,12 @@ type BlobResponse struct { Height int `json:"height"` } +type BlobSimilarResponse struct { + ID string `json:"id"` + Width int `json:"width"` + Height int `json:"height"` + Similar []PostSimilarityListItem `json:"similar"` +} type PostPaginationResponse struct { CurrentPage int `json:"currentPage"` PostCount int `json:"postCount"` diff --git a/pkg/services/auth.go b/pkg/services/auth.go index 1ee22de..c259a22 100644 --- a/pkg/services/auth.go +++ b/pkg/services/auth.go @@ -2,7 +2,6 @@ package services import ( "errors" - "log" "time" "github.com/Damillora/Shioriko/pkg/config" @@ -13,7 +12,7 @@ import ( func Login(username string, password string) *database.User { user := GetUserFromUsername(username) - log.Println(user.Username) + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) if err != nil { return nil diff --git a/pkg/services/blob.go b/pkg/services/blob.go new file mode 100644 index 0000000..22fb508 --- /dev/null +++ b/pkg/services/blob.go @@ -0,0 +1,48 @@ +package services + +import ( + "encoding/binary" + + "github.com/Damillora/Shioriko/pkg/database" + "github.com/Damillora/Shioriko/pkg/models" + "github.com/corona10/goimagehash" +) + +func SimilaritySearch(originalHashInt uint64) ([]models.PostSimilarityListItem, error) { + originalHash := goimagehash.NewImageHash(originalHashInt, goimagehash.PHash) + + hashSlice := make([]byte, 8) + binary.LittleEndian.PutUint64(hashSlice, originalHashInt) + + var blobs []database.Blob + database.DB. + Joins("inner join posts on posts.blob_id = blobs.id"). + Where("hash1 = ?", hashSlice[0:2]). + Or("hash2 = ?", hashSlice[2:4]). + Or("hash3 = ?", hashSlice[4:6]). + Or("hash4 = ?", hashSlice[6:8]). + Find(&blobs) + + posts := make([]models.PostSimilarityListItem, 0) + for _, blob := range blobs { + hash2 := append(blob.Hash1, blob.Hash2...) + hash3 := append(hash2, blob.Hash3...) + hash4 := append(hash3, blob.Hash4...) + + hashInt := binary.LittleEndian.Uint64(hash4) + + compareHash := goimagehash.NewImageHash(hashInt, goimagehash.PHash) + + distance, _ := compareHash.Distance(originalHash) + if distance < 1 { + var post database.Post + database.DB.Where("blob_id = ?", blob.ID).Find(&post) + posts = append(posts, models.PostSimilarityListItem{ + ID: post.ID, + Distance: distance, + }) + } + } + return posts, nil + +} diff --git a/pkg/services/post.go b/pkg/services/post.go index 7f9287b..1e45bf2 100644 --- a/pkg/services/post.go +++ b/pkg/services/post.go @@ -1,8 +1,6 @@ package services import ( - "log" - "github.com/Damillora/Shioriko/pkg/database" "github.com/Damillora/Shioriko/pkg/models" "github.com/google/uuid" @@ -18,7 +16,7 @@ func GetPostAll(page int) []database.Post { func GetPostTags(page int, tagSyntax []string) []database.Post { tags, err := ParseReadTags(tagSyntax) - log.Println(tags) + if err != nil { return []database.Post{} } diff --git a/web/app/src/api.js b/web/app/src/api.js index dd31dfe..8d425c6 100644 --- a/web/app/src/api.js +++ b/web/app/src/api.js @@ -102,4 +102,16 @@ export async function postUpdate(id, { source_url, tags }) { } }) return response.data; +} +export async function postDelete({id}) { + const endpoint = url + "/api/post/"+id; + const response = await axios({ + url: endpoint, + method: "DELETE", + headers: { + 'Authorization': 'Bearer ' + current_token, + }, + withCredentials: true, + }) + return response.status == 200; } \ No newline at end of file diff --git a/web/app/src/routes/Post.svelte b/web/app/src/routes/Post.svelte index b6903f6..f87f12f 100644 --- a/web/app/src/routes/Post.svelte +++ b/web/app/src/routes/Post.svelte @@ -1,8 +1,8 @@
@@ -42,6 +55,10 @@ class="button is-primary" to="/post/edit/{post.id}">Edit +

Uploader: {post.uploader} @@ -87,4 +104,27 @@

+ +