From 64265d39f79b340ca7c71fe215af1f43ffbc2b9e Mon Sep 17 00:00:00 2001
From: qwertyforce <44163887+qwertyforce@users.noreply.github.com>
Date: Mon, 14 Sep 2020 23:11:10 +0300
Subject: [PATCH] reverse image search
---
components/AppBar.tsx | 6 ++
package-lock.json | 138 +++++++++++++++++++++++++++++
package.json | 3 +
pages/reverse_search.tsx | 51 +++++++++++
pages/show.tsx | 61 +++++++++++++
server/helpers/db_ops.ts | 53 +++++++++--
server/index.ts | 6 +-
server/routes/import_from_derpi.ts | 2 +-
server/routes/reverse_search.ts | 33 +++++++
9 files changed, 342 insertions(+), 11 deletions(-)
create mode 100644 pages/reverse_search.tsx
create mode 100644 pages/show.tsx
create mode 100644 server/routes/reverse_search.ts
diff --git a/components/AppBar.tsx b/components/AppBar.tsx
index 0976397..0742399 100644
--- a/components/AppBar.tsx
+++ b/components/AppBar.tsx
@@ -6,6 +6,9 @@ import Typography from '@material-ui/core/Typography';
import SearchIcon from '@material-ui/icons/Search';
import InputBase from '@material-ui/core/InputBase';
import Link from './Link'
+import ImageSearchIcon from '@material-ui/icons/ImageSearch';
+import { IconButton } from '@material-ui/core';
+
import { useRouter } from 'next/router'
const useStyles = makeStyles((theme) => ({
@@ -94,6 +97,9 @@ export default function DenseAppBar() {
inputProps={{ 'aria-label': 'search' }}
/>
+
+
+
diff --git a/package-lock.json b/package-lock.json
index a942c4c..ecc8fce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2787,6 +2787,15 @@
"@types/node": "*"
}
},
+ "@types/multer": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.4.tgz",
+ "integrity": "sha512-wdfkiKBBEMTODNbuF3J+qDDSqJxt50yB9pgDiTcFew7f97Gcc7/sM4HR66ofGgpJPOALWOqKAch4gPyqEXSkeQ==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*"
+ }
+ },
"@types/node": {
"version": "14.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.2.tgz",
@@ -3416,6 +3425,11 @@
}
}
},
+ "append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
+ },
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@@ -3588,6 +3602,11 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
+ "attr-accept": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+ "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
+ },
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -4181,6 +4200,38 @@
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
},
+ "busboy": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
+ "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
+ "requires": {
+ "dicer": "0.2.5",
+ "readable-stream": "1.1.x"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ }
+ }
+ },
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -5204,6 +5255,38 @@
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
"dev": true
},
+ "dicer": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
+ "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
+ "requires": {
+ "readable-stream": "1.1.x",
+ "streamsearch": "0.1.2"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ }
+ }
+ },
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -6343,6 +6426,21 @@
"flat-cache": "^2.0.1"
}
},
+ "file-selector": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.13.tgz",
+ "integrity": "sha512-T2efCBY6Ps+jLIWdNQsmzt/UnAjKOEAlsZVdnQztg/BtAZGNL4uX1Jet9cMM8gify/x4CSudreji2HssGBNVIQ==",
+ "requires": {
+ "tslib": "^2.0.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+ "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+ }
+ }
+ },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -9395,6 +9493,16 @@
"object-visit": "^1.0.0"
}
},
+ "material-ui-dropzone": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/material-ui-dropzone/-/material-ui-dropzone-3.4.0.tgz",
+ "integrity": "sha512-c+H0+dQ65+252KJA3oIfcOXou78/+1nHAAakSZxmN9TudtZMoWnXXuUc8JRdM2zCy5p9kopUw5rG9o4/6ISPig==",
+ "requires": {
+ "@babel/runtime": "^7.4.4",
+ "clsx": "^1.0.2",
+ "react-dropzone": "^10.2.1"
+ }
+ },
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -9719,6 +9827,21 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
+ "multer": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz",
+ "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==",
+ "requires": {
+ "append-field": "^1.0.0",
+ "busboy": "^0.2.11",
+ "concat-stream": "^1.5.2",
+ "mkdirp": "^0.5.1",
+ "object-assign": "^4.1.1",
+ "on-finished": "^2.3.0",
+ "type-is": "^1.6.4",
+ "xtend": "^4.0.0"
+ }
+ },
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
@@ -11512,6 +11635,16 @@
"scheduler": "^0.19.1"
}
},
+ "react-dropzone": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.2.tgz",
+ "integrity": "sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==",
+ "requires": {
+ "attr-accept": "^2.0.0",
+ "file-selector": "^0.1.12",
+ "prop-types": "^15.7.2"
+ }
+ },
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -12755,6 +12888,11 @@
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
},
+ "streamsearch": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+ "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+ },
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
diff --git a/package.json b/package.json
index 25b090f..7b101fb 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,9 @@
"express-session": "^1.17.1",
"express-validator": "^6.6.1",
"imghash": "0.0.7",
+ "material-ui-dropzone": "^3.4.0",
"mongodb": "^3.6.1",
+ "multer": "^1.4.2",
"next": "latest",
"nodemailer": "^6.4.11",
"pm2": "^4.4.1",
@@ -45,6 +47,7 @@
"@types/express-session": "^1.17.0",
"@types/grecaptcha": "^3.0.1",
"@types/mongodb": "^3.5.27",
+ "@types/multer": "^1.4.4",
"@types/node": "^14.6.2",
"@types/nodemailer": "^6.4.0",
"@types/react": "^16.9.48",
diff --git a/pages/reverse_search.tsx b/pages/reverse_search.tsx
new file mode 100644
index 0000000..84b7a7f
--- /dev/null
+++ b/pages/reverse_search.tsx
@@ -0,0 +1,51 @@
+import React, { useState } from 'react';
+import Box from '@material-ui/core/Box';
+import AppBar from '../components/AppBar'
+import { DropzoneArea } from 'material-ui-dropzone';
+import Button from '@material-ui/core/Button';
+import config from '../config/config'
+import axios from "axios"
+import { useRouter } from 'next/router'
+
+export default function ReverseSearch() {
+ const router = useRouter()
+ const [Files, setFiles] = useState([]);
+ const send_image = (token: string) => {
+ const formData = new FormData();
+ formData.append("image", Files[0]);
+ formData.append("g-recaptcha-response", token);
+ axios(`/reverse_search`, {
+ method: "post",
+ data: formData,
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ }).then((resp) => {
+ router.push("/show?ids="+resp.data.ids)
+ }).catch((err) => {
+ console.log(err)
+ })
+ }
+ const _send_image = () => {
+ /*global grecaptcha*/ // defined in public/index.html
+ grecaptcha.ready(function () {
+ grecaptcha.execute(config.recaptcha_site_key, { action: 'login' }).then(function (token) {
+ send_image(token)
+ });
+ })
+ }
+ return (
+
+
+
+ setFiles((files as never))}
+ />
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/pages/show.tsx b/pages/show.tsx
new file mode 100644
index 0000000..54a9324
--- /dev/null
+++ b/pages/show.tsx
@@ -0,0 +1,61 @@
+import React from "react";
+import Gallery from "react-photo-gallery";
+import { makeStyles } from '@material-ui/core/styles';
+import config from '../config/config'
+import AppBar from '../components/AppBar'
+import db_ops from '../server/helpers/db_ops'
+import { useRouter } from 'next/router'
+import Photo from '../components/Photo'
+import ErrorPage from 'next/error'
+const useStyles = makeStyles(() => ({
+ pagination:{
+ display:"flex",
+ justifyContent:'center'
+ }
+}));
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export default function Show(props:any){
+ const classes = useStyles();
+ if (props.err) {
+ return
+ }
+ return (
+
+
+ {/*
+ // @ts-ignore */ }
+
{/* FIX THIS SHIT */}
+
+
+ )
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function getServerSideProps(context: any) {
+ if (context.query.ids) {
+ const ids = context.query.ids.split(',')
+ const images:Array>=[]
+ for(const id of ids){
+ const img_data=await db_ops.image_ops.find_image_by_id(parseInt(id))
+ images.push(img_data[0])
+ }
+ const photos=[]
+ for (const image of images){
+ photos.push({
+ src: `${config.domain}/images/${image.id}.${image.file_ext}`,
+ key: `${config.domain}/image/${image.id}`,
+ width: image.width,
+ height: image.height
+ })
+ }
+ return {
+ props: {
+ photos: photos
+ }
+ }
+ }
+ return {
+ props: { err: true }, // will be passed to the page component as props
+ }
+}
\ No newline at end of file
diff --git a/server/helpers/db_ops.ts b/server/helpers/db_ops.ts
index 3a98efc..c4ad632 100644
--- a/server/helpers/db_ops.ts
+++ b/server/helpers/db_ops.ts
@@ -91,8 +91,36 @@ async function generate_id() {
});
return id;
}
+/////////////////////////////////////////////////IMAGE SEARCH OPS
+async function get_all_phash_distances(){
+ const phash_distances = findDocuments("img_search", {})
+ return phash_distances
+}
+async function get_phash_distances_by_image_id(id:number){
+ const phash_distances = findDocuments("img_search", {id:id})
+ return phash_distances
+}
+
+async function add_image_to_image_search(id:number, phash_dist:Array>){
+ insertDocuments("img_search", [{
+ id:id,
+ phash_dist:phash_dist
+ }])
+}
+async function update_phash_dist_by_id(id:number, phash_dist:Array>){
+ updateDocument("images", {id: id},{phash_dist:phash_dist})
+}
+
+////////////////////////////////////////////////
+
/////////////////////////////////////////////////IMAGES OPS
+async function get_ids_and_phashes(){
+ const collection = client.db(db_main).collection("images");
+ const data = collection.aggregate([{ $project : { id : 1, phash : 1,_id : 0} }]).toArray()
+ return data
+}
+
async function update_image_data_by_id(id:number,update:Record){
updateDocument("images", {id: id},update)
}
@@ -274,15 +302,22 @@ async function create_new_user_not_activated(email:string, pass:string, token:st
/////////////////////////////////////////////////////////
export default {
- image_ops:{
- add_image,
- get_all_images,
- find_image_by_id,
- get_max_image_id,
- find_images_by_tags,
- find_image_by_phash,
- find_image_by_sha512,
- update_image_data_by_id
+ image_ops: {
+ add_image,
+ get_all_images,
+ find_image_by_id,
+ get_max_image_id,
+ find_images_by_tags,
+ get_ids_and_phashes,
+ find_image_by_phash,
+ find_image_by_sha512,
+ update_image_data_by_id
+ },
+ image_search:{
+ get_all_phash_distances,
+ update_phash_dist_by_id,
+ add_image_to_image_search,
+ get_phash_distances_by_image_id,
},
password_recovery:{
update_user_password_by_id,
diff --git a/server/index.ts b/server/index.ts
index dc4a296..b0dce4d 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -7,6 +7,7 @@ import connectMongo from 'connect-mongo';
const MongoStore = connectMongo(session);
import rateLimit from "express-rate-limit";
import cors from 'cors';
+import multer from 'multer'
//import https from 'https';
//import path from 'path';
import { check } from 'express-validator';
@@ -32,9 +33,12 @@ import forgot_password from './routes/forgot_password';
import activate_account_email from './routes/activate_account_email';
import update_image_data from './routes/update_image_data'
import import_from_derpi from './routes/import_from_derpi'
+import reverse_search from './routes/reverse_search'
next_app.prepare().then(() => {
const app = express()
const api_router=express.Router()
+ const storage = multer.memoryStorage()
+ const upload = multer({ storage: storage,limits:{files:1,fileSize:50000000}}) //50MB
const limiter = rateLimit({
windowMs: 15 * 60, // 15 minutes
max: 200 // limit each IP to w00 requests per windowMs
@@ -84,7 +88,7 @@ next_app.prepare().then(() => {
api_router.get('/auth/github/callback', github_oauth_callback)
api_router.get('/auth/google/callback', google_oauth_callback)
-
+ api_router.post('/reverse_search', [upload.single('image'),recaptcha.middleware.verify], reverse_search)
api_router.post('/update_image_data', update_image_data)
api_router.post('/import_from_derpi', import_from_derpi)
diff --git a/server/routes/import_from_derpi.ts b/server/routes/import_from_derpi.ts
index 91cadad..4d68046 100644
--- a/server/routes/import_from_derpi.ts
+++ b/server/routes/import_from_derpi.ts
@@ -44,7 +44,7 @@ async function import_from_derpi(req: Request, res: Response) {
});
const parsed_author = await parse_author(derpi_data.tags)
const derpi_link = "https://derpibooru.org/images/" + derpi_data.id
- const phash = await imghash.hash(`${PATH_TO_IMAGES}/${image_id}.${derpi_data.format.toLowerCase()}`, 16);
+ const phash = await imghash.hash(image, 16);
await db_ops.image_ops.add_image(image_id, derpi_data.format.toLowerCase(), derpi_data.width, derpi_data.height, parsed_author, derpi_data.size,
derpi_link, derpi_data.upvotes, derpi_data.downvotes, derpi_data.id, derpi_data.created_at,
derpi_data.source_url, derpi_data.tags, derpi_data.wilson_score, derpi_data.sha512_hash, phash, derpi_data.description)
diff --git a/server/routes/reverse_search.ts b/server/routes/reverse_search.ts
new file mode 100644
index 0000000..80d76ce
--- /dev/null
+++ b/server/routes/reverse_search.ts
@@ -0,0 +1,33 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+/* eslint-disable @typescript-eslint/no-var-requires */
+// import db_ops from './../helpers/db_ops'
+import { Request, Response } from 'express';
+import db_ops from '../helpers/db_ops'
+const imghash: any = require('imghash');
+function hamming_distance(str1: string, str2: string) {
+ let distance = 0;
+ for (let i = 0; i < str1.length; i += 1) {
+ if (str1[i] !== str2[i]) {
+ distance += 1;
+ }
+ }
+ return distance;
+}
+async function reverse_search(req: Request, res: Response) {
+ if (req.recaptcha?.error) {
+ return res.status(403).json({
+ message: "Captcha error"
+ })
+ }
+ const phash= await imghash.hash(req.file.buffer,16)
+ const images=await db_ops.image_ops.get_ids_and_phashes()
+ for(let i=0;ia.dist-b.dist)
+ images.length=30
+ const ids=images.map((el)=>el.id)
+ res.json({ids:ids.join(',')})
+}
+
+export default reverse_search;
\ No newline at end of file