From 0067a5915e4f639d222305b6e8b140969b66e393 Mon Sep 17 00:00:00 2001 From: Profitroll <47523801+profitrollgame@users.noreply.github.com> Date: Tue, 20 Dec 2022 01:22:32 +0100 Subject: [PATCH] Migrated from main API --- .gitignore | 157 ++++++++++++++++++++++++++++++++++ config_example.json | 16 ++++ favicon.ico | Bin 0 -> 11343 bytes modules/app.py | 79 +++++++++++++++++ modules/database.py | 33 ++++++++ modules/extensions_loader.py | 47 +++++++++++ modules/hasher.py | 55 ++++++++++++ modules/utils.py | 75 +++++++++++++++++ photos_api.py | 17 ++++ requests/albums.py | 159 +++++++++++++++++++++++++++++++++++ requests/photos.py | 145 ++++++++++++++++++++++++++++++++ requirements.txt | 6 ++ 12 files changed, 789 insertions(+) create mode 100644 .gitignore create mode 100644 config_example.json create mode 100644 favicon.ico create mode 100644 modules/app.py create mode 100644 modules/database.py create mode 100644 modules/extensions_loader.py create mode 100644 modules/hasher.py create mode 100644 modules/utils.py create mode 100644 photos_api.py create mode 100644 requests/albums.py create mode 100644 requests/photos.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e4bb6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,157 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Custom +.vscode +config.json \ No newline at end of file diff --git a/config_example.json b/config_example.json new file mode 100644 index 0000000..cf3d516 --- /dev/null +++ b/config_example.json @@ -0,0 +1,16 @@ +{ + "database": { + "name": "photos", + "host": "127.0.0.1", + "port": 27017, + "user": null, + "password": null + }, + "messages": { + "key_expired": "API key expired", + "key_invalid": "Invalid API key", + "key_valid": "Valid API key", + "bad_request": "Bad request. Read the docs at photos.end-play.xyz/docs", + "ip_blacklisted": "Your IP is blacklisted. Make sure you are using correct API address." + } +} \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c5522aa058c603861e5ffdf9b6b3fee66290ef3b GIT binary patch literal 11343 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 literal 0 HcmV?d00001 diff --git a/modules/app.py b/modules/app.py new file mode 100644 index 0000000..5cdd8f6 --- /dev/null +++ b/modules/app.py @@ -0,0 +1,79 @@ +from os import sep +from fastapi import FastAPI, Security, HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from fastapi.security import APIKeyQuery, APIKeyHeader, APIKeyCookie +from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html +from starlette.status import HTTP_401_UNAUTHORIZED +from fastapi.openapi.models import APIKey + +from modules.utils import configGet, jsonLoad + +app = FastAPI(title="END PLAY Photos", docs_url=None, redoc_url=None, version="2.0") + +api_key_query = APIKeyQuery(name="apikey", auto_error=False) +api_key_header = APIKeyHeader(name="apikey", auto_error=False) +api_key_cookie = APIKeyCookie(name="apikey", auto_error=False) + + +def get_all_api_keys(): + return jsonLoad(f'{configGet("data_location")}{sep}api_keys.json') + +def get_all_expired_keys(): + return jsonLoad(f'{configGet("data_location")}{sep}expired_keys.json') + +def check_project_key(project: str, apikey: APIKey) -> bool: + keys = jsonLoad(f'{configGet("data_location")}{sep}api_keys.json') + if apikey in keys: + if keys[apikey] != []: + if project in keys[apikey]: + return True + else: + return False + else: + return False + else: + return False + + +async def get_api_key( + api_key_query: str = Security(api_key_query), + api_key_header: str = Security(api_key_header), + api_key_cookie: str = Security(api_key_cookie), +): + + keys = get_all_api_keys() + expired = get_all_expired_keys() + + def is_valid(key): + if (key in keys) or (key == "publickey"): + return True + else: + return False + + if is_valid(api_key_query): + return api_key_query + elif is_valid(api_key_header): + return api_key_header + elif is_valid(api_key_cookie): + return api_key_cookie + else: + if (api_key_query in expired) or (api_key_header in expired) or (api_key_cookie in expired): + raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=configGet("key_expired", "messages")) + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.get("/docs", include_in_schema=False) +async def custom_swagger_ui_html(): + return get_swagger_ui_html( + openapi_url=app.openapi_url, # type: ignore + title=app.title + " - Documentation", + swagger_favicon_url="/favicon.ico" + ) + +@app.get("/redoc", include_in_schema=False) +async def custom_redoc_html(): + return get_redoc_html( + openapi_url=app.openapi_url, # type: ignore + title=app.title + " - Documentation", + redoc_favicon_url="/favicon.ico" + ) \ No newline at end of file diff --git a/modules/database.py b/modules/database.py new file mode 100644 index 0000000..ea625de --- /dev/null +++ b/modules/database.py @@ -0,0 +1,33 @@ +from modules.utils import configGet +from pymongo import MongoClient + +db_config = configGet("database") + +if db_config["user"] is not None and db_config["password"] is not None: + con_string = 'mongodb://{0}:{1}@{2}:{3}/{4}'.format( + db_config["user"], + db_config["password"], + db_config["host"], + db_config["port"], + db_config["name"] + ) +else: + con_string = 'mongodb://{0}:{1}/{2}'.format( + db_config["host"], + db_config["port"], + db_config["name"] + ) + +db_client = MongoClient(con_string) + +db = db_client.get_database(name=db_config["name"]) + +collections = db.list_collection_names() + +for collection in ["albums", "photos", "tokens"]: + if not collection in collections: + db.create_collection(collection) + +col_albums = db.get_collection("albums") +col_photos = db.get_collection("photos") +col_tokens = db.get_collection("tokens") \ No newline at end of file diff --git a/modules/extensions_loader.py b/modules/extensions_loader.py new file mode 100644 index 0000000..2fcd295 --- /dev/null +++ b/modules/extensions_loader.py @@ -0,0 +1,47 @@ +from importlib.util import module_from_spec, spec_from_file_location +from os import getcwd, path, walk + +#================================================================================= + +# Import functions +# Took from https://stackoverflow.com/a/57892961 +def get_py_files(src): + cwd = getcwd() # Current Working directory + py_files = [] + for root, dirs, files in walk(src): + for file in files: + if file.endswith(".py"): + py_files.append(path.join(cwd, root, file)) + return py_files + + +def dynamic_import(module_name, py_path): + try: + module_spec = spec_from_file_location(module_name, py_path) + module = module_from_spec(module_spec) # type: ignore + module_spec.loader.exec_module(module) # type: ignore + return module + except SyntaxError: + print(f"Could not load extension {module_name} due to invalid syntax. Check logs/errors.log for details.", flush=True) + return + except Exception as exp: + print(f"Could not load extension {module_name} due to {exp}", flush=True) + return + + +def dynamic_import_from_src(src, star_import = False): + my_py_files = get_py_files(src) + for py_file in my_py_files: + module_name = path.split(py_file)[-1][:-3] + print(f"Importing {module_name} extension...", flush=True) + imported_module = dynamic_import(module_name, py_file) + if imported_module != None: + if star_import: + for obj in dir(imported_module): + globals()[obj] = imported_module.__dict__[obj] + else: + globals()[module_name] = imported_module + print(f"Successfully loaded {module_name} extension", flush=True) + return + +#================================================================================= \ No newline at end of file diff --git a/modules/hasher.py b/modules/hasher.py new file mode 100644 index 0000000..6045bb3 --- /dev/null +++ b/modules/hasher.py @@ -0,0 +1,55 @@ +from modules.database import col_photos +import numpy as np +from numpy.typing import NDArray +from scipy import spatial +import cv2 + +def hash_array_to_hash_hex(hash_array): + # convert hash array of 0 or 1 to hash string in hex + hash_array = np.array(hash_array, dtype = np.uint8) + hash_str = ''.join(str(i) for i in 1 * hash_array.flatten()) + return (hex(int(hash_str, 2))) + +def hash_hex_to_hash_array(hash_hex) -> NDArray: + # convert hash string in hex to hash values of 0 or 1 + hash_str = int(hash_hex, 16) + array_str = bin(hash_str)[2:] + return np.array([i for i in array_str], dtype = np.float32) + +def get_duplicates_cache(album: str) -> dict: + output = {} + for photo in col_photos.find( {"album": album} ): + output[photo["filename"]] = [photo["_id"].__str__(), photo["hash"]] + return output + +async def get_phash(filepath: str) -> str: + img = cv2.imread(filepath) + # resize image and convert to gray scale + img = cv2.resize(img, (64, 64)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + img = np.array(img, dtype = np.float32) + # calculate dct of image + dct = cv2.dct(img) + # to reduce hash length take only 8*8 top-left block + # as this block has more information than the rest + dct_block = dct[: 8, : 8] + # caclulate mean of dct block excluding first term i.e, dct(0, 0) + dct_average = (dct_block.mean() * dct_block.size - dct_block[0, 0]) / (dct_block.size - 1) + # convert dct block to binary values based on dct_average + dct_block[dct_block < dct_average] = 0.0 + dct_block[dct_block != 0] = 1.0 + # store hash value + return hash_array_to_hash_hex(dct_block.flatten()) + +async def get_duplicates(hash: str, album: str) -> list: + duplicates = [] + cache = get_duplicates_cache(album) + for image_name in cache.keys(): + distance = spatial.distance.hamming( + hash_hex_to_hash_array(cache[image_name][1]), + hash_hex_to_hash_array(hash) + ) + print("{0:<30} {1}".format(image_name, distance), flush=True) + if distance <= 0.25: + duplicates.append({"id": cache[image_name][0], "filename": image_name, "difference": distance}) + return duplicates \ No newline at end of file diff --git a/modules/utils.py b/modules/utils.py new file mode 100644 index 0000000..4119c07 --- /dev/null +++ b/modules/utils.py @@ -0,0 +1,75 @@ +from typing import Any, Union +from ujson import loads, dumps, JSONDecodeError +from traceback import print_exc + +# Print to stdout and then to log +def logWrite(message: str, debug: bool = False) -> None: + # save to log file and rotation is to be done + # logAppend(f'{message}', debug=debug) + print(f"{message}", flush=True) + +def jsonLoad(filepath: str) -> Any: + """Load json file + + ### Args: + * filepath (`str`): Path to input file + + ### Returns: + * `Any`: Some json deserializable + """ + with open(filepath, "r", encoding='utf8') as file: + try: + output = loads(file.read()) + except JSONDecodeError: + logWrite(f"Could not load json file {filepath}: file seems to be incorrect!\n{print_exc()}") + raise + except FileNotFoundError: + logWrite(f"Could not load json file {filepath}: file does not seem to exist!\n{print_exc()}") + raise + file.close() + return output + +def jsonSave(contents: Union[list, dict], filepath: str) -> None: + """Save contents into json file + + ### Args: + * contents (`Union[list, dict]`): Some json serializable + * filepath (`str`): Path to output file + """ + try: + with open(filepath, "w", encoding='utf8') as file: + file.write(dumps(contents, ensure_ascii=False, indent=4)) + file.close() + except Exception as exp: + logWrite(f"Could not save json file {filepath}: {exp}\n{print_exc()}") + return + +def configGet(key: str, *args: str) -> Any: + """Get value of the config key + + ### Args: + * key (`str`): The last key of the keys path. + * *args (`str`): Path to key like: dict[args][key]. + + ### Returns: + * `Any`: Value of provided key + """ + this_dict = jsonLoad("config.json") + this_key = this_dict + for dict_key in args: + this_key = this_key[dict_key] + return this_key[key] + +def apiKeyInvalid(obj): + obj.send_response(401) + obj.send_header('Content-type', 'application/json; charset=utf-8') + obj.end_headers() + obj.wfile.write(b'{"code":401, "message": "Invalid API key"}') + return + +def apiKeyExpired(obj): + obj.send_response(403) + obj.send_header('Content-type', 'application/json; charset=utf-8') + obj.end_headers() + obj.wfile.write(b'{"code":403, "message": "API key expired"}') + return \ No newline at end of file diff --git a/photos_api.py b/photos_api.py new file mode 100644 index 0000000..2bd70c0 --- /dev/null +++ b/photos_api.py @@ -0,0 +1,17 @@ +from os import makedirs, sep +from modules.app import app +from modules.utils import * +from modules.extensions_loader import dynamic_import_from_src +from fastapi.responses import FileResponse + +makedirs(f"data{sep}users", exist_ok=True) + + +@app.get("/favicon.ico", response_class=FileResponse, include_in_schema=False) +async def favicon(): + return FileResponse("favicon.ico") + + +#================================================================================= +dynamic_import_from_src("requests", star_import = True) +#================================================================================= \ No newline at end of file diff --git a/requests/albums.py b/requests/albums.py new file mode 100644 index 0000000..a427cef --- /dev/null +++ b/requests/albums.py @@ -0,0 +1,159 @@ +import re +from os import makedirs, rename, sep +from shutil import rmtree +from typing import Union +from modules.utils import configGet +from modules.app import app, check_project_key, get_api_key +from modules.database import col_photos, col_albums +from bson.objectid import ObjectId +from bson.errors import InvalidId + +from fastapi import HTTPException, Depends +from fastapi.responses import UJSONResponse, Response +from fastapi.openapi.models import APIKey +from starlette.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT + +@app.post("/albums", response_class=UJSONResponse, include_in_schema=True) +async def album_create(name: str, title: str, apikey: APIKey = Depends(get_api_key)): + + if (check_project_key("photos", apikey)): + + if re.search(re.compile('^[a-z,0-9,_]*$'), name) is False: + return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Album name can only contain: a-z, 0-9 and _ characters.") + + if 2 > len(name) > 20: + return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Album name must be >2 and <20 characters.") + + if 2 > len(title) > 40: + return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Album title must be >2 and <40 characters.") + + if col_albums.find_one( {"name": name} ) is not None: + return HTTPException(status_code=HTTP_409_CONFLICT, detail=f"Album with name '{name}' already exists.") + + makedirs(f'{configGet("data_location")}{sep}photos{sep}images{sep}{name}', exist_ok=True) + + uploaded = col_albums.insert_one( {"name": name, "title": title} ) + + return UJSONResponse( + { + "id": uploaded.inserted_id.__str__(), + "name": name, + "title": title + } + ) + + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.get("/albums", response_class=UJSONResponse, include_in_schema=True) +async def album_find(q: str, apikey: APIKey = Depends(get_api_key)): + + if (check_project_key("photos", apikey)): + + output = {"results": []} + albums = list(col_albums.find( {"name": re.compile(q)} )) + + for album in albums: + output["results"].append( {"id": album["_id"].__str__(), "name": album["name"]} ) + + return UJSONResponse(output) + + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.patch("/albums/{id}", response_class=UJSONResponse, include_in_schema=True) +async def album_patch(id: str, name: Union[str, None] = None, title: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)): + + if (check_project_key("photos", apikey)): + + try: + album = col_albums.find_one( {"_id": ObjectId(id)} ) + if album is None: + raise InvalidId(id) + except InvalidId: + return HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Could not find an album with such id.") + + if title is not None: + if 2 > len(title) > 40: + return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Album title must be >2 and <40 characters.") + else: + title = album["title"] + + if name is not None: + if re.search(re.compile('^[a-z,0-9,_]*$'), name) is False: + return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Album name can only contain: a-z, 0-9 and _ characters.") + if 2 > len(name) > 20: + return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Album name must be >2 and <20 characters.") + rename(f'{configGet("data_location")}{sep}photos{sep}images{sep}{album["name"]}', f'{configGet("data_location")}{sep}photos{sep}images{sep}{name}') + col_photos.update_many( {"album": album["name"]}, {"$set": {"album": name}} ) + else: + name = album["name"] + + col_albums.update_one( {"_id": ObjectId(id)}, {"$set": {"name": name, "title": title}} ) + + return UJSONResponse( + { + "name": name, + "title": title + } + ) + + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.put("/albums/{id}", response_class=UJSONResponse, include_in_schema=True) +async def album_put(id: str, name: str, title: str, apikey: APIKey = Depends(get_api_key)): + + if (check_project_key("photos", apikey)): + + try: + album = col_albums.find_one( {"_id": ObjectId(id)} ) + if album is None: + raise InvalidId(id) + except InvalidId: + return HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Could not find an album with such id.") + + if re.search(re.compile('^[a-z,0-9,_]*$'), name) is False: + return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Album name can only contain: a-z, 0-9 and _ characters.") + + if 2 > len(name) > 20: + return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Album name must be >2 and <20 characters.") + + if 2 > len(title) > 40: + return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Album title must be >2 and <40 characters.") + + rename(f'{configGet("data_location")}{sep}photos{sep}images{sep}{album["name"]}', f'{configGet("data_location")}{sep}photos{sep}images{sep}{name}') + col_photos.update_many( {"album": album["name"]}, {"$set": {"album": name}} ) + + col_albums.update_one( {"_id": ObjectId(id)}, {"$set": {"name": name, "title": title}} ) + + return UJSONResponse( + { + "name": name, + "title": title + } + ) + + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.delete("/album/{id}", response_class=UJSONResponse, include_in_schema=True) +async def album_delete(id: str, apikey: APIKey = Depends(get_api_key)): + + if (check_project_key("photos", apikey)): + + try: + album = col_albums.find_one_and_delete( {"_id": ObjectId(id)} ) + if album is None: + raise InvalidId(id) + except InvalidId: + return HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Could not find an album with such id.") + + col_photos.delete_many( {"album": album["name"]} ) + + rmtree(f'{configGet("data_location")}{sep}photos{sep}images{sep}{album["name"]}') + + return Response(status_code=HTTP_204_NO_CONTENT) + + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) \ No newline at end of file diff --git a/requests/photos.py b/requests/photos.py new file mode 100644 index 0000000..e748808 --- /dev/null +++ b/requests/photos.py @@ -0,0 +1,145 @@ +import re +from secrets import token_urlsafe +from magic import Magic +from datetime import datetime +from os import makedirs, sep, path, remove +from modules.hasher import get_phash, get_duplicates +from modules.utils import configGet +from modules.app import app, check_project_key, get_api_key +from modules.database import col_photos, col_albums, col_tokens +from bson.objectid import ObjectId +from bson.errors import InvalidId + +from fastapi import HTTPException, Depends, UploadFile +from fastapi.responses import UJSONResponse, Response +from fastapi.openapi.models import APIKey +from starlette.status import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT + +@app.post("/albums/{album}/photos", response_class=UJSONResponse, include_in_schema=True) +async def photo_upload(file: UploadFile, album: str, ignore_duplicates: bool = False, apikey: APIKey = Depends(get_api_key)): + + if (check_project_key("photos", apikey)): + + if col_albums.find_one( {"name": album} ) is None: + return HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Provided album '{album}' does not exist.") + + # if not file.content_type.startswith("image"): + # return HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE, detail="Provided file is not an image, not accepting.") + + makedirs(f'data{sep}users{sep}sample_user{sep}albums{sep}{album}', exist_ok=True) + + filename = file.filename + + if path.exists(f'data{sep}users{sep}sample_user{sep}albums{sep}{album}{sep}{file.filename}'): + base_name = file.filename.split(".")[:-1] + extension = file.filename.split(".")[-1] + filename = ".".join(base_name)+f"_{int(datetime.now().timestamp())}."+extension + + with open(f'data{sep}users{sep}sample_user{sep}albums{sep}{album}{sep}{filename}', "wb") as f: + f.write(await file.read()) + + file_hash = await get_phash(f'data{sep}users{sep}sample_user{sep}albums{sep}{album}{sep}{filename}') + duplicates = await get_duplicates(file_hash, album) + + if len(duplicates) > 0 and ignore_duplicates is False: + return UJSONResponse( + { + "detail": "Image duplicates found. Pass 'ignore_duplicates=true' to ignore.", + "duplicates": duplicates + }, + status_code=HTTP_409_CONFLICT + ) + + uploaded = col_photos.insert_one( {"album": album, "hash": file_hash, "filename": filename} ) + + return UJSONResponse( + { + "id": uploaded.inserted_id.__str__(), + "album": album, + "hash": file_hash, + "filename": filename + } + ) + + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.get("/photos/{id}", include_in_schema=True) +async def photo_get(id: str, apikey: APIKey = Depends(get_api_key)): + + if (check_project_key("photos", apikey)): + + try: + image = col_photos.find_one( {"_id": ObjectId(id)} ) + if image is None: + raise InvalidId(id) + except InvalidId: + return HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Could not find an image with such id.") + + image_path = f'data{sep}users{sep}sample_user{sep}albums{sep}{image["album"]}{sep}{image["filename"]}' + + mime = Magic(mime=True).from_file(image_path) + + with open(image_path, "rb") as f: image_file = f.read() + + return Response(image_file, media_type=mime) + + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.delete("/photos/{id}", include_in_schema=True) +async def photo_delete(id: str, apikey: APIKey = Depends(get_api_key)): + + if (check_project_key("photos", apikey)): + + try: + image = col_photos.find_one_and_delete( {"_id": ObjectId(id)} ) + if image is None: + raise InvalidId(id) + except InvalidId: + return HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Could not find an image with such id.") + + remove(f'data{sep}users{sep}sample_user{sep}albums{sep}{image["album"]}{sep}{image["filename"]}') + + return Response(status_code=HTTP_204_NO_CONTENT) + + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.get("/albums/{album}/photos", response_class=UJSONResponse, include_in_schema=True) +async def photo_find(q: str, album: str, page: int = 1, page_size: int = 100, apikey: APIKey = Depends(get_api_key)): + + if (check_project_key("photos", apikey)): + + if col_albums.find_one( {"name": album} ) is None: + return HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Provided album '{album}' does not exist.") + + if page <= 0 or page_size <= 0: + return HTTPException(status_code=HTTP_400_BAD_REQUEST, detail="Parameters 'page' and 'page_size' must be greater or equal to 1.") + + output = {"results": []} + skip = (page-1)*page_size + images = list(col_photos.find({"album": album, "filename": re.compile(q)}, limit=page_size, skip=skip)) + + for image in images: + output["results"].append({"id": image["_id"].__str__(), "filename": image["filename"]}) + + if col_photos.count_documents( {"album": album, "filename": re.compile(q)} ) > page*page_size: + token = str(token_urlsafe(32)) + col_tokens.insert_one( {"token": token, "query": q, "album": album, "page": page+1, "page_size": page_size, "apikey": apikey} ) + output["next_page"] = f"https://api.end-play.xyz/photoFindToken?token={token}" # type: ignore + + return UJSONResponse(output) + + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.get("/photos/token/{token}", response_class=UJSONResponse, include_in_schema=True) +async def photo_find_token(token: str): + + found_record = col_tokens.find_one( {"token": token} ) + + if found_record is None: + return HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid search token.") + + return await photo_find(q=found_record["query"], album=found_record["album"], page=found_record["page"], page_size=found_record["page_size"], apikey=found_record["apikey"]) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4b48a18 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi[all] +pymongo==4.3.3 +ujson~=5.6.0 +scipy~=1.9.3 +python-magic~=0.4.27 +opencv-python~=4.6.0.66 \ No newline at end of file