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 @@
+
+
+
+
+ Are you sure to delete post {post.id}?
+
+
+
{/if}
diff --git a/web/app/src/routes/Upload.svelte b/web/app/src/routes/Upload.svelte
index af73530..a0fd15a 100644
--- a/web/app/src/routes/Upload.svelte
+++ b/web/app/src/routes/Upload.svelte
@@ -1,12 +1,13 @@