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