|
|
|
|
@ -1,4 +1,4 @@
|
|
|
|
|
from fastapi import FastAPI, File, UploadFile, Depends
|
|
|
|
|
from fastapi import FastAPI, File, UploadFile, Depends, BackgroundTasks
|
|
|
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware # CORS
|
|
|
|
|
@ -7,12 +7,14 @@ from secrets import compare_digest
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
import sqlite3
|
|
|
|
|
import zipfile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# use database residing here
|
|
|
|
|
DB_LOCATION = (
|
|
|
|
|
"../testbox/photovoter.dblite" # Q: any allowances for this being not OUR database?
|
|
|
|
|
)
|
|
|
|
|
DATA_LOCATION = "/tmp/123"
|
|
|
|
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
security = HTTPBasic()
|
|
|
|
|
@ -152,24 +154,66 @@ async def rate_picture(cookie: str, picture_id: int, mark: int):
|
|
|
|
|
@app.post(
|
|
|
|
|
"/upload_pictures/",
|
|
|
|
|
responses={
|
|
|
|
|
202: {"description": "Archive accepted into processing"},
|
|
|
|
|
401: {"description": "Authentication is required to access this resource"},
|
|
|
|
|
415: {"description": "Cannot process uploaded archive"},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
async def upload_pictures(
|
|
|
|
|
credentials: HTTPBasicCredentials = Depends(security), file: UploadFile = File(...)
|
|
|
|
|
background_tasks: BackgroundTasks,
|
|
|
|
|
credentials: HTTPBasicCredentials = Depends(security),
|
|
|
|
|
file: UploadFile = File(...),
|
|
|
|
|
):
|
|
|
|
|
"""Интерфейс для загрузки фотографий"""
|
|
|
|
|
"""Условно кладём в браузер zip с фотографиями и он их потихоньку ест.
|
|
|
|
|
Доступ к этому интерфейсу, наверное, лучше ограничить паролем или как-нибудь ещё.
|
|
|
|
|
Пока исходим из предположения, что только я буду загружать фотографии."""
|
|
|
|
|
Пока исходим из предположения, что только я буду загружать фотографии.
|
|
|
|
|
"""
|
|
|
|
|
# check authenticity
|
|
|
|
|
correct_username = compare_digest(credentials.username, "1")
|
|
|
|
|
correct_password = compare_digest(credentials.password, "1")
|
|
|
|
|
if not (correct_username and correct_password):
|
|
|
|
|
return JSONResponse(status_code=401)
|
|
|
|
|
# slurp the zip
|
|
|
|
|
# *detach from the interface, if possible
|
|
|
|
|
if not zipfile.is_zipfile(file.file):
|
|
|
|
|
return JSONResponse(status_code=415)
|
|
|
|
|
# detach from the interface
|
|
|
|
|
# unpack zip
|
|
|
|
|
tasks = BackgroundTasks()
|
|
|
|
|
tasks.add_task(
|
|
|
|
|
unpack_pictures_zip,
|
|
|
|
|
file=file,
|
|
|
|
|
time=datetime.utcnow().replace(microsecond=0).isoformat(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# feed the pictures to util/import_photos.py
|
|
|
|
|
return {"filename": file.filename}
|
|
|
|
|
return JSONResponse("Accepted", background=tasks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def unpack_pictures_zip(file: UploadFile, time):
|
|
|
|
|
"""
|
|
|
|
|
Unpack and process zip archived photo
|
|
|
|
|
Extract pictures in the DATA_LOCATION/processing
|
|
|
|
|
#TODO: and feed them to util/import_photos.py
|
|
|
|
|
#TODO: Walk the nested DATA_LOCATION/processing ourselves
|
|
|
|
|
Uses: DATA_LOCATION
|
|
|
|
|
"""
|
|
|
|
|
# we only call this function sporadically, so import here
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
print(f"Accepted {file.filename} at {time} into processing")
|
|
|
|
|
os.chdir(DATA_LOCATION)
|
|
|
|
|
os.mkdir("processing")
|
|
|
|
|
os.chdir("processing")
|
|
|
|
|
|
|
|
|
|
# using private ._file field is a dirty hack, but
|
|
|
|
|
# SpooledTemporaryFile does not implement seekable
|
|
|
|
|
# required by zipfile 'r' mode
|
|
|
|
|
# https://bugs.python.org/issue26175
|
|
|
|
|
with zipfile.ZipFile(file.file._file) as photo_zip:
|
|
|
|
|
problem_files = photo_zip.testzip()
|
|
|
|
|
if problem_files is not None:
|
|
|
|
|
print(f"Errors in {file.filename} accepted at {time}: {problem_files}")
|
|
|
|
|
photo_zip.extractall()
|
|
|
|
|
photo_zip.close()
|
|
|
|
|
print(f"Succesfully processed {file.filename} accepted at {time}")
|
|
|
|
|
|