Compare commits
4 Commits
8ee3687c73
...
4892916e16
Author | SHA1 | Date | |
---|---|---|---|
|
4892916e16 | ||
|
7291faede5 | ||
|
653688b376 | ||
|
2e76962ca0 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -154,5 +154,4 @@ cython_debug/
|
||||
|
||||
# Custom
|
||||
.vscode
|
||||
config.json
|
||||
pages
|
||||
config.json
|
21
classes/models.py
Normal file
21
classes/models.py
Normal 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
|
@ -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
27
extensions/pages.py
Normal 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)
|
@ -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
3
modules/scheduler.py
Normal file
@ -0,0 +1,3 @@
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
scheduler = AsyncIOScheduler()
|
24
pages/home/index.html
Normal file
24
pages/home/index.html
Normal 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
6
pages/home/script.js
Normal 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
148
pages/home/style.css
Normal 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
2
pages/matter.css
Normal file
File diff suppressed because one or more lines are too long
50
pages/register/index.html
Normal file
50
pages/register/index.html
Normal 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
6
pages/register/script.js
Normal 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
138
pages/register/style.css
Normal 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;
|
||||
}
|
@ -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()
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user