Compare commits

...

4 Commits

Author SHA1 Message Date
Profitroll
4892916e16 Added scheduler 2022-12-20 22:24:46 +01:00
Profitroll
7291faede5 Added photos compression 2022-12-20 22:24:35 +01:00
Profitroll
653688b376 Added models where possible 2022-12-20 17:07:48 +01:00
Profitroll
2e76962ca0 Added some pages 2022-12-20 15:34:47 +01:00
15 changed files with 470 additions and 17 deletions

3
.gitignore vendored
View File

@ -154,5 +154,4 @@ cython_debug/
# Custom
.vscode
config.json
pages
config.json

21
classes/models.py Normal file
View File

@ -0,0 +1,21 @@
from typing import Union
from pydantic import BaseModel
class Photo(BaseModel):
id: str
album: str
hash: str
filename: str
class Album(BaseModel):
id: str
name: str
title: str
class AlbumModified(BaseModel):
name: str
title: str
class SearchResults(BaseModel):
results: list
next_page: Union[str, None] = None

View File

@ -2,6 +2,7 @@ import re
from os import makedirs, path, rename
from shutil import rmtree
from typing import Union
from classes.models import Album, AlbumModified, SearchResults
from modules.app import app
from modules.database import col_photos, col_albums
from modules.security import User, get_current_active_user
@ -12,7 +13,7 @@ from fastapi import HTTPException, Security
from fastapi.responses import UJSONResponse, Response
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT
@app.post("/albums", response_class=UJSONResponse, description="Create album with name and title")
@app.post("/albums", response_class=UJSONResponse, response_model=Album, description="Create album with name and title")
async def album_create(name: str, title: str, current_user: User = Security(get_current_active_user, scopes=["albums.write"])):
if re.search(re.compile('^[a-z,0-9,_]*$'), name) is False:
@ -39,7 +40,7 @@ async def album_create(name: str, title: str, current_user: User = Security(get_
}
)
@app.get("/albums", description="Find album by name")
@app.get("/albums", response_model=SearchResults, description="Find album by name")
async def album_find(q: str, current_user: User = Security(get_current_active_user, scopes=["albums.list"])):
output = {"results": []}
@ -50,7 +51,7 @@ async def album_find(q: str, current_user: User = Security(get_current_active_us
return UJSONResponse(output)
@app.patch("/albums/{id}", response_class=UJSONResponse, description="Modify album's name or title by id")
@app.patch("/albums/{id}", response_class=UJSONResponse, response_model=AlbumModified, description="Modify album's name or title by id")
async def album_patch(id: str, name: Union[str, None] = None, title: Union[str, None] = None, current_user: User = Security(get_current_active_user, scopes=["albums.write"])):
try:
@ -88,7 +89,7 @@ async def album_patch(id: str, name: Union[str, None] = None, title: Union[str,
}
)
@app.put("/albums/{id}", response_class=UJSONResponse, description="Modify album's name and title by id")
@app.put("/albums/{id}", response_class=UJSONResponse, response_model=AlbumModified, description="Modify album's name and title by id")
async def album_put(id: str, name: str, title: str, current_user: User = Security(get_current_active_user, scopes=["albums.write"])):
try:

27
extensions/pages.py Normal file
View File

@ -0,0 +1,27 @@
from os import path
from modules.app import app
from fastapi.responses import HTMLResponse, Response
@app.get("/pages/matter.css", include_in_schema=False)
async def page_matter():
with open(path.join("pages", "matter.css"), "r", encoding="utf-8") as f:
output = f.read()
return Response(content=output)
@app.get("/pages/{page}/{file}", include_in_schema=False)
async def page_assets(page:str, file: str):
with open(path.join("pages", page, file), "r", encoding="utf-8") as f:
output = f.read()
return Response(content=output)
@app.get("/", include_in_schema=False)
async def page_home():
with open(path.join("pages", "home", "index.html"), "r", encoding="utf-8") as f:
output = f.read()
return HTMLResponse(content=output)
@app.get("/register", include_in_schema=False)
async def page_register():
with open(path.join("pages", "register", "index.html"), "r", encoding="utf-8") as f:
output = f.read()
return HTMLResponse(content=output)

View File

@ -2,9 +2,11 @@ import re
import pickle
from secrets import token_urlsafe
from magic import Magic
from datetime import datetime
from os import makedirs, path, remove
from datetime import datetime, timedelta
from os import makedirs, path, remove, system
from classes.models import Photo, SearchResults
from modules.hasher import get_phash, get_duplicates
from modules.scheduler import scheduler
from modules.security import User, get_current_active_user
from modules.app import app
from modules.database import col_photos, col_albums, col_tokens
@ -15,8 +17,28 @@ from fastapi import HTTPException, UploadFile, Security
from fastapi.responses import UJSONResponse, Response
from starlette.status import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT
@app.post("/albums/{album}/photos", response_class=UJSONResponse, description="Upload a photo to album")
async def photo_upload(file: UploadFile, album: str, ignore_duplicates: bool = False, current_user: User = Security(get_current_active_user, scopes=["photos.write"])):
from modules.utils import logWrite
async def compress_image(image_path: str):
image_type = Magic(mime=True).from_file(image_path)
if image_type not in ["image/jpeg", "image/png"]:
logWrite(f"Not compressing {image_path} because its mime is '{image_type}'")
return
size_before = path.getsize(image_path) / 1024
if image_type == "image/jpeg":
system(f"jpegoptim {image_path} -o --max=60 --strip-all")
elif image_type == "image/png":
system(f"optipng -o3 {image_path}")
size_after = path.getsize(image_path) / 1024
logWrite(f"Compressed '{path.split(image_path)[-1]}' from {size_before} Kb to {size_after} Kb")
@app.post("/albums/{album}/photos", response_class=UJSONResponse, response_model=Photo, description="Upload a photo to album")
async def photo_upload(file: UploadFile, album: str, ignore_duplicates: bool = False, compress: bool = True, current_user: User = Security(get_current_active_user, scopes=["photos.write"])):
if col_albums.find_one( {"user": current_user.user, "name": album} ) is None:
return HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Provided album '{album}' does not exist.")
@ -50,6 +72,9 @@ async def photo_upload(file: UploadFile, album: str, ignore_duplicates: bool = F
uploaded = col_photos.insert_one( {"user": current_user.user, "album": album, "hash": file_hash, "filename": filename} )
if compress is True:
scheduler.add_job(compress_image, trigger="date", run_date=datetime.now()+timedelta(seconds=1), args=[path.join("data", "users", current_user.user, "albums", album, filename)])
return UJSONResponse(
{
"id": uploaded.inserted_id.__str__(),
@ -91,7 +116,7 @@ async def photo_delete(id: str, current_user: User = Security(get_current_active
return Response(status_code=HTTP_204_NO_CONTENT)
@app.get("/albums/{album}/photos", response_class=UJSONResponse, description="Find a photo by filename")
@app.get("/albums/{album}/photos", response_class=UJSONResponse, response_model=SearchResults, description="Find a photo by filename")
async def photo_find(q: str, album: str, page: int = 1, page_size: int = 100, current_user: User = Security(get_current_active_user, scopes=["photos.list"])):
if col_albums.find_one( {"user": current_user.user, "name": album} ) is None:
@ -111,13 +136,12 @@ async def photo_find(q: str, album: str, page: int = 1, page_size: int = 100, cu
token = str(token_urlsafe(32))
col_tokens.insert_one( {"token": token, "query": q, "album": album, "page": page+1, "page_size": page_size, "user": pickle.dumps(current_user)} )
output["next_page"] = f"/albums/{album}/photos/token?token={token}" # type: ignore
with open("something.txt", "w", encoding="utf-8") as f:
f.write(pickle.loads(pickle.dumps(current_user)).user)
else:
output["next_page"] = None # type: ignore
return UJSONResponse(output)
@app.get("/albums/{album}/photos/token", response_class=UJSONResponse, description="Find a photo by token")
@app.get("/albums/{album}/photos/token", response_class=UJSONResponse, response_model=SearchResults, description="Find a photo by token")
async def photo_find_token(token: str):
found_record = col_tokens.find_one( {"token": token} )

3
modules/scheduler.py Normal file
View File

@ -0,0 +1,3 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler()

24
pages/home/index.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>END PLAY Photos API • Home</title>
<link href="/pages/matter.css" rel="stylesheet">
<link rel="stylesheet" href="/pages/home/style.css">
</head>
<body>
<form class="registration">
<h1>👋 Welcome!</h1>
<p>You need to register in order to use this API.</p>
<p>After registering use official docs to learn how to authentiticate and start managing photos.</p>
<center><a class="matter-button-contained" href="https://photos.end-play.xyz/register">Sign Up</a></center>
</form>
</body>
</html>

6
pages/home/script.js Normal file
View File

@ -0,0 +1,6 @@
// JavaScript is used for toggling loading state
var form = document.querySelector('form');
form.onsubmit = function (event) {
event.preventDefault();
form.classList.add('signed');
};

148
pages/home/style.css Normal file
View File

@ -0,0 +1,148 @@
/* Material Customization */
:root {
--pure-material-primary-rgb: 255, 191, 0;
--pure-material-onsurface-rgb: 0, 0, 0;
}
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: url("https://res.cloudinary.com/finnhvman/image/upload/v1541930411/pattern.png");
}
.registration {
position: relative;
border-radius: 8px;
padding: 16px 48px;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
overflow: hidden;
background-color: white;
}
h1 {
margin: 32px 0;
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
font-weight: normal;
text-align: center;
}
.registration > label {
display: block;
margin: 24px 0;
width: 320px;
}
p {
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
font-weight: normal;
text-align: center;
}
a.matter-button-contained {
text-decoration: none;
}
a {
color: rgb(var(--pure-material-primary-rgb));
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
display: block !important;
margin: 32px auto;
}
.done,
.progress {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: white;
visibility: hidden;
}
.done {
transition: visibility 0s 1s;
}
.signed > .done {
visibility: visible;
}
.done > a {
display: inline-block;
text-decoration: none;
}
.progress {
opacity: 0;
}
.signed > .progress {
animation: loading 4s;
}
@keyframes loading {
0% {
visibility: visible;
}
12.5% {
opacity: 0;
}
25% {
opacity: 1;
}
87.5% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.left-footer,
.right-footer {
position: fixed;
padding: 14px;
bottom: 14px;
color: #555;
background-color: #eee;
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
font-size: 14px;
line-height: 1.5;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.left-footer {
left: 0;
border-radius: 0 4px 4px 0;
text-align: left;
}
.right-footer {
right: 0;
border-radius: 4px 0 0 4px;
text-align: right;
}
.left-footer > a,
.right-footer > a {
color: black;
}
.left-footer > a:hover,
.right-footer > a:hover {
text-decoration: underline;
}

2
pages/matter.css Normal file

File diff suppressed because one or more lines are too long

50
pages/register/index.html Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>END PLAY Photos API • Sign Up</title>
<link href="/pages/matter.css" rel="stylesheet">
<link rel="stylesheet" href="/pages/home/style.css">
</head>
<body>
<!-- partial:index.partial.html -->
<form class="registration" method="post">
<h1>👋 Welcome!</h1>
<label class="matter-textfield-outlined">
<input placeholder=" " type="text" alt="You won't be able to change it later!" required>
<span>Username</span>
</label>
<label class="matter-textfield-outlined">
<input placeholder=" " type="email" required>
<span>Email</span>
</label>
<label class="matter-textfield-outlined">
<input placeholder=" " type="password" required>
<span>Password</span>
</label>
<label class="matter-checkbox">
<input type="checkbox" required>
<span>I agree to the <a href="https://codepen.io/collection/nZKBZe/" target="_blank" title="Actually not a Terms of Service">Terms of Service</a></span>
</label>
<button class="matter-button-contained" type="submit">Sign Up</button>
<div class="done">
<h1>👌 You're all set!</h1>
<a class="matter-button-text" href="javascript:window.location.reload(true)">Again</a>
</div>
<div class="progress">
<progress class="matter-progress-circular" />
</div>
</form>
<!-- partial -->
<script src="./script.js"></script>
</body>
</html>

6
pages/register/script.js Normal file
View File

@ -0,0 +1,6 @@
// JavaScript is used for toggling loading state
var form = document.querySelector('form');
form.onsubmit = function (event) {
event.preventDefault();
form.classList.add('signed');
};

138
pages/register/style.css Normal file
View File

@ -0,0 +1,138 @@
/* Material Customization */
:root {
--pure-material-primary-rgb: 255, 191, 0;
--pure-material-onsurface-rgb: 0, 0, 0;
}
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: url("https://res.cloudinary.com/finnhvman/image/upload/v1541930411/pattern.png");
}
.registration {
position: relative;
border-radius: 8px;
padding: 16px 48px;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
overflow: hidden;
background-color: white;
}
h1 {
margin: 32px 0;
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
font-weight: normal;
text-align: center;
}
.registration > label {
display: block;
margin: 24px 0;
width: 320px;
}
a {
color: rgb(var(--pure-material-primary-rgb));
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
display: block !important;
margin: 32px auto;
}
.done,
.progress {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: white;
visibility: hidden;
}
.done {
transition: visibility 0s 1s;
}
.signed > .done {
visibility: visible;
}
.done > a {
display: inline-block;
text-decoration: none;
}
.progress {
opacity: 0;
}
.signed > .progress {
animation: loading 4s;
}
@keyframes loading {
0% {
visibility: visible;
}
12.5% {
opacity: 0;
}
25% {
opacity: 1;
}
87.5% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.left-footer,
.right-footer {
position: fixed;
padding: 14px;
bottom: 14px;
color: #555;
background-color: #eee;
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
font-size: 14px;
line-height: 1.5;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.left-footer {
left: 0;
border-radius: 0 4px 4px 0;
text-align: left;
}
.right-footer {
right: 0;
border-radius: 4px 0 0 4px;
text-align: right;
}
.left-footer > a,
.right-footer > a {
color: black;
}
.left-footer > a:hover,
.right-footer > a:hover {
text-decoration: underline;
}

View File

@ -1,6 +1,7 @@
from os import makedirs, sep
from modules.app import app
from modules.utils import *
from modules.scheduler import scheduler
from modules.extensions_loader import dynamic_import_from_src
from fastapi.responses import FileResponse
@ -14,4 +15,6 @@ async def favicon():
#=================================================================================
dynamic_import_from_src("extensions", star_import = True)
#=================================================================================
#=================================================================================
scheduler.start()

View File

@ -5,4 +5,5 @@ scipy~=1.9.3
python-magic~=0.4.27
opencv-python~=4.6.0.66
python-jose[cryptography]~=3.3.0
passlib~=1.7.4
passlib~=1.7.4
apscheduler~=3.9.1.post1