diff --git a/.gitignore b/.gitignore index 071a25aa09cfbc796e571c51b76e273ffe1ef65b..0943baf896a6e46bc105cf63b3576035655bde20 100644 --- a/.gitignore +++ b/.gitignore @@ -102,5 +102,5 @@ ENV/ # Custom .idea/ -/logs +logs/ *.sqlite \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 82687a85d4f7bf8a320febb24bebb832e38ee38b..9449565628c898d9e5a522238dc2cd14487d2bd2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,6 +4,7 @@ from logging.handlers import RotatingFileHandler from config import app_config from flask import Flask +from flask_cors import CORS from flask_moment import Moment from flask_sqlalchemy import SQLAlchemy @@ -13,6 +14,9 @@ moment = Moment() def create_app(config): app = Flask(__name__) + # cors + CORS(app, resources={r"/api/v1.0/*": {"origins": "*"}}) + config_name = config if not isinstance(config, str): config_name = os.getenv('FLASK_CONFIG', 'default') diff --git a/app/api/errors.py b/app/api/errors.py index 4167eb4ab492758e2c2aff5e663cd8faead592ad..aa4a6def663bdf47684a85375b9490df8e1aa2b0 100644 --- a/app/api/errors.py +++ b/app/api/errors.py @@ -6,9 +6,7 @@ def error_response(status_code, message=None): payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')} if message: payload['message'] = message - response = jsonify(payload) - response.status_code = status_code - return response + return jsonify(success=False, data=payload, status_code=status_code) def bad_request(message): diff --git a/app/api/users.py b/app/api/users.py index ca9e45d32427c35950ed2dcfd495d2f069913552..acd88ad80d9e292129b34d5fdfcad58876b35b26 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,8 +1,11 @@ from flask import jsonify, request, url_for from app import db -from app.model.models import User +from app.model.models import User, Photo from app.api import bp from app.api.errors import bad_request +from app.utils import FaceNet, file_utils, crypto_utils + +FACE_NET = FaceNet() @bp.route('/users', methods=['GET']) @@ -10,17 +13,38 @@ def get_users(): page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 10, type=int), 100) data = User.to_collection_dict(User.query, page, per_page, 'api.get_users') - return jsonify(success=True, data=data, status=200) + return jsonify(success=True, data=data, status_code=200) -@bp.route('/user', methods=['POST']) +@bp.route('/user/face', methods=['POST']) def create_user(): - data = request.get_json() or {} - if 'name' not in data or 'cell_phone_number' not in data: - return bad_request('must include name and cell_phone_number fields') + data = request.json or {} + if 'name' not in data or 'cell_phone_number' not in data or 'photos' not in data: + return bad_request('Must include name, cell_phone_number and photos fields') + # User + user = User.query.filter_by(name=data['name'], cell_phone_number=data['cell_phone_number']).first() + if user is None: + u = User() + u.from_dict(data) + db.session.add(u) + db.session.commit() + user = u + # Photo + photos = data['photos'] + photo_count = Photo.query.filter_by(id=user.id).count() + for i in range(len(photos)): + embedding = str(FACE_NET.get_embedding(photos[i])) + photo_name = crypto_utils.encrypt( + user.name + '-' + user.cell_phone_number + '-' + str(i + 1 + photo_count)) + file_utils.DOT_WEBP + file_utils.write_webcam_blob(photos[i], photo_name) + p = Photo(name=photo_name, embedding=embedding, user_id=user.id) + db.session.add(p) + db.session.commit() + return jsonify(success=True, data=None, status_code=201) + + +@bp.route('/foo', methods=['POST']) +def foo(): + data = request.json print(data) - user = User() - user.from_dict(data, new_user=True) - # db.session.add(user) - # db.session.commit() - return jsonify(success=True, data=None, status=201) + return jsonify(success=True, data=data, status_code=201) diff --git a/app/model/models.py b/app/model/models.py index 0b531f8cbb2eccaf88bb38f90a2ad6d387841ec9..556f0c0359c37b032bb6ba56c997af63d42ed5cb 100644 --- a/app/model/models.py +++ b/app/model/models.py @@ -30,8 +30,8 @@ class PaginatedAPIMixin(object): class User(PaginatedAPIMixin, db.Model): __tablename__ = 't_user' id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(20), nullable=False) - cell_phone_number = db.Column(db.String(20), nullable=False) + name = db.Column(db.String(512), nullable=False) + cell_phone_number = db.Column(db.String(32), nullable=False) photos = db.relationship('Photo', backref='user', lazy=True) def __repr__(self): @@ -45,12 +45,17 @@ class User(PaginatedAPIMixin, db.Model): } return data + def from_dict(self, data): + for field in ['name', 'cell_phone_number']: + if field in data: + setattr(self, field, data[field]) + class Photo(db.Model): __tablename__ = 't_photo' id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200), nullable=False) embedding = db.Column(db.String(512), nullable=False) - photo_path = db.Column(db.String(200), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('t_user.id'), nullable=False) def __repr__(self): diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5d09a65c430a135f09f8c80f39c9ff95404d3bd9 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1 @@ +from app.utils.face_net import FaceNet diff --git a/app/utils/crypto_utils.py b/app/utils/crypto_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..524d200b2072e41074aeff028d10263a01d8b7df --- /dev/null +++ b/app/utils/crypto_utils.py @@ -0,0 +1,21 @@ +from cryptography.fernet import Fernet + +KEY = b'9cGYWV01y_PG4EF35ebyLmc222uhimfinpUBzTRX97s=' + +F = Fernet(KEY) + + +def encrypt(data: str): + return str(F.encrypt(bytes(data, encoding="utf8")), encoding='utf8') + + +def decrypt(token): + return str(F.decrypt(bytes(token, encoding="utf8")), encoding='utf8') + + +if __name__ == '__main__': + data_1 = 'test1-15922865715-1' + token_1 = encrypt(data_1) + print(token_1) + data_2 = decrypt(token_1) + print(data_2) diff --git a/app/utils/face_net.py b/app/utils/face_net.py new file mode 100644 index 0000000000000000000000000000000000000000..15504190e903c0b055fd07bd3c7eac8ed5affa37 --- /dev/null +++ b/app/utils/face_net.py @@ -0,0 +1,13 @@ +class FaceNet: + __shared_state = {} + + def __init__(self): + self.__dict__ = self.__shared_state + self.state = 'Init' + + def __str__(self): + return self.state + + @staticmethod + def get_embedding(input_image): + return [0.1] * 128 diff --git a/app/utils/file_utils.py b/app/utils/file_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f7aa4d379e2ace3680371058eda6875a0ebfd7a3 --- /dev/null +++ b/app/utils/file_utils.py @@ -0,0 +1,23 @@ +from base64 import b64decode +from pathlib import Path + +FILE_PREFIX = Path(Path(__file__).resolve().drive + '/face-recognition-data') + +DOT_WEBP = '.webp' + + +def write_webcam_blob(blob: str, file_path: str) -> bool: + """Store file to disk, the name is "name-cell_phone_number-index.webp" + :param blob: webp data + :param file_path: username + :return: + """ + if not FILE_PREFIX.exists(): + FILE_PREFIX.mkdir(parents=True) + write_file_path = FILE_PREFIX / file_path + if write_file_path.exists(): + return False + with open(write_file_path, 'wb') as fh: + data = blob.split(',')[1] + fh.write(b64decode(data)) + return True diff --git a/config.py b/config.py index 6ab6cb5ed1dfd5d2ffb3ca58f90c95aa37f4988b..0a576f7736adbcefd103ac1e9432306cbbd06d3c 100644 --- a/config.py +++ b/config.py @@ -20,7 +20,7 @@ class Config: class DevelopmentConfig(Config): DEBUG = True - SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.sqlite') + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') @classmethod def init_app(cls, app): diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..036a9a4e4169eac2e9ccb6584c06cf87824f1cdb --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +from app import create_app + +app = create_app('development') +if __name__ == '__main__': + app.run() diff --git a/requirements.txt b/requirements.txt index d21ef638997b8a8644effd9172611b03b98f8566..bb2e5d6f0aa0bc8e1877aa141f763830294ac8e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ -Werkzeug==0.16.0 +Flask_Migrate==2.5.2 +Flask_Script==2.0.6 +Flask_Moment==0.9.0 Flask==1.1.1 -config==0.4.2 -flask_moment==0.9.0 -Flask-Migrate==2.5.2 -Flask-Script==2.0.6 -flask_sqlalchemy==2.4.1 -python-dotenv==0.10.3 \ No newline at end of file +Werkzeug==0.16.0 +Flask_Cors==3.0.8 +Flask_SQLAlchemy==2.4.1 +cryptography==2.8 +python-dotenv==0.10.3 diff --git a/scheme.sql b/scheme.sql index 28489acbc30d88e4a7b6130f3bcf780229d5c72a..98fce22fc3e31be3287bf78fc3956bb4c3c8077b 100644 --- a/scheme.sql +++ b/scheme.sql @@ -31,8 +31,8 @@ drop table if exists t_photo; create table t_photo ( id int primary key auto_increment, - embedding varchar(512) not null, - photo_path varchar(200) not null, + name varchar(512) not null, + embedding varchar(1024) not null, user_id int, constraint userid foreign key (user_id) references t_user (id) ); diff --git a/tests/test_file_utils.py b/tests/test_file_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..daae1ec336f35905379a5c7b4d03932c1887609f --- /dev/null +++ b/tests/test_file_utils.py @@ -0,0 +1,11 @@ +import unittest +from app.utils import file_utils + + +class FileUtilsTestCase(unittest.TestCase): + + def test_write_webcam_blob(self): + blob = """ + data:image/webp;base64,UklGRugHAABXRUJQVlA4INwHAABQNwCdASq2AIkAPm0uk0YkIqGhL7IreIANiWcAx6Ds0ixVJDwpIce38p356/xGUns55e/u8Z8BEYZSX4JMs0Tm+4aUte7YngLe0DkZyIQBshtxCG/M5F6WKsz3Ovj4vfUyC69UiVezX+eY/hausrSoS4sLHN6Fm1pfTruAUHSZ1HJ83r1WadPVUUS99SmyN7cKVUX0shkusI1owYr40K5RmcF9ksJCTXqUDjIX35IBfu/VRO0o+0FEbT1woPgILHKECfcbra57NbbRzbWBc3A9TGCab7E6d/+IYOxjSWc8vUw5vU8Rhb3Z+QP/DVfN1DeWcm1wJxin6YjdrWmmALCoaGAVmNFsRlwFZjvGIJWUbeq6T877V5gA3/XADvN7HmusuVxSYSmW7FADdYGKGRnCmfh2X7UfS7XSA/sdjUasSSNxcqSK+CTEowYQvd9d05uNTgHAXVy/zzWOELw6etnrapEdvLtEqZqOFyBcsmgwejgz+X4ILcOoTAw1Er9UFki0ytm6+0OHjMS5Mq1tWgcjHcxZldRrs35x+6G9QtY7sWGwhCm0fCc3rsLqgKynFUA0LcA3kUUi3W8xSsKxqxsAEb/QAP7zgmHuuMaa+6tAf6k67lM1J6KUeRJgU3UsTnp5+mn6JERTTxl9p/dGaRFje+jSPm2yj212qRTcUqQdliQz9b4QKkYQiwGpzZvOlCnz+O3XapzTLGNWooDYdi4hPuOGwmHulyg1s0G/iIsZmkXFlTkSTTlSSYomewPysHaZeXC5f7lMhErgjnZdsdWz1f176wwbGl1jjCv33r1QZqnlVRhrTrNJjreEo7QV+/D92+K2YmOtI1iiBamE3ltk9u4QVblHYFELDeq0ru+6/g1goYwy+nOMW33AsqO8F67qgJGO1YbCGQXTueg8nsv+W0HTqEud0+C00r0l6HS2SALK5qi7UK9ZP2yjbj8++nB/dNpW3LBzXZhJv39C05YB6+JTUmu4e9Sg7Rd1/12djMH25EjJnlNUJTPkwQUAYfj+h5ka0ytYBO6p5XhX6IyaQMGU72DUUL6DCQYlmPga0u5H+9AKeWK2pfuQmAgrcqvvGQm/2beRaxF4GSKx3h/50R8DwHT+1fa/8Im+U0AYctFpfrOdSSCakjQxkQ11IcIiVpij3kRrdKtRVmcDIvGo4WH9ZFVE2yBc9jd1tFTod1lr6evkvnatfHf2i4sCIw6VFwg3kO5OmXJd/Rje/7WnZSj30oKrQmi2zBASr2BlQBLfFz3O7/044d7c+OwTJ1V8kdA/Ii3ONJYYMAj4/WPWuLFAXZm58G+adIfrnhylu6W+ukRZXBojkdejgvWTNZjh+P7DQ+kHpLBcp2KTgMF6VtJ/9OxmIzRU9ILxfVNTSbaYq3VO6G5zIE7GaKSVY7D9pvCYEC7VHidJPveqJhdAgABYAFnhvd1j5wsHN5VcBOGW3tRbV6u0INkwHw6Pnneh/xD+X99dAEmSsBzMjRWaGuJ3KujQmC43JMPThw1DjUOPygowu3KaLXeGhC6YC6pyNLRaY2HtoR8Ak91jAO8iF5v+7t/liVJ/+ogEbMTnzE5vljxNeF6O678JXRrNFZYxMIBgwxKgiYXP/miuCTnrCBZRGYCYlYVdDl79FnNZl4JfuogUvGP8Pppu29du2RZpMm9lzhZTsVXrhQqZYYfc+LGeCnexnMKw0PatraqdaS754Edwv2mhL1wwkeHk6xNVqq05n1b8IBK/ErlbzYpeZcr086h4npkB0ptGktMNF1wHmDQwOfIBv89M9knMNxWwa2YeQ/PDcEG62VOKjUB9s/2iPdO+4kRjDssUXaJF2JJrlHjjalUGPoDaZ+xm6Hnn3lPqqkKpSxYaH94lyceRNnoT8/qXcTXaD0xNGeJdMw4FFqhvjN9mhD1Goi5tiU8WG74k3eGEnsHsFpHFfcgLv+xiwZYAxwnMtcmIXahvCAIgkowb4mk0s+1+P/+sC6A2771f/SBeYTisph8LMQkGwGI1Mg9JMVZAV3bOrR0QCwfq58MmLJuc8MfqGam5XQcYns0pD9kni/suXvQubHZFarj+cjR/Sz6Kfktz4FUJl49S4dxGHbN+tmq1i+MHqhh+6oedBmuKUxU5IBmK3IfJwutRRm7dxaZWj8/pQpbAoxzmRHNDnD7+p1dgLe1WmXK2rFmATeKjhhCdtniBlHdM51BgtV62XNpi6NZfl9dCeGlvYPsKHxarNZFuPFY20RSypD8/TDP7Va+JIYNLQJohimp/a/IO7nWhWjmD08nC/rIvkd+WcuXWqY3g9VmtCdOmAc+hembwxmQfI0LjsMu1RT5PM/CFix00jkgfadU1DLF4VSQOM1zgREAdgjfpkaFTkmc9ognZh9BqlfBXt5ogSLRYan8L57GLyIiryyXre43iWjdGb2RT0MUlZlxskhQU0iM4Y6Xm3NXiw6GijdA/qsADEVVkY80gyux9AHRNurfxFrOcH1imMqEkJmfmBe+07EoW0fm2yPNnJwhB/UiCplfNOFIrv4+x883eKVtgkkXeAt7ms8N21DIyAhUhPI1+QW7HkSApj6tW50gIotwLa1NNGmMWC25lP2TeiuH+7D3rPRtzpn6a7NmHqJE3MY/MSV4b7wH69yIjfJ52aFpRlC8E+UmM4d7xRGOByZ1qr+LKLT/wZ0ggkRkAAA== + """ + self.assertTrue(file_utils.write_webcam_blob(blob, 'test1-15922865715-1'))