Compare commits
5 Commits
f9df399682
...
v0.2
Author | SHA1 | Date | |
---|---|---|---|
a380da81bb | |||
e858e7d7f4 | |||
fcbbd4f2bf | |||
77efd3be89 | |||
735a1e9261 |
99
README.md
Normal file
99
README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<h1 align="center">Photos API</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://git.end-play.xyz/profitroll/PhotosAPILICENSE"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
|
||||||
|
<a href="https://git.end-play.xyz/profitroll/PhotosAPI"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Small and simple API server for saving photos and videos.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
* [Python 3.7+](https://www.python.org) (3.9+ recommended)
|
||||||
|
* [MongoDB](https://www.mongodb.com)
|
||||||
|
* [exiftool](https://exiftool.org)
|
||||||
|
* [jpegoptim](https://github.com/tjko/jpegoptim)
|
||||||
|
* [optipng](https://optipng.sourceforge.net)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
First you need to have a Python interpreter, MongoDB and optionally git. You can also ignore git and simply download source code, should also work fine. After that you're ready to go.
|
||||||
|
|
||||||
|
> In this README I assume that you're using default python in your
|
||||||
|
> system and your system's PATH contains it. If your default python
|
||||||
|
> is `python3` or for example `/home/user/.local/bin/python3.9` - use it instead.
|
||||||
|
|
||||||
|
1. Install Mongo:
|
||||||
|
|
||||||
|
Please follow [official installation manual](https://www.mongodb.com/docs/manual/installation) for that.
|
||||||
|
|
||||||
|
2. Download Photos API:
|
||||||
|
|
||||||
|
1. `git clone https://git.end-play.xyz/profitroll/PhotosAPI.git` (if you're using git)
|
||||||
|
2. `cd PhotosAPI`
|
||||||
|
|
||||||
|
3. Create virtual environment [Optional yet recommended]:
|
||||||
|
|
||||||
|
1. Install virtualenv module: `pip install virtualenv`
|
||||||
|
2. Create venv: `python -m venv env`
|
||||||
|
3. Activate it using `source venv/bin/activate` on Linux, `venv\Scripts\activate.bat` in CMD or `venv\Scripts\Activate.ps1` in PowerShell.
|
||||||
|
|
||||||
|
4. Install project's dependencies:
|
||||||
|
|
||||||
|
`python -m pip install -r requirements.txt`
|
||||||
|
|
||||||
|
5. Configure your API:
|
||||||
|
|
||||||
|
1. Copy file `config_example.json` to `config.json`
|
||||||
|
2. Open `config.json` using your favorite text editor. For example `nano config.json`
|
||||||
|
3. Change `"database"` keys to match your MongoDB setup
|
||||||
|
4. Change `"external_address"` to the ip/http address you may get in responses. By default it's `"localhost"`. This is extremely useful when running behind reverse-proxy.
|
||||||
|
|
||||||
|
After configuring everything listed above your API will be able to boot, however further configuration can be done. You can read about it in [repository's wiki](https://git.end-play.xyz/profitroll/PhotosAPI/wiki/Configuration). There's no need to focus on that now, it makes more sense to configure it afterwards.
|
||||||
|
|
||||||
|
6. Start your API:
|
||||||
|
|
||||||
|
You can run your API by the following command:
|
||||||
|
`uvicorn photos_api:app --host 127.0.0.1 --port 8054`
|
||||||
|
|
||||||
|
Learn more about available uvicorn arguments using `uvicorn --help`
|
||||||
|
|
||||||
|
## Using as a service
|
||||||
|
|
||||||
|
It's a good practice to use your API as a systemd service on Linux. Here's a quick overview how that can be done.
|
||||||
|
|
||||||
|
1. Create user and move your API
|
||||||
|
|
||||||
|
You don't always need to do so, but that's a cleaner way to deploy a service.
|
||||||
|
|
||||||
|
1. Create service user `photosapi` using `sudo useradd -r -U photosapi`
|
||||||
|
2. Assuming you are still in directory `PhotosAPI`, use `cd ..` to go up a level and then move your API to the distinguished folder. For example, `/opt/`: `sudo mv ./PhotosAPI /opt/`
|
||||||
|
3. Make your user and its group own their directory using `sudo chown -R photosapi:photosapi /opt/PhotosAPI`
|
||||||
|
|
||||||
|
2. Configure service
|
||||||
|
|
||||||
|
Here's an example service file for PhotosAPI that is using virtual environment:
|
||||||
|
|
||||||
|
```systemd
|
||||||
|
[Unit]
|
||||||
|
Description=Photos API
|
||||||
|
After=network.target mongod.service
|
||||||
|
Wants=network-online.target mongod.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/bin/bash -c 'source venv/bin/activate && venv/bin/uvicorn photos_api:app --port 8054'
|
||||||
|
WorkingDirectory=/opt/PhotosAPI
|
||||||
|
User=photosapi
|
||||||
|
Group=photosapi
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Create a service by pasting code above into `/etc/systemd/system/photos-api.service`
|
||||||
|
2. Enable your service to start on system boot using `sudo systemctl enable photos-api.service`
|
||||||
|
3. Start your service now using `sudo systemctl start photos-api.service`
|
||||||
|
4. Check if it's running using `sudo systemctl status photos-api.service`
|
||||||
|
5. If something goes wrong - check API's logs using `sudo journalctl -u photos-api.service`
|
@@ -13,7 +13,7 @@
|
|||||||
"media_token_access": false,
|
"media_token_access": false,
|
||||||
"media_token_valid_hours": 12,
|
"media_token_valid_hours": 12,
|
||||||
"registration_enabled": true,
|
"registration_enabled": true,
|
||||||
"registration_requires_confirmation": true,
|
"registration_requires_confirmation": false,
|
||||||
"mailer": {
|
"mailer": {
|
||||||
"smtp": {
|
"smtp": {
|
||||||
"host": "",
|
"host": "",
|
||||||
|
@@ -444,6 +444,7 @@ async def photo_delete(
|
|||||||
|
|
||||||
photo_find_responses = {
|
photo_find_responses = {
|
||||||
400: SearchPageInvalidError().openapi,
|
400: SearchPageInvalidError().openapi,
|
||||||
|
401: SearchTokenInvalidError().openapi,
|
||||||
404: AlbumNameNotFoundError("name").openapi,
|
404: AlbumNameNotFoundError("name").openapi,
|
||||||
422: PhotoSearchQueryEmptyError().openapi,
|
422: PhotoSearchQueryEmptyError().openapi,
|
||||||
}
|
}
|
||||||
@@ -451,7 +452,7 @@ photo_find_responses = {
|
|||||||
|
|
||||||
@app.get(
|
@app.get(
|
||||||
"/albums/{album}/photos",
|
"/albums/{album}/photos",
|
||||||
description="Find a photo by filename",
|
description="Find a photo by filename, caption, location or token",
|
||||||
response_class=UJSONResponse,
|
response_class=UJSONResponse,
|
||||||
response_model=SearchResultsPhoto,
|
response_model=SearchResultsPhoto,
|
||||||
responses=photo_find_responses,
|
responses=photo_find_responses,
|
||||||
@@ -460,6 +461,7 @@ async def photo_find(
|
|||||||
album: str,
|
album: str,
|
||||||
q: Union[str, None] = None,
|
q: Union[str, None] = None,
|
||||||
caption: Union[str, None] = None,
|
caption: Union[str, None] = None,
|
||||||
|
token: Union[str, None] = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 100,
|
page_size: int = 100,
|
||||||
lat: Union[float, None] = None,
|
lat: Union[float, None] = None,
|
||||||
@@ -467,6 +469,24 @@ async def photo_find(
|
|||||||
radius: Union[int, None] = None,
|
radius: Union[int, None] = None,
|
||||||
current_user: User = Security(get_current_active_user, scopes=["photos.list"]),
|
current_user: User = Security(get_current_active_user, scopes=["photos.list"]),
|
||||||
):
|
):
|
||||||
|
if token is not None:
|
||||||
|
found_record = col_tokens.find_one({"token": token})
|
||||||
|
|
||||||
|
if found_record is None:
|
||||||
|
raise SearchTokenInvalidError()
|
||||||
|
|
||||||
|
return await photo_find(
|
||||||
|
album=album,
|
||||||
|
q=found_record["query"],
|
||||||
|
caption=found_record["caption"],
|
||||||
|
lat=found_record["lat"],
|
||||||
|
lng=found_record["lng"],
|
||||||
|
radius=found_record["radius"],
|
||||||
|
page=found_record["page"],
|
||||||
|
page_size=found_record["page_size"],
|
||||||
|
current_user=current_user,
|
||||||
|
)
|
||||||
|
|
||||||
if col_albums.find_one({"user": current_user.user, "name": album}) is None:
|
if col_albums.find_one({"user": current_user.user, "name": album}) is None:
|
||||||
raise AlbumNameNotFoundError(album)
|
raise AlbumNameNotFoundError(album)
|
||||||
|
|
||||||
@@ -543,39 +563,16 @@ async def photo_find(
|
|||||||
{
|
{
|
||||||
"token": token,
|
"token": token,
|
||||||
"query": q,
|
"query": q,
|
||||||
"album": album,
|
"caption": caption,
|
||||||
|
"lat": lat,
|
||||||
|
"lng": lng,
|
||||||
|
"radius": radius,
|
||||||
"page": page + 1,
|
"page": page + 1,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"user": pickle.dumps(current_user),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
output["next_page"] = f"/albums/{album}/photos/token?token={token}" # type: ignore
|
output["next_page"] = f"/albums/{album}/photos/?token={token}" # type: ignore
|
||||||
else:
|
else:
|
||||||
output["next_page"] = None # type: ignore
|
output["next_page"] = None # type: ignore
|
||||||
|
|
||||||
return UJSONResponse(output)
|
return UJSONResponse(output)
|
||||||
|
|
||||||
|
|
||||||
photo_find_token_responses = {401: SearchTokenInvalidError().openapi}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get(
|
|
||||||
"/albums/{album}/photos/token",
|
|
||||||
description="Find a photo by token",
|
|
||||||
response_class=UJSONResponse,
|
|
||||||
response_model=SearchResultsPhoto,
|
|
||||||
responses=photo_find_token_responses,
|
|
||||||
)
|
|
||||||
async def photo_find_token(token: str):
|
|
||||||
found_record = col_tokens.find_one({"token": token})
|
|
||||||
|
|
||||||
if found_record is None:
|
|
||||||
raise SearchTokenInvalidError()
|
|
||||||
|
|
||||||
return await photo_find(
|
|
||||||
q=found_record["query"],
|
|
||||||
album=found_record["album"],
|
|
||||||
page=found_record["page"],
|
|
||||||
page_size=found_record["page_size"],
|
|
||||||
current_user=pickle.loads(found_record["user"]),
|
|
||||||
)
|
|
||||||
|
@@ -260,6 +260,7 @@ async def video_delete(
|
|||||||
|
|
||||||
video_find_responses = {
|
video_find_responses = {
|
||||||
400: SearchPageInvalidError().openapi,
|
400: SearchPageInvalidError().openapi,
|
||||||
|
401: SearchTokenInvalidError().openapi,
|
||||||
404: AlbumNameNotFoundError("name").openapi,
|
404: AlbumNameNotFoundError("name").openapi,
|
||||||
422: VideoSearchQueryEmptyError().openapi,
|
422: VideoSearchQueryEmptyError().openapi,
|
||||||
}
|
}
|
||||||
@@ -267,7 +268,7 @@ video_find_responses = {
|
|||||||
|
|
||||||
@app.get(
|
@app.get(
|
||||||
"/albums/{album}/videos",
|
"/albums/{album}/videos",
|
||||||
description="Find a video by filename",
|
description="Find a video by filename, caption or token",
|
||||||
response_class=UJSONResponse,
|
response_class=UJSONResponse,
|
||||||
response_model=SearchResultsVideo,
|
response_model=SearchResultsVideo,
|
||||||
responses=video_find_responses,
|
responses=video_find_responses,
|
||||||
@@ -276,10 +277,26 @@ async def video_find(
|
|||||||
album: str,
|
album: str,
|
||||||
q: Union[str, None] = None,
|
q: Union[str, None] = None,
|
||||||
caption: Union[str, None] = None,
|
caption: Union[str, None] = None,
|
||||||
|
token: Union[str, None] = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 100,
|
page_size: int = 100,
|
||||||
current_user: User = Security(get_current_active_user, scopes=["videos.list"]),
|
current_user: User = Security(get_current_active_user, scopes=["videos.list"]),
|
||||||
):
|
):
|
||||||
|
if token is not None:
|
||||||
|
found_record = col_tokens.find_one({"token": token})
|
||||||
|
|
||||||
|
if found_record is None:
|
||||||
|
raise SearchTokenInvalidError()
|
||||||
|
|
||||||
|
return await video_find(
|
||||||
|
album=album,
|
||||||
|
q=found_record["query"],
|
||||||
|
caption=found_record["caption"],
|
||||||
|
page=found_record["page"],
|
||||||
|
page_size=found_record["page_size"],
|
||||||
|
current_user=current_user,
|
||||||
|
)
|
||||||
|
|
||||||
if col_albums.find_one({"user": current_user.user, "name": album}) is None:
|
if col_albums.find_one({"user": current_user.user, "name": album}) is None:
|
||||||
raise AlbumNameNotFoundError(album)
|
raise AlbumNameNotFoundError(album)
|
||||||
|
|
||||||
@@ -341,39 +358,13 @@ async def video_find(
|
|||||||
{
|
{
|
||||||
"token": token,
|
"token": token,
|
||||||
"query": q,
|
"query": q,
|
||||||
"album": album,
|
"caption": caption,
|
||||||
"page": page + 1,
|
"page": page + 1,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"user": pickle.dumps(current_user),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
output["next_page"] = f"/albums/{album}/videos/token?token={token}" # type: ignore
|
output["next_page"] = f"/albums/{album}/videos/?token={token}" # type: ignore
|
||||||
else:
|
else:
|
||||||
output["next_page"] = None # type: ignore
|
output["next_page"] = None # type: ignore
|
||||||
|
|
||||||
return UJSONResponse(output)
|
return UJSONResponse(output)
|
||||||
|
|
||||||
|
|
||||||
video_find_token_responses = {401: SearchTokenInvalidError().openapi}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get(
|
|
||||||
"/albums/{album}/videos/token",
|
|
||||||
description="Find a video by token",
|
|
||||||
response_class=UJSONResponse,
|
|
||||||
response_model=SearchResultsVideo,
|
|
||||||
responses=video_find_token_responses,
|
|
||||||
)
|
|
||||||
async def video_find_token(token: str):
|
|
||||||
found_record = col_tokens.find_one({"token": token})
|
|
||||||
|
|
||||||
if found_record is None:
|
|
||||||
raise SearchTokenInvalidError()
|
|
||||||
|
|
||||||
return await video_find(
|
|
||||||
q=found_record["query"],
|
|
||||||
album=found_record["album"],
|
|
||||||
page=found_record["page"],
|
|
||||||
page_size=found_record["page_size"],
|
|
||||||
current_user=pickle.loads(found_record["user"]),
|
|
||||||
)
|
|
||||||
|
@@ -2,7 +2,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
|
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="END PLAY Photos", docs_url=None, redoc_url=None, version="0.1")
|
app = FastAPI(title="END PLAY Photos", docs_url=None, redoc_url=None, version="0.2")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/docs", include_in_schema=False)
|
@app.get("/docs", include_in_schema=False)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
fastapi[all]~=0.94.0
|
fastapi[all]==0.95.0
|
||||||
pymongo==4.3.3
|
pymongo==4.3.3
|
||||||
ujson~=5.7.0
|
ujson~=5.7.0
|
||||||
scipy~=1.10.1
|
scipy~=1.10.1
|
||||||
@@ -7,4 +7,4 @@ opencv-python~=4.7.0.72
|
|||||||
python-jose[cryptography]~=3.3.0
|
python-jose[cryptography]~=3.3.0
|
||||||
passlib~=1.7.4
|
passlib~=1.7.4
|
||||||
apscheduler~=3.10.1
|
apscheduler~=3.10.1
|
||||||
exif==1.5.0
|
exif==1.6.0
|
Reference in New Issue
Block a user