├── .env-example ├── .gitignore ├── LICENSE.txt ├── api ├── Dockerfile ├── app │ ├── .env-example │ ├── main.py │ ├── services │ │ ├── __init__.py │ │ ├── helpers │ │ │ ├── alena.py │ │ │ ├── detectFileExtension.py │ │ │ └── uniqueFileName.py │ │ ├── images │ │ │ ├── __init__.py │ │ │ ├── generateQr.py │ │ │ └── resize.py │ │ ├── security │ │ │ └── customBearerCheck.py │ │ ├── serveDataFromUrl.py │ │ ├── serveQrcode.py │ │ ├── serveUploadedFiles.py │ │ ├── storage │ │ │ ├── __init__.py │ │ │ ├── googleCloud.py │ │ │ ├── local.py │ │ │ └── s3.py │ │ └── videos │ │ │ ├── __init__.py │ │ │ └── optimize.py │ ├── static │ │ ├── logo │ │ │ └── logo.png │ │ ├── pictures │ │ │ ├── original │ │ │ │ ├── afba38beae434b9fb4691bf8559947aa.png │ │ │ │ └── dcb8ac79618540688ea36e688a8c3635.png │ │ │ └── thumbnail │ │ │ │ ├── 72014f9f91ab40c7b8df61ab350bcc71.webp │ │ │ │ └── dcb8ac79618540688ea36e688a8c3635.webp │ │ └── qr │ │ │ └── 04de739e41154172b8858146f4d8edfe.png │ └── test_main.py ├── gunicorn_conf.py ├── poetry.lock ├── pyproject.toml ├── start-reload.sh └── start.sh ├── certbot ├── Dockerfile └── run-certbot.sh ├── docker-compose.dev.yml ├── docker-compose.prod.yml ├── nginx ├── Dockerfile ├── logrotate │ └── nginx ├── nginx.conf ├── sites │ ├── app.local.conf.example │ ├── app.ssl.conf.example │ └── ssl │ │ └── .gitignore └── startup.sh └── readme.md /.env-example: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | ###################### General Setup ###################### 3 | ########################################################### 4 | 5 | # Defines domains application will use for ssl [certbot] 6 | DOMAINS=example.com,www.example.com 7 | SSL_DOMAIN_OWNER=example@example.com 8 | 9 | 10 | ### Paths ################################################# 11 | 12 | # Point to the path of your applications code on your host 13 | APP_CODE_PATH_HOST=./api/app 14 | 15 | # Point to where the `APP_CODE_PATH_HOST` should be in the container. You may add flags to the path `:cached`, `:delegated`. When using Docker Sync add `:nocopy` 16 | APP_CODE_PATH_CONTAINER=/var/www:cached 17 | 18 | # Choose storage path on your machine. For all storage systems 19 | DATA_PATH_HOST=~/.etomer/data 20 | 21 | ### Services ############################################## 22 | INSTALL_FFMPEG=false 23 | 24 | 25 | ### NGINX ################################################# 26 | 27 | NGINX_HOST_HTTP_PORT=80 28 | NGINX_HOST_HTTPS_PORT=443 29 | NGINX_HOST_LOG_PATH=./logs/nginx/ 30 | NGINX_SITES_PATH=./nginx/sites/ 31 | NGINX_PYTHON_UPSTREAM_CONTAINER=api 32 | NGINX_PYTHON_UPSTREAM_PORT=9000 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .env 3 | .vscode 4 | logs 5 | data 6 | certbot/letsecrypt/ 7 | .DS_Store 8 | key.json 9 | docker-compose.yml 10 | nginx/sites/app.conf 11 | __pycache__/ 12 | *.pyc 13 | .pytest_cache -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Filemanager-Fastapi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.1-slim-buster 2 | 3 | ENV WORKDIR=/usr/src/app 4 | ENV USER=app 5 | ENV APP_HOME=/home/app/web 6 | ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 7 | 8 | WORKDIR $WORKDIR 9 | RUN apt-get update && apt-get install -y python3-dev build-essential 10 | 11 | # Image modifications 12 | RUN apt-get install -y libtiff5-dev libjpeg62-turbo-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev libmagic1 13 | 14 | RUN if [ ! -e /lib/libz.so ]; then \ 15 | ln -s /usr/lib/x86_64-linux-gnu/libz.so /lib/ \ 16 | ;fi 17 | 18 | RUN if [ ! -e /lib/libjpeg.so ]; then \ 19 | ln -s /usr/lib/x86_64-linux-gnu/libjpeg.so /lib/ \ 20 | ;fi 21 | 22 | # Install FFMPEG: 23 | ARG INSTALL_FFMPEG=false 24 | RUN echo "Oh dang look at that $INSTALL_FFMPEG" 25 | 26 | RUN if [ ${INSTALL_FFMPEG} = true ]; then \ 27 | apt-get install -y ffmpeg \ 28 | ;fi 29 | RUN ffmpeg -version 30 | RUN ffmpeg -encoders 31 | RUN ffmpeg -decoders 32 | 33 | RUN apt-get install curl -y 34 | 35 | # Install Poetry 36 | RUN if [ ! -e /usr/local/bin/poetry ]; then \ 37 | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ 38 | cd /usr/local/bin && \ 39 | ln -s /opt/poetry/bin/poetry && \ 40 | poetry config virtualenvs.create false \ 41 | ;fi 42 | 43 | 44 | COPY ./pyproject.toml ./poetry.lock* /$WORKDIR/ 45 | COPY ./gunicorn_conf.py /gunicorn_conf.py 46 | 47 | RUN poetry install --no-root --no-dev 48 | 49 | COPY ./start.sh /start.sh 50 | RUN chmod +rx /start.sh 51 | 52 | COPY ./start-reload.sh /start-reload.sh 53 | RUN chmod +rx /start-reload.sh 54 | 55 | COPY ./app /app 56 | WORKDIR /app/ 57 | 58 | ENV PYTHONPATH=/app -------------------------------------------------------------------------------- /api/app/.env-example: -------------------------------------------------------------------------------- 1 | # SERVER 2 | API_URL=http://localhost/ 3 | # several separate with , 4 | FILE_MANAGER_BEARER_TOKEN=very_secret,any_more_secret_tokens 5 | # several separate with , 6 | CORS_ORIGINS=http://localhost,http://ff.etomer.io 7 | 8 | 9 | # Docs 10 | # leave redoc_url or docs_url None or place any url name you want redoc to be seen for example '/redoc' 11 | redoc_url=None 12 | docs_url=/docs 13 | 14 | 15 | # STORAGE 16 | # supported: local, google, s3 17 | PREFERED_STORAGE=local 18 | MULTIPLE_FILE_UPLOAD_LIMIT=6 19 | # save original file or nah True/False 20 | SAVE_ORIGINAL=False 21 | 22 | # google storage 23 | GOOGLE_STORAGE_KEY_FILE=./key.json 24 | GOOGLE_CLIENT_EMAIL=secret_client 25 | DEFAULT_BUCKET_NAME=static.google.com 26 | GOOGLE_BUCKET_URL=https://static.google.com/ 27 | 28 | # google container paths 29 | VIDEO_ORIGINAL_GOOGLE_CLOUD_PATH=videos/original/ 30 | VIDEO_OPTIMIZED_GOOGLE_CLOUD_PATH=videos/optimized/ 31 | IMAGE_ORIGINAL_GOOGLE_CLOUD_PATH=pictures/original/ 32 | IMAGE_THUMBNAIL_GOOGLE_CLOUD_PATH=pictures/thumbnail/ 33 | QR_IMAGE_GOOGLE_CLOUD_PATH=pictures/qr/ 34 | 35 | # S3 storage 36 | AWS_ACCESS_KEY_ID=secret_key_id 37 | AWS_SECRET_ACCESS_KEY=secret_key 38 | AWS_DEFAULT_REGION=eu-central-1 39 | AWS_BUCKET=static-service 40 | AWS_BUCKET_URL=https://static.aws 41 | 42 | # S3 bucket paths 43 | VIDEO_ORIGINAL_S3_CLOUD_PATH=videos/not_streamable_videos/ 44 | VIDEO_OPTIMIZED_S3_CLOUD_PATH=videos/story/ 45 | IMAGE_ORIGINAL_S3_CLOUD_PATH=images/original/ 46 | IMAGE_THUMBNAIL_S3_CLOUD_PATH=images/thumbs/ 47 | QR_IMAGE_S3_CLOUD_PATH=images/qr/ 48 | 49 | # local storage paths 50 | IMAGE_ORIGINAL_LOCAL_PATH=static/pictures/original/ 51 | IMAGE_THUMBNAIL_LOCAL_PATH=static/pictures/thumbnail/ 52 | VIDEO_ORIGINAL_LOCAL_PATH=static/videos/original/ 53 | VIDEO_OPTIMIZED_LOCAL_PATH=static/videos/optimized/ 54 | QR_IMAGE_LOGO_PATH=static/logo/logo.png 55 | QR_IMAGE_LOCAL_PATH=static/qr/ 56 | 57 | # IMAGES 58 | IMAGE_CONVERTING_PREFERED_FORMAT=webp 59 | IMAGE_AllOWED_FILE_FORMAT=png,jpeg,jpg,webp 60 | # make thumbnails or nah True/False 61 | IMAGE_THUMBNAIL=True 62 | THUMBNAIL_MAX_WIDHT=320 63 | # suported:pillow-simd,ffmpeg 64 | # default:pillow-simd 65 | IMAGE_OPTIMIZATION_USING=pillow-simd 66 | 67 | 68 | # VIDEOS 69 | # Video optimization only with ffmpeg 70 | VIDEO_OPTIMIZE=True 71 | # Based on ffmpeg mov, mp4, m4a, 3gp, 3g2, mj2, psp, m4b, ism, ismv, isma, f4v 72 | VIDEO_AllOWED_FILE_FORMAT=mp4 73 | # FFMPEG config is for mp4/webm you can add any format you want 74 | VIDEO_DESIRED_FILE_FORMAT=webm 75 | 76 | 77 | # QRCODES 78 | QR_IMAGE_WITH_LOGO=True 79 | -------------------------------------------------------------------------------- /api/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, File, UploadFile, BackgroundTasks, Depends, HTTPException,status,Query 2 | from fastapi.responses import FileResponse 3 | from fastapi.middleware.cors import CORSMiddleware 4 | from fastapi.security import HTTPBearer,OAuth2AuthorizationCodeBearer,HTTPBasicCredentials 5 | from fastapi.staticfiles import StaticFiles 6 | from fastapi.middleware.cors import CORSMiddleware 7 | 8 | from dotenv import load_dotenv 9 | from typing import List,Optional 10 | import os 11 | import sys 12 | 13 | from services.serveUploadedFiles import handle_upload_image_file, handle_multiple_image_file_uploads, handle_upload_video_file 14 | from services.serveQrcode import handle_qr_code 15 | from services.security.customBearerCheck import validate_token 16 | from services.storage.local import response_image_file 17 | from services.serveDataFromUrl import handle_download_data_from_url, handle_multiple_image_file_downloads 18 | 19 | load_dotenv() 20 | app = FastAPI(docs_url=None if os.environ.get('docs_url') == 'None' else '/docs', redoc_url=None if os.environ.get('redoc_url') == 'None' else '/redoc') 21 | 22 | # If you want to serve files from local server you need to mount your static file directory 23 | if os.environ.get('PREFERED_STORAGE') == 'local' and 'pytest' not in sys.modules.keys(): 24 | app.mount("/static", StaticFiles(directory="static"), name="static") 25 | 26 | # If you want cors configuration also possible thanks to fast-api 27 | origins = os.environ.get('CORS_ORIGINS').split(',') 28 | 29 | app.add_middleware( 30 | CORSMiddleware, 31 | allow_origins=origins, 32 | allow_credentials=True, 33 | allow_methods=["*"], 34 | allow_headers=["*"], 35 | ) 36 | 37 | @app.get("/", tags=["main"]) 38 | def root( 39 | cpu_load: Optional[str] = Query( 40 | False, 41 | description='True/False depending your needs, gets average CPU load value', 42 | regex='^(True|False)$' 43 | ), 44 | token: str = Depends(validate_token)): 45 | 46 | result = { 47 | "Hello": f"Token is {token}", 48 | } 49 | 50 | if cpu_load == 'True': 51 | result['cpu_average_load'] = os.getloadavg() 52 | return result 53 | 54 | 55 | # File size validates NGINX 56 | @app.post("/image", tags=["image"]) 57 | async def upload_image_file( 58 | thumbnail: Optional[str] = Query( 59 | os.environ.get('IMAGE_THUMBNAIL'), 60 | description='True/False depending your needs', 61 | regex='^(True|False)$' 62 | ), 63 | file: UploadFile = File(...), 64 | OAuth2AuthorizationCodeBearer = Depends(validate_token)): 65 | return handle_upload_image_file(True if thumbnail == 'True' else False, file) 66 | 67 | 68 | @app.post("/images", tags=["image"]) 69 | async def upload_image_files( 70 | thumbnail: Optional[str] = Query( 71 | os.environ.get('IMAGE_THUMBNAIL'), 72 | description='True/False depending your needs', 73 | regex='^(True|False)$' 74 | ), 75 | files: List[UploadFile] = File(...), 76 | OAuth2AuthorizationCodeBearer = Depends(validate_token) 77 | ): 78 | fileAmount = len(files) 79 | if fileAmount > int(os.environ.get('MULTIPLE_FILE_UPLOAD_LIMIT')): 80 | raise HTTPException( 81 | status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, 82 | detail='Amount of files must not be more than {}'.format(os.environ.get('MULTIPLE_FILE_UPLOAD_LIMIT')) 83 | ) 84 | return handle_multiple_image_file_uploads(files, fileAmount, True if thumbnail == 'True' else False) 85 | 86 | 87 | @app.get("/image", tags=["image"]) 88 | async def get_image( 89 | image: str = Query(..., 90 | description='uploaded image name', 91 | max_length=50 92 | ), 93 | image_type: str = Query( 94 | ..., 95 | description='Should provide verision of image you want from localStorage original, thumbnail or qrImage', 96 | regex='^(original|thumbnail|qrImage)$' 97 | ), 98 | OAuth2AuthorizationCodeBearer = Depends(validate_token) 99 | ): 100 | return response_image_file(image, image_type) 101 | 102 | 103 | @app.post("/qrImage", tags=["image"]) 104 | async def text_to_generate_qr_image( 105 | qr_text: str = Query( 106 | ..., 107 | description='Provide text to generate qr image', 108 | ), 109 | with_logo: Optional[str] = Query( 110 | os.environ.get('QR_IMAGE_WITH_LOGO'), 111 | description='True/False depending your needs default is {}'.format(os.environ.get('QR_IMAGE_WITH_LOGO')), 112 | regex='^(True|False)$' 113 | ), 114 | OAuth2AuthorizationCodeBearer = Depends(validate_token)): 115 | return handle_qr_code(qr_text, True if with_logo == 'True' else False) 116 | 117 | 118 | @app.post("/video", tags=["video"]) 119 | async def upload_video_file( 120 | optimize: Optional[str] = Query( 121 | os.environ.get('VIDEO_OPTIMIZE'), 122 | description='True/False depending your needs default is {}'.format(os.environ.get('VIDEO_OPTIMIZE')), 123 | regex='^(True|False)$' 124 | ), 125 | file: UploadFile = File(..., description='Allows mov, mp4, m4a, 3gp, 3g2, mj2'), 126 | OAuth2AuthorizationCodeBearer = Depends(validate_token)): 127 | return handle_upload_video_file(True if optimize == 'True' else False, file) 128 | 129 | 130 | @app.get("/imageUrl", tags=["from url"]) 131 | async def image_from_url( 132 | image_url: str = Query( 133 | None, 134 | description = "Pass valid image url to upload", 135 | min_length = 5 136 | ), 137 | thumbnail: Optional[str] = Query( 138 | os.environ.get('IMAGE_THUMBNAIL'), 139 | description='True/False depending your needs', 140 | regex='^(True|False)$' 141 | ), 142 | OAuth2AuthorizationCodeBearer = Depends(validate_token)): 143 | return handle_download_data_from_url(image_url, True if thumbnail == 'True' else False, file_type='image') 144 | 145 | 146 | @app.get("/imageUrls", tags=["from url"]) 147 | async def images_from_urls( 148 | image_urls: List[str] = Query( 149 | None, 150 | description = "Pass valid image urls to upload", 151 | min_length = 5 152 | ), 153 | OAuth2AuthorizationCodeBearer = Depends(validate_token)): 154 | fileAmount = len(image_urls) 155 | if fileAmount > int(os.environ.get('MULTIPLE_FILE_UPLOAD_LIMIT')): 156 | raise HTTPException( 157 | status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, 158 | detail='Amount of files must not be more than {}'.format(os.environ.get('MULTIPLE_FILE_UPLOAD_LIMIT')) 159 | ) 160 | return handle_multiple_image_file_downloads(image_urls, fileAmount) 161 | 162 | 163 | @app.get("/videoUrl", tags=["from url"]) 164 | async def video_from_url( 165 | video_url: str = Query( 166 | None, 167 | description = "Pass valid video url to upload", 168 | min_length = 5 169 | ), 170 | optimize: Optional[str] = Query( 171 | os.environ.get('VIDEO_OPTIMIZE'), 172 | description='True/False depending your needs default is {}'.format(os.environ.get('VIDEO_OPTIMIZE')), 173 | regex='^(True|False)$' 174 | ), 175 | OAuth2AuthorizationCodeBearer = Depends(validate_token)): 176 | return handle_download_data_from_url(video_url, False, True if optimize == 'True' else False, file_type='video') -------------------------------------------------------------------------------- /api/app/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/services/__init__.py -------------------------------------------------------------------------------- /api/app/services/helpers/alena.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Alena will manage directories 3 | ''' 4 | import os 5 | from pathlib import Path 6 | 7 | 8 | def cleaning_service(pathsToClean, images = False, videos = False): 9 | if images: 10 | if pathsToClean.get('original'): 11 | if Path(os.environ.get('IMAGE_ORIGINAL_LOCAL_PATH') + pathsToClean['original']).is_file(): 12 | Path(os.environ.get('IMAGE_ORIGINAL_LOCAL_PATH') + pathsToClean['original']).unlink() 13 | if pathsToClean.get('thumbnail'): 14 | if Path(os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH') + pathsToClean['thumbnail']).is_file(): 15 | Path(os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH') + pathsToClean['thumbnail']).unlink() 16 | if pathsToClean.get('qrImage'): 17 | if Path(os.environ.get('QR_IMAGE_LOCAL_PATH') + pathsToClean['qrImage']).is_file(): 18 | Path(os.environ.get('QR_IMAGE_LOCAL_PATH') + pathsToClean['qrImage']).unlink() 19 | elif videos: 20 | if pathsToClean.get('original'): 21 | if Path(os.environ.get('VIDEO_ORIGINAL_LOCAL_PATH') + pathsToClean['original']).is_file(): 22 | Path(os.environ.get('VIDEO_ORIGINAL_LOCAL_PATH') + pathsToClean['original']).unlink() 23 | if pathsToClean.get('optimized'): 24 | if Path(os.environ.get('VIDEO_OPTIMIZED_LOCAL_PATH') + pathsToClean['optimized']).is_file(): 25 | Path(os.environ.get('VIDEO_OPTIMIZED_LOCAL_PATH') + pathsToClean['optimized']).unlink() 26 | 27 | def local_savings(images = False, videos = False, qrCodes = False): 28 | if images: 29 | Path(os.environ.get('IMAGE_ORIGINAL_LOCAL_PATH')).mkdir(parents=True, exist_ok=True) 30 | Path(os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH')).mkdir(parents=True, exist_ok=True) 31 | elif videos: 32 | Path(os.environ.get('VIDEO_ORIGINAL_LOCAL_PATH')).mkdir(parents=True, exist_ok=True) 33 | Path(os.environ.get('VIDEO_OPTIMIZED_LOCAL_PATH')).mkdir(parents=True, exist_ok=True) 34 | elif qrCodes: 35 | Path(os.environ.get('QR_IMAGE_LOCAL_PATH')).mkdir(parents=True, exist_ok=True) 36 | -------------------------------------------------------------------------------- /api/app/services/helpers/detectFileExtension.py: -------------------------------------------------------------------------------- 1 | import magic 2 | import mimetypes 3 | from pathlib import Path 4 | from fastapi import HTTPException,status 5 | 6 | def magic_extensions(file_path: Path): 7 | mime = magic.Magic(mime=True) 8 | detectedExtension = mimetypes.guess_all_extensions(mime.from_buffer(open(file_path, "rb").read(2048))) 9 | if len(detectedExtension) > 0: 10 | return detectedExtension[0] 11 | else: 12 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='File extension not detected') 13 | -------------------------------------------------------------------------------- /api/app/services/helpers/uniqueFileName.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | 4 | def generate_unique_name(extension, desiredExtension = False): 5 | unique = uuid4().hex 6 | # First goes original, second is thumbnail with desiredExtension 7 | return unique + '.' + extension, unique + '.' + desiredExtension if desiredExtension else desiredExtension -------------------------------------------------------------------------------- /api/app/services/images/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/services/images/__init__.py -------------------------------------------------------------------------------- /api/app/services/images/generateQr.py: -------------------------------------------------------------------------------- 1 | import os 2 | import qrcode 3 | from PIL import Image 4 | from pathlib import Path 5 | from ..helpers.uniqueFileName import generate_unique_name 6 | 7 | 8 | def qr_code_image(text = str, with_logo = bool): 9 | print(with_logo) 10 | qrImagePIL = qrcode.QRCode( 11 | error_correction=qrcode.constants.ERROR_CORRECT_H, 12 | border=2, 13 | ) 14 | 15 | qrImagePIL.add_data(text) 16 | 17 | qrImagePIL.make() 18 | 19 | qrImage = qrImagePIL.make_image().convert('RGB') 20 | if with_logo: 21 | logo = Image.open(os.environ.get('QR_IMAGE_LOGO_PATH')) 22 | qrImage.paste(logo, ((qrImage.size[0] - logo.size[0]) // 2, (qrImage.size[1] - logo.size[1]) // 2)) 23 | 24 | qrUniqueName = generate_unique_name('png')[0] 25 | qrImage.save(os.environ.get('QR_IMAGE_LOCAL_PATH') + qrUniqueName) 26 | 27 | return { 28 | 'qrImage' : qrUniqueName 29 | } 30 | -------------------------------------------------------------------------------- /api/app/services/images/resize.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ffmpeg 3 | from PIL import Image 4 | from uuid import uuid4 5 | from pathlib import Path 6 | from fastapi import HTTPException, status 7 | from ..helpers.alena import local_savings 8 | from ..helpers.uniqueFileName import generate_unique_name 9 | 10 | 11 | def resize_image(temp_stored_file: Path, extension: str, thumbnail: bool, desiredExtension: str): 12 | if not thumbnail and not os.environ.get('SAVE_ORIGINAL') == 'True': 13 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='Save original is dissabled, contact admin') 14 | 15 | local_savings(images=True) 16 | 17 | if os.environ.get('IMAGE_OPTIMIZATION_USING') == 'ffmpeg': 18 | return resize_image_pillow_FFMPEG(temp_stored_file, extension, thumbnail, desiredExtension) 19 | return resize_image_pillow_SIMD(temp_stored_file, extension, thumbnail, desiredExtension) 20 | 21 | 22 | def resize_image_pillow_SIMD(temp_stored_file: Path, extension: str, thumbnail: bool, desiredExtension: str): 23 | 24 | try: 25 | origin, thumb = generate_unique_name(extension, desiredExtension) 26 | img = Image.open(temp_stored_file) 27 | if os.environ.get('SAVE_ORIGINAL') == 'True': 28 | img.save(Path(os.environ.get('IMAGE_ORIGINAL_LOCAL_PATH') + origin).absolute()) 29 | else: 30 | origin = None 31 | if thumbnail: 32 | resize_width = int(os.environ.get('THUMBNAIL_MAX_WIDHT')) 33 | wpercent = (resize_width/float(img.size[0])) 34 | hsize = int((float(img.size[1])*float(wpercent))) 35 | img.thumbnail((resize_width,hsize), Image.BICUBIC) 36 | img.save(Path(os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH') + thumb).absolute()) 37 | else: 38 | thumb = None 39 | return { 40 | 'original': origin, 41 | 'thumbnail': thumb 42 | } 43 | except: 44 | raise HTTPException(status_code=503, detail="Image manipulation failed using pillow-SIMD") 45 | 46 | def resize_image_pillow_FFMPEG(temp_stored_file: Path, extension: str, thumbnail: bool, desiredExtension: str): 47 | try: 48 | origin, thumb = generate_unique_name(extension, desiredExtension) 49 | # Save original (reduces size magically) 50 | if os.environ.get('SAVE_ORIGINAL') == 'True': 51 | ( 52 | ffmpeg 53 | .input(temp_stored_file) 54 | .output(os.environ.get('IMAGE_ORIGINAL_LOCAL_PATH') + origin) 55 | .run(quiet=True) 56 | ) 57 | else: 58 | origin = None 59 | if thumbnail: 60 | # Resizes and Save 61 | ( 62 | ffmpeg 63 | .input(temp_stored_file) 64 | .filter("scale", os.environ.get('THUMBNAIL_MAX_WIDHT'), "-1") 65 | .output(os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH') + thumb) 66 | .run(quiet=True) 67 | ) 68 | else: 69 | thumb = None 70 | return { 71 | 'original': origin, 72 | 'thumbnail': thumb 73 | } 74 | except: 75 | raise HTTPException(status_code=503, detail="Image manipulation failed using FFMPEG") -------------------------------------------------------------------------------- /api/app/services/security/customBearerCheck.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import secrets 4 | from fastapi.security import HTTPBearer,OAuth2AuthorizationCodeBearer,HTTPBasicCredentials 5 | from fastapi import Depends, HTTPException, status 6 | security = HTTPBearer() 7 | 8 | def validate_token(credentials: HTTPBasicCredentials = Depends(security)): 9 | 10 | for eachKey in os.environ.get('FILE_MANAGER_BEARER_TOKEN').split(','): 11 | if secrets.compare_digest(credentials.credentials, eachKey): 12 | return True 13 | else: 14 | raise HTTPException( 15 | status_code=status.HTTP_401_UNAUTHORIZED, 16 | detail="Incorrect token" 17 | ) 18 | -------------------------------------------------------------------------------- /api/app/services/serveDataFromUrl.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import concurrent.futures 3 | from typing import List 4 | from fastapi import HTTPException,status,Query 5 | from .serveUploadedFiles import handle_upload_image_file,handle_upload_video_file 6 | import sys 7 | 8 | def handle_download_data_from_url(url = str, thumbnail = bool, optimize = None, file_type = str): 9 | try: 10 | get_data = requests.get(url, stream = True) 11 | if get_data.status_code == 200: 12 | get_data.raw.decode_content = True 13 | if file_type == 'image': 14 | return handle_upload_image_file(thumbnail, upload_file=None, raw_data_file=get_data) 15 | elif file_type =='video': 16 | return handle_upload_video_file(optimize, upload_file=None, raw_data_file=get_data) 17 | else: 18 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f'Unsuccessful request for: {url}') 19 | except: 20 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f'Unable to download data from: {url}') 21 | 22 | 23 | def handle_multiple_image_file_downloads(URL_FILES: List[Query], workers: int, thumbnail = bool): 24 | # We can use a with statement to ensure threads are cleaned up promptly 25 | with concurrent.futures.ThreadPoolExecutor(max_workers = workers) as executor: 26 | 27 | future_to_url = {executor.submit(handle_download_data_from_url, eachFile, thumbnail, file_type = 'image'): eachFile for eachFile in URL_FILES} 28 | result = [] 29 | for future in concurrent.futures.as_completed(future_to_url): 30 | try: 31 | result.append(future.result()) 32 | except: 33 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='Multiple upload failed') 34 | return result -------------------------------------------------------------------------------- /api/app/services/serveQrcode.py: -------------------------------------------------------------------------------- 1 | import os 2 | import copy 3 | import _thread 4 | from fastapi import HTTPException,status 5 | 6 | from .helpers.alena import local_savings 7 | from .images.generateQr import qr_code_image 8 | from .storage.googleCloud import upload_image_file_to_google_storage 9 | from .storage.s3 import upload_image_file_to_s3_storage 10 | 11 | def handle_qr_code(text = str, with_logo = bool): 12 | try: 13 | local_savings(qrCodes=True) 14 | 15 | qrCodePaths = qr_code_image(text, with_logo) 16 | 17 | if os.environ.get('PREFERED_STORAGE') == 'google': 18 | _thread.start_new_thread(upload_image_file_to_google_storage, (copy.deepcopy(qrCodePaths),)) 19 | qrCodePaths['qrImage'] = os.environ.get('GOOGLE_BUCKET_URL') + os.environ.get('QR_IMAGE_GOOGLE_CLOUD_PATH') + qrCodePaths['qrImage'] if qrCodePaths.get('qrImage') else None 20 | 21 | elif os.environ.get('PREFERED_STORAGE') == 's3': 22 | _thread.start_new_thread(upload_image_file_to_s3_storage, (copy.deepcopy(qrCodePaths),)) 23 | qrCodePaths['qrImage'] = os.environ.get('AWS_BUCKET_URL') + os.environ.get('QR_IMAGE_S3_CLOUD_PATH') + qrCodePaths['qrImage'] if qrCodePaths.get('qrImage') else None 24 | 25 | elif os.environ.get('PREFERED_STORAGE') == 'local': 26 | qrCodePaths['qrImage'] = os.environ.get('API_URL') + os.environ.get('QR_IMAGE_LOCAL_PATH') + qrCodePaths['qrImage'] if qrCodePaths.get('qrImage') else None 27 | 28 | qrCodePaths['storage'] = os.environ.get('PREFERED_STORAGE') 29 | return qrCodePaths 30 | except: 31 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='The file format not supported') -------------------------------------------------------------------------------- /api/app/services/serveUploadedFiles.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import _thread 4 | import copy 5 | from typing import List 6 | from pathlib import Path 7 | import concurrent.futures 8 | from .images.resize import resize_image 9 | from .videos.optimize import video_file_FFMPEG 10 | from tempfile import NamedTemporaryFile 11 | from fastapi import UploadFile,HTTPException,status 12 | from .storage.googleCloud import upload_image_file_to_google_storage,upload_video_file_to_google_storage 13 | from .storage.s3 import upload_image_file_to_s3_storage,upload_video_file_to_s3_storage 14 | from .helpers.detectFileExtension import magic_extensions 15 | import sys 16 | 17 | def save_upload_file_tmp(upload_file: None, raw_data_file = None) -> Path: 18 | try: 19 | if raw_data_file: 20 | with NamedTemporaryFile(delete=False, suffix=None) as tmp: 21 | shutil.copyfileobj(raw_data_file.raw, tmp) 22 | 23 | else: 24 | with NamedTemporaryFile(delete=False, suffix=None) as tmp: 25 | shutil.copyfileobj(upload_file.file, tmp) 26 | 27 | extension = magic_extensions(Path(tmp.name)) 28 | final_temp_file = tmp.name + extension 29 | os.rename(Path(tmp.name), final_temp_file) 30 | 31 | except: 32 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='Impossible to manipulate the file') 33 | finally: 34 | if upload_file: 35 | upload_file.file.close() 36 | else: 37 | raw_data_file.close() 38 | return Path(final_temp_file), extension 39 | 40 | 41 | def handle_upload_image_file(thumbnail, upload_file: None, raw_data_file = None): 42 | try: 43 | tmp_path, file_extension = save_upload_file_tmp(upload_file, raw_data_file) 44 | 45 | if file_extension[1:] in os.environ.get('IMAGE_AllOWED_FILE_FORMAT').split(','): 46 | 47 | imagePaths = resize_image(tmp_path, file_extension[1:], thumbnail, os.environ.get('IMAGE_CONVERTING_PREFERED_FORMAT')) 48 | 49 | if os.environ.get('PREFERED_STORAGE') == 'google': 50 | _thread.start_new_thread(upload_image_file_to_google_storage, (copy.deepcopy(imagePaths),)) 51 | imagePaths['original'] = os.environ.get('GOOGLE_BUCKET_URL') + os.environ.get('IMAGE_ORIGINAL_GOOGLE_CLOUD_PATH') + imagePaths['original'] if imagePaths.get('original') else None 52 | imagePaths['thumbnail'] = os.environ.get('GOOGLE_BUCKET_URL') + os.environ.get('IMAGE_THUMBNAIL_GOOGLE_CLOUD_PATH') + imagePaths['thumbnail'] if imagePaths.get('thumbnail') else None 53 | 54 | elif os.environ.get('PREFERED_STORAGE') == 's3': 55 | _thread.start_new_thread(upload_image_file_to_s3_storage, (copy.deepcopy(imagePaths),)) 56 | imagePaths['original'] = os.environ.get('AWS_BUCKET_URL') + os.environ.get('IMAGE_ORIGINAL_S3_CLOUD_PATH') + imagePaths['original'] if imagePaths.get('original') else None 57 | imagePaths['thumbnail'] = os.environ.get('AWS_BUCKET_URL') + os.environ.get('IMAGE_THUMBNAIL_S3_CLOUD_PATH') + imagePaths['thumbnail'] if imagePaths.get('thumbnail') else None 58 | 59 | elif os.environ.get('PREFERED_STORAGE') == 'local': 60 | imagePaths['original'] = os.environ.get('API_URL') + os.environ.get('IMAGE_ORIGINAL_LOCAL_PATH') + imagePaths['original'] if imagePaths.get('original') else None 61 | imagePaths['thumbnail'] = os.environ.get('API_URL') + os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH') + imagePaths['thumbnail'] if imagePaths.get('thumbnail') else None 62 | 63 | imagePaths['storage'] = os.environ.get('PREFERED_STORAGE') 64 | imagePaths['file_name'] = imagePaths['original'].split('/')[-1] 65 | return imagePaths 66 | else: 67 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='The file format not supported') 68 | finally: 69 | tmp_path.unlink() # Delete the temp file 70 | 71 | 72 | def handle_multiple_image_file_uploads(FILES: List[UploadFile], workers: int, thumbnail: bool): 73 | # We can use a with statement to ensure threads are cleaned up promptly 74 | with concurrent.futures.ThreadPoolExecutor(max_workers = workers) as executor: 75 | future_to_url = {executor.submit(handle_upload_image_file, eachFile, thumbnail): eachFile for eachFile in FILES} 76 | result = [] 77 | for future in concurrent.futures.as_completed(future_to_url): 78 | try: 79 | result.append(future.result()) 80 | except: 81 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='Multiple upload failed') 82 | return result 83 | 84 | 85 | def handle_upload_video_file(optimize, upload_file: None, raw_data_file = None): 86 | 87 | try: 88 | tmp_path, file_extension = save_upload_file_tmp(upload_file, raw_data_file) 89 | 90 | if file_extension[1:] in os.environ.get('VIDEO_AllOWED_FILE_FORMAT').split(','): 91 | videoPaths = video_file_FFMPEG(tmp_path, optimize) 92 | 93 | if os.environ.get('PREFERED_STORAGE') == 'google': 94 | 95 | _thread.start_new_thread(upload_video_file_to_google_storage, (copy.deepcopy(videoPaths),)) 96 | videoPaths['original'] = os.environ.get('GOOGLE_BUCKET_URL') + os.environ.get('VIDEO_ORIGINAL_GOOGLE_CLOUD_PATH') + videoPaths['original'] if videoPaths.get('original') else None 97 | videoPaths['optimized'] = os.environ.get('GOOGLE_BUCKET_URL') + os.environ.get('VIDEO_OPTIMIZED_GOOGLE_CLOUD_PATH') + videoPaths['optimized'] if videoPaths.get('optimized') else None 98 | 99 | elif os.environ.get('PREFERED_STORAGE') == 's3': 100 | 101 | _thread.start_new_thread(upload_video_file_to_s3_storage, (copy.deepcopy(videoPaths),)) 102 | videoPaths['original'] = os.environ.get('AWS_BUCKET_URL') + os.environ.get('VIDEO_ORIGINAL_S3_CLOUD_PATH') + videoPaths['original'] if videoPaths.get('original') else None 103 | videoPaths['optimized'] = os.environ.get('AWS_BUCKET_URL') + os.environ.get('VIDEO_OPTIMIZED_S3_CLOUD_PATH') + videoPaths['optimized'] if videoPaths.get('optimized') else None 104 | 105 | elif os.environ.get('PREFERED_STORAGE') == 'local': 106 | videoPaths['original'] = os.environ.get('API_URL') + os.environ.get('VIDEO_ORIGINAL_LOCAL_PATH') + videoPaths['original'] if videoPaths.get('original') else None 107 | videoPaths['optimized'] = os.environ.get('API_URL') + os.environ.get('VIDEO_OPTIMIZED_LOCAL_PATH') + videoPaths['optimized'] if videoPaths.get('optimized') else None 108 | 109 | videoPaths['storage'] = os.environ.get('PREFERED_STORAGE') 110 | videoPaths['file_name'] = videoPaths['original'].split('/')[-1] 111 | 112 | return videoPaths 113 | else: 114 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='Not valid format') 115 | except: 116 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='Corrupted file') 117 | 118 | finally: 119 | tmp_path.unlink() # Delete the temp file -------------------------------------------------------------------------------- /api/app/services/storage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/services/storage/__init__.py -------------------------------------------------------------------------------- /api/app/services/storage/googleCloud.py: -------------------------------------------------------------------------------- 1 | import os 2 | from libcloud.storage.types import Provider 3 | from libcloud.storage.providers import get_driver 4 | from ..helpers.alena import cleaning_service 5 | 6 | def authorize_google(): 7 | cls = get_driver(Provider.GOOGLE_STORAGE) 8 | googleStorageDriver = cls(os.environ.get('GOOGLE_CLIENT_EMAIL'), os.environ.get('GOOGLE_STORAGE_KEY_FILE')) 9 | 10 | googleContainer = googleStorageDriver.get_container(os.environ.get('DEFAULT_BUCKET_NAME')) 11 | return googleStorageDriver, googleContainer 12 | 13 | 14 | def upload_video_file_to_google_storage(videoPaths: dict): 15 | googleCloudStorageDriver, container = authorize_google() 16 | 17 | if videoPaths.get('original'): 18 | with open('./' + os.environ.get('VIDEO_ORIGINAL_LOCAL_PATH') + videoPaths['original'], 'rb') as iterator: 19 | googleCloudStorageDriver.upload_object_via_stream(iterator=iterator, 20 | container=container, 21 | object_name=os.environ.get('VIDEO_ORIGINAL_GOOGLE_CLOUD_PATH') + videoPaths['original']) 22 | if videoPaths.get('optimized'): 23 | with open('./' + os.environ.get('VIDEO_OPTIMIZED_LOCAL_PATH') + videoPaths['optimized'], 'rb') as iterator: 24 | googleCloudStorageDriver.upload_object_via_stream(iterator=iterator, 25 | container=container, 26 | object_name=os.environ.get('VIDEO_OPTIMIZED_GOOGLE_CLOUD_PATH') + videoPaths['optimized']) 27 | return cleaning_service(videoPaths, videos=True) 28 | 29 | def upload_image_file_to_google_storage(imagePaths: dict): 30 | googleCloudStorageDriver, container = authorize_google() 31 | 32 | if imagePaths.get('original'): 33 | with open('./' + os.environ.get('IMAGE_ORIGINAL_LOCAL_PATH') + imagePaths['original'], 'rb') as iterator: 34 | googleCloudStorageDriver.upload_object_via_stream(iterator=iterator, 35 | container=container, 36 | object_name=os.environ.get('IMAGE_ORIGINAL_GOOGLE_CLOUD_PATH') + imagePaths['original']) 37 | if imagePaths.get('thumbnail'): 38 | with open('./' + os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH') + imagePaths['thumbnail'], 'rb') as iterator: 39 | googleCloudStorageDriver.upload_object_via_stream(iterator=iterator, 40 | container=container, 41 | object_name=os.environ.get('IMAGE_THUMBNAIL_GOOGLE_CLOUD_PATH') + imagePaths['thumbnail']) 42 | if imagePaths.get('qrImage'): 43 | with open('./' + os.environ.get('QR_IMAGE_LOCAL_PATH') + imagePaths['qrImage'], 'rb') as iterator: 44 | googleCloudStorageDriver.upload_object_via_stream(iterator=iterator, 45 | container=container, 46 | object_name=os.environ.get('QR_IMAGE_GOOGLE_CLOUD_PATH') + imagePaths['qrImage']) 47 | return cleaning_service(imagePaths, images=True) -------------------------------------------------------------------------------- /api/app/services/storage/local.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from fastapi.responses import FileResponse 4 | from fastapi import HTTPException,status 5 | 6 | 7 | def response_image_file(filename:str, image_type:str): 8 | validPath = { 9 | 'original': os.environ.get('IMAGE_ORIGINAL_LOCAL_PATH'), 10 | 'thumbnail': os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH'), 11 | 'qrImage': os.environ.get('QR_IMAGE_LOCAL_PATH'), 12 | } 13 | 14 | if not Path(validPath[image_type] + filename).is_file(): 15 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='File not found please recheck name') 16 | 17 | return FileResponse(validPath[image_type] + filename) 18 | -------------------------------------------------------------------------------- /api/app/services/storage/s3.py: -------------------------------------------------------------------------------- 1 | import os 2 | from libcloud.storage.types import Provider 3 | from libcloud.storage.providers import get_driver 4 | from ..helpers.alena import cleaning_service 5 | 6 | def authorize_aws_s3(): 7 | cls = get_driver(Provider.S3) 8 | awsS3storageDriver = cls( 9 | os.environ.get('AWS_ACCESS_KEY_ID'), 10 | os.environ.get('AWS_SECRET_ACCESS_KEY'), 11 | region = os.environ.get('AWS_DEFAULT_REGION') 12 | ) 13 | 14 | s3Bucket= awsS3storageDriver.get_container(container_name=os.environ.get('AWS_BUCKET')) 15 | extra = {'content_type': 'application/octet-stream'} 16 | 17 | return awsS3storageDriver, s3Bucket, extra 18 | 19 | 20 | def upload_video_file_to_s3_storage(videoPaths: dict): 21 | awsS3cloudStorageDriver, container, extra = authorize_aws_s3() 22 | 23 | if videoPaths.get('original'): 24 | with open('./' + os.environ.get('VIDEO_ORIGINAL_LOCAL_PATH') + videoPaths['original'], 'rb') as iterator: 25 | awsS3cloudStorageDriver.upload_object_via_stream(iterator=iterator, 26 | container=container, 27 | object_name=os.environ.get('VIDEO_ORIGINAL_S3_CLOUD_PATH') + videoPaths['original'], 28 | extra=extra) 29 | if videoPaths.get('optimized'): 30 | with open('./' + os.environ.get('VIDEO_OPTIMIZED_LOCAL_PATH') + videoPaths['optimized'], 'rb') as iterator: 31 | awsS3cloudStorageDriver.upload_object_via_stream(iterator=iterator, 32 | container=container, 33 | object_name=os.environ.get('VIDEO_OPTIMIZED_S3_CLOUD_PATH') + videoPaths['optimized'], 34 | extra=extra) 35 | return cleaning_service(videoPaths, videos=True) 36 | 37 | def upload_image_file_to_s3_storage(imagePaths: dict): 38 | awsS3cloudStorageDriver, container, extra = authorize_aws_s3() 39 | 40 | if imagePaths.get('original'): 41 | with open('./' + os.environ.get('IMAGE_ORIGINAL_LOCAL_PATH') + imagePaths['original'], 'rb') as iterator: 42 | awsS3cloudStorageDriver.upload_object_via_stream(iterator=iterator, 43 | container=container, 44 | object_name=os.environ.get('IMAGE_ORIGINAL_S3_CLOUD_PATH') + imagePaths['original'], 45 | extra=extra) 46 | if imagePaths.get('thumbnail'): 47 | with open('./' + os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH') + imagePaths['thumbnail'], 'rb') as iterator: 48 | awsS3cloudStorageDriver.upload_object_via_stream(iterator=iterator, 49 | container=container, 50 | object_name=os.environ.get('IMAGE_THUMBNAIL_S3_CLOUD_PATH') + imagePaths['thumbnail'], 51 | extra=extra) 52 | if imagePaths.get('qrImage'): 53 | with open('./' + os.environ.get('QR_IMAGE_LOCAL_PATH') + imagePaths['qrImage'], 'rb') as iterator: 54 | awsS3cloudStorageDriver.upload_object_via_stream(iterator=iterator, 55 | container=container, 56 | object_name=os.environ.get('QR_IMAGE_S3_CLOUD_PATH') + imagePaths['qrImage'], 57 | extra=extra) 58 | return cleaning_service(imagePaths, images=True) -------------------------------------------------------------------------------- /api/app/services/videos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/services/videos/__init__.py -------------------------------------------------------------------------------- /api/app/services/videos/optimize.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ffmpeg 3 | from PIL import Image 4 | from pathlib import Path 5 | from ..helpers.uniqueFileName import generate_unique_name 6 | from ..helpers.alena import local_savings 7 | from fastapi import HTTPException, status 8 | 9 | 10 | def video_file_FFMPEG(temp_stored_file: Path, optimize: bool): 11 | try: 12 | if not optimize and not os.environ.get('SAVE_ORIGINAL') == 'True': 13 | raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='Save original is dissabled, contact admin') 14 | 15 | local_savings(videos=True) 16 | origin, optimized = generate_unique_name(os.environ.get('VIDEO_AllOWED_FILE_FORMAT'), os.environ.get('VIDEO_DESIRED_FILE_FORMAT')) 17 | # Save original with config is ready for original file of mp4 or mov also decreases size by default 18 | if os.environ.get('SAVE_ORIGINAL') == 'True': 19 | ( 20 | ffmpeg 21 | .input(temp_stored_file) 22 | .output(os.environ.get('VIDEO_ORIGINAL_LOCAL_PATH') + origin, vcodec='h264', acodec='aac') 23 | .run(quiet=True) 24 | ) 25 | else: 26 | origin = None 27 | if optimize: 28 | audio = ffmpeg.input(temp_stored_file).audio 29 | video = ffmpeg.input(temp_stored_file).video.filter('scale', size='640x1136', force_original_aspect_ratio='decrease').filter('pad', '640', '1136', '(ow-iw)/2', '(oh-ih)/2') 30 | ffmpeg.concat(video, audio, v=1, a=1) 31 | # ffmpeg config for webm 32 | # Also is possible to use vcodec libvpx-vp9 but sometimes it increzes size needs testing may it suits you more 33 | # Check docs https://trac.ffmpeg.org/wiki/Encode/VP9 34 | if os.environ.get('VIDEO_DESIRED_FILE_FORMAT') == 'webm': 35 | out = ffmpeg.output(video, audio, os.environ.get('VIDEO_OPTIMIZED_LOCAL_PATH') + optimized, crf='10', qmin='0', qmax='50', video_bitrate='1M', vcodec='libvpx', acodec='libvorbis') 36 | # ffmpeg config for mp4 37 | elif os.environ.get('VIDEO_DESIRED_FILE_FORMAT') == 'mp4': 38 | out = ffmpeg.output(video, audio, os.environ.get('VIDEO_OPTIMIZED_LOCAL_PATH') + optimized, vcodec='h264', acodec='aac') 39 | out.run(quiet=True) 40 | else: 41 | optimized = None 42 | return { 43 | 'original': origin, 44 | 'optimized': optimized 45 | } 46 | except: 47 | raise HTTPException(status_code=503, detail="Video manipulation failed using FFMPEG") -------------------------------------------------------------------------------- /api/app/static/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/static/logo/logo.png -------------------------------------------------------------------------------- /api/app/static/pictures/original/afba38beae434b9fb4691bf8559947aa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/static/pictures/original/afba38beae434b9fb4691bf8559947aa.png -------------------------------------------------------------------------------- /api/app/static/pictures/original/dcb8ac79618540688ea36e688a8c3635.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/static/pictures/original/dcb8ac79618540688ea36e688a8c3635.png -------------------------------------------------------------------------------- /api/app/static/pictures/thumbnail/72014f9f91ab40c7b8df61ab350bcc71.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/static/pictures/thumbnail/72014f9f91ab40c7b8df61ab350bcc71.webp -------------------------------------------------------------------------------- /api/app/static/pictures/thumbnail/dcb8ac79618540688ea36e688a8c3635.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/static/pictures/thumbnail/dcb8ac79618540688ea36e688a8c3635.webp -------------------------------------------------------------------------------- /api/app/static/qr/04de739e41154172b8858146f4d8edfe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JexPY/filemanager-fastapi/da830fe6d9a3d515e0d04e6e690ff366225ec251/api/app/static/qr/04de739e41154172b8858146f4d8edfe.png -------------------------------------------------------------------------------- /api/app/test_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import AsyncClient 3 | from main import app 4 | from dotenv import load_dotenv 5 | from services.helpers.alena import cleaning_service 6 | from pathlib import Path 7 | import os 8 | import sys 9 | 10 | 11 | load_dotenv() 12 | headers = { 13 | 'Authorization': 'Bearer {}'.format(os.environ.get('FILE_MANAGER_BEARER_TOKEN')) 14 | } 15 | 16 | _ORIGINAL_IMAGE = open('api/app/static/pictures/original/dcb8ac79618540688ea36e688a8c3635.png', 'rb') 17 | _ORIGINAL_IMAGE_NAME = 'dcb8ac79618540688ea36e688a8c3635.png' 18 | 19 | @pytest.mark.asyncio 20 | async def test_root(): 21 | params = {'cpu_load': 'True'} 22 | async with AsyncClient(app=app, base_url=os.environ.get('API_URL'), headers=headers, params=params) as ac: 23 | response = await ac.get("/") 24 | 25 | if params.get('cpu_load') != 'True': 26 | assert response.json() == {"Hello": "Token is True"} 27 | assert response.status_code == 200 28 | 29 | @pytest.mark.asyncio 30 | async def test_upload_image_file(): 31 | params = {'thumbnail': 'True'} 32 | 33 | image_file = {'file': (_ORIGINAL_IMAGE_NAME, _ORIGINAL_IMAGE, 'image/png')} 34 | 35 | async with AsyncClient(app=app, base_url=os.environ.get('API_URL'), headers=headers, params=params) as ac: 36 | response = await ac.post("/image", files=image_file) 37 | 38 | assert response.status_code == 200 39 | 40 | imagePaths = { 41 | 'original' : response.json().get('thumbnail').split('/')[len(response.json().get('thumbnail').split('/')) - 1].split('.')[0] + '.png', 42 | 'thumbnail' : response.json().get('thumbnail').split('/')[len(response.json().get('thumbnail').split('/')) - 1] 43 | } 44 | assert Path(os.environ.get('IMAGE_THUMBNAIL_LOCAL_PATH') + '/' +imagePaths['thumbnail']).is_file() == True 45 | cleaning_service(imagePaths, images = True) -------------------------------------------------------------------------------- /api/gunicorn_conf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import multiprocessing 3 | import os 4 | 5 | workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") 6 | max_workers_str = os.getenv("MAX_WORKERS") 7 | use_max_workers = None 8 | if max_workers_str: 9 | use_max_workers = int(max_workers_str) 10 | web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) 11 | 12 | host = os.getenv("HOST", "0.0.0.0") 13 | port = os.getenv("PORT", "80") 14 | bind_env = os.getenv("BIND", None) 15 | use_loglevel = os.getenv("LOG_LEVEL", "info") 16 | if bind_env: 17 | use_bind = bind_env 18 | else: 19 | use_bind = f"{host}:{port}" 20 | 21 | cores = multiprocessing.cpu_count() 22 | workers_per_core = float(workers_per_core_str) 23 | default_web_concurrency = workers_per_core * cores 24 | if web_concurrency_str: 25 | web_concurrency = int(web_concurrency_str) 26 | assert web_concurrency > 0 27 | else: 28 | web_concurrency = max(int(default_web_concurrency), 2) 29 | if use_max_workers: 30 | web_concurrency = min(web_concurrency, use_max_workers) 31 | accesslog_var = os.getenv("ACCESS_LOG", "-") 32 | use_accesslog = accesslog_var or None 33 | errorlog_var = os.getenv("ERROR_LOG", "-") 34 | use_errorlog = errorlog_var or None 35 | graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120") 36 | timeout_str = os.getenv("TIMEOUT", "120") 37 | keepalive_str = os.getenv("KEEP_ALIVE", "5") 38 | 39 | # Gunicorn config variables 40 | loglevel = use_loglevel 41 | workers = web_concurrency 42 | bind = use_bind 43 | errorlog = use_errorlog 44 | worker_tmp_dir = "/dev/shm" 45 | accesslog = use_accesslog 46 | graceful_timeout = int(graceful_timeout_str) 47 | timeout = int(timeout_str) 48 | keepalive = int(keepalive_str) 49 | 50 | 51 | # For debugging and testing 52 | log_data = { 53 | "loglevel": loglevel, 54 | "workers": workers, 55 | "bind": bind, 56 | "graceful_timeout": graceful_timeout, 57 | "timeout": timeout, 58 | "keepalive": keepalive, 59 | "errorlog": errorlog, 60 | "accesslog": accesslog, 61 | # Additional, non-gunicorn variables 62 | "workers_per_core": workers_per_core, 63 | "use_max_workers": use_max_workers, 64 | "host": host, 65 | "port": port, 66 | } 67 | print(json.dumps(log_data)) -------------------------------------------------------------------------------- /api/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "File support for asyncio." 4 | name = "aiofiles" 5 | optional = false 6 | python-versions = "*" 7 | version = "0.5.0" 8 | 9 | [[package]] 10 | category = "main" 11 | description = "A standard Python library that abstracts away differences among multiple cloud provider APIs. For more information and documentation, please see https://libcloud.apache.org" 12 | name = "apache-libcloud" 13 | optional = false 14 | python-versions = ">=3.5.*, <4" 15 | version = "3.2.0" 16 | 17 | [package.dependencies] 18 | requests = ">=2.5.0" 19 | 20 | [[package]] 21 | category = "dev" 22 | description = "An abstract syntax tree for Python with inference support." 23 | name = "astroid" 24 | optional = false 25 | python-versions = ">=3.5" 26 | version = "2.4.2" 27 | 28 | [package.dependencies] 29 | lazy-object-proxy = ">=1.4.0,<1.5.0" 30 | six = ">=1.12,<2.0" 31 | wrapt = ">=1.11,<2.0" 32 | 33 | [[package]] 34 | category = "main" 35 | description = "Atomic file writes." 36 | marker = "sys_platform == \"win32\"" 37 | name = "atomicwrites" 38 | optional = false 39 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 40 | version = "1.4.0" 41 | 42 | [[package]] 43 | category = "main" 44 | description = "Classes Without Boilerplate" 45 | name = "attrs" 46 | optional = false 47 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 48 | version = "20.2.0" 49 | 50 | [package.extras] 51 | dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] 52 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 53 | tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 54 | tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 55 | 56 | [[package]] 57 | category = "main" 58 | description = "Python package for providing Mozilla's CA Bundle." 59 | name = "certifi" 60 | optional = false 61 | python-versions = "*" 62 | version = "2020.6.20" 63 | 64 | [[package]] 65 | category = "main" 66 | description = "Foreign Function Interface for Python calling C code." 67 | name = "cffi" 68 | optional = false 69 | python-versions = "*" 70 | version = "1.14.3" 71 | 72 | [package.dependencies] 73 | pycparser = "*" 74 | 75 | [[package]] 76 | category = "main" 77 | description = "Universal encoding detector for Python 2 and 3" 78 | name = "chardet" 79 | optional = false 80 | python-versions = "*" 81 | version = "3.0.4" 82 | 83 | [[package]] 84 | category = "main" 85 | description = "Composable command line interface toolkit" 86 | name = "click" 87 | optional = false 88 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 89 | version = "7.1.2" 90 | 91 | [[package]] 92 | category = "main" 93 | description = "Cross-platform colored terminal text." 94 | marker = "platform_system == \"Windows\" or sys_platform == \"win32\"" 95 | name = "colorama" 96 | optional = false 97 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 98 | version = "0.4.4" 99 | 100 | [[package]] 101 | category = "main" 102 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 103 | name = "cryptography" 104 | optional = false 105 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 106 | version = "3.1.1" 107 | 108 | [package.dependencies] 109 | cffi = ">=1.8,<1.11.3 || >1.11.3" 110 | six = ">=1.4.1" 111 | 112 | [package.extras] 113 | docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"] 114 | docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] 115 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 116 | ssh = ["bcrypt (>=3.1.5)"] 117 | test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] 118 | 119 | [[package]] 120 | category = "main" 121 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 122 | name = "fastapi" 123 | optional = false 124 | python-versions = ">=3.6" 125 | version = "0.60.2" 126 | 127 | [package.dependencies] 128 | pydantic = ">=0.32.2,<2.0.0" 129 | starlette = "0.13.6" 130 | 131 | [package.extras] 132 | all = ["requests (>=2.24.0,<3.0.0)", "aiofiles (>=0.5.0,<0.6.0)", "jinja2 (>=2.11.2,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<2.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "graphene (>=2.1.8,<3.0.0)", "ujson (>=3.0.0,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn (>=0.11.5,<0.12.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)"] 133 | dev = ["python-jose (>=3.1.0,<4.0.0)", "passlib (>=1.7.2,<2.0.0)", "autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn (>=0.11.5,<0.12.0)", "graphene (>=2.1.8,<3.0.0)"] 134 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.5.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.2.0)", "typer (>=0.3.0,<0.4.0)", "typer-cli (>=0.0.9,<0.0.10)", "pyyaml (>=5.3.1,<6.0.0)"] 135 | test = ["pytest (5.4.3)", "pytest-cov (2.10.0)", "pytest-asyncio (>=0.14.0,<0.15.0)", "mypy (0.782)", "black (19.10b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.15.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "databases (>=0.3.2,<0.4.0)", "orjson (>=3.2.1,<4.0.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "aiofiles (>=0.5.0,<0.6.0)", "flask (>=1.1.2,<2.0.0)"] 136 | 137 | [[package]] 138 | category = "main" 139 | description = "Python bindings for FFmpeg - with complex filtering support" 140 | name = "ffmpeg-python" 141 | optional = false 142 | python-versions = "*" 143 | version = "0.2.0" 144 | 145 | [package.dependencies] 146 | future = "*" 147 | 148 | [package.extras] 149 | dev = ["future (0.17.1)", "numpy (1.16.4)", "pytest-mock (1.10.4)", "pytest (4.6.1)", "Sphinx (2.1.0)", "tox (3.12.1)"] 150 | 151 | [[package]] 152 | category = "main" 153 | description = "Clean single-source support for Python 3 and 2" 154 | name = "future" 155 | optional = false 156 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 157 | version = "0.18.2" 158 | 159 | [[package]] 160 | category = "main" 161 | description = "WSGI HTTP Server for UNIX" 162 | name = "gunicorn" 163 | optional = false 164 | python-versions = ">=3.4" 165 | version = "20.0.4" 166 | 167 | [package.dependencies] 168 | setuptools = ">=3.0" 169 | 170 | [package.extras] 171 | eventlet = ["eventlet (>=0.9.7)"] 172 | gevent = ["gevent (>=0.13)"] 173 | setproctitle = ["setproctitle"] 174 | tornado = ["tornado (>=0.2)"] 175 | 176 | [[package]] 177 | category = "main" 178 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 179 | name = "h11" 180 | optional = false 181 | python-versions = "*" 182 | version = "0.9.0" 183 | 184 | [[package]] 185 | category = "main" 186 | description = "A minimal low-level HTTP client." 187 | name = "httpcore" 188 | optional = false 189 | python-versions = ">=3.6" 190 | version = "0.11.1" 191 | 192 | [package.dependencies] 193 | h11 = ">=0.8,<0.10" 194 | sniffio = ">=1.0.0,<2.0.0" 195 | 196 | [package.extras] 197 | http2 = ["h2 (>=3.0.0,<4.0.0)"] 198 | 199 | [[package]] 200 | category = "main" 201 | description = "A collection of framework independent HTTP protocol utils." 202 | marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" 203 | name = "httptools" 204 | optional = false 205 | python-versions = "*" 206 | version = "0.1.1" 207 | 208 | [package.extras] 209 | test = ["Cython (0.29.14)"] 210 | 211 | [[package]] 212 | category = "main" 213 | description = "The next generation HTTP client." 214 | name = "httpx" 215 | optional = false 216 | python-versions = ">=3.6" 217 | version = "0.15.5" 218 | 219 | [package.dependencies] 220 | certifi = "*" 221 | httpcore = ">=0.11.0,<0.12.0" 222 | sniffio = "*" 223 | 224 | [package.dependencies.rfc3986] 225 | extras = ["idna2008"] 226 | version = ">=1.3,<2" 227 | 228 | [package.extras] 229 | brotli = ["brotlipy (>=0.7.0,<0.8.0)"] 230 | http2 = ["h2 (>=3.0.0,<4.0.0)"] 231 | 232 | [[package]] 233 | category = "main" 234 | description = "Internationalized Domain Names in Applications (IDNA)" 235 | name = "idna" 236 | optional = false 237 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 238 | version = "2.10" 239 | 240 | [[package]] 241 | category = "main" 242 | description = "iniconfig: brain-dead simple config-ini parsing" 243 | name = "iniconfig" 244 | optional = false 245 | python-versions = "*" 246 | version = "1.1.1" 247 | 248 | [[package]] 249 | category = "dev" 250 | description = "A Python utility / library to sort Python imports." 251 | name = "isort" 252 | optional = false 253 | python-versions = ">=3.6,<4.0" 254 | version = "5.6.4" 255 | 256 | [package.extras] 257 | colors = ["colorama (>=0.4.3,<0.5.0)"] 258 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 259 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 260 | 261 | [[package]] 262 | category = "dev" 263 | description = "A fast and thorough lazy object proxy." 264 | name = "lazy-object-proxy" 265 | optional = false 266 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 267 | version = "1.4.3" 268 | 269 | [[package]] 270 | category = "dev" 271 | description = "McCabe checker, plugin for flake8" 272 | name = "mccabe" 273 | optional = false 274 | python-versions = "*" 275 | version = "0.6.1" 276 | 277 | [[package]] 278 | category = "main" 279 | description = "Core utilities for Python packages" 280 | name = "packaging" 281 | optional = false 282 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 283 | version = "20.4" 284 | 285 | [package.dependencies] 286 | pyparsing = ">=2.0.2" 287 | six = "*" 288 | 289 | [[package]] 290 | category = "main" 291 | description = "Python Imaging Library (Fork)" 292 | name = "pillow-simd" 293 | optional = false 294 | python-versions = ">=3.5" 295 | version = "7.0.0.post3" 296 | 297 | [[package]] 298 | category = "main" 299 | description = "plugin and hook calling mechanisms for python" 300 | name = "pluggy" 301 | optional = false 302 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 303 | version = "0.13.1" 304 | 305 | [package.extras] 306 | dev = ["pre-commit", "tox"] 307 | 308 | [[package]] 309 | category = "main" 310 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 311 | name = "py" 312 | optional = false 313 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 314 | version = "1.9.0" 315 | 316 | [[package]] 317 | category = "main" 318 | description = "C parser in Python" 319 | name = "pycparser" 320 | optional = false 321 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 322 | version = "2.20" 323 | 324 | [[package]] 325 | category = "main" 326 | description = "Data validation and settings management using python 3.6 type hinting" 327 | name = "pydantic" 328 | optional = false 329 | python-versions = ">=3.6" 330 | version = "1.6.1" 331 | 332 | [package.extras] 333 | dotenv = ["python-dotenv (>=0.10.4)"] 334 | email = ["email-validator (>=1.0.3)"] 335 | typing_extensions = ["typing-extensions (>=3.7.2)"] 336 | 337 | [[package]] 338 | category = "dev" 339 | description = "python code static checker" 340 | name = "pylint" 341 | optional = false 342 | python-versions = ">=3.5.*" 343 | version = "2.6.0" 344 | 345 | [package.dependencies] 346 | astroid = ">=2.4.0,<=2.5" 347 | colorama = "*" 348 | isort = ">=4.2.5,<6" 349 | mccabe = ">=0.6,<0.7" 350 | toml = ">=0.7.1" 351 | 352 | [[package]] 353 | category = "main" 354 | description = "Python parsing module" 355 | name = "pyparsing" 356 | optional = false 357 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 358 | version = "2.4.7" 359 | 360 | [[package]] 361 | category = "main" 362 | description = "pytest: simple powerful testing with Python" 363 | name = "pytest" 364 | optional = false 365 | python-versions = ">=3.5" 366 | version = "6.1.1" 367 | 368 | [package.dependencies] 369 | atomicwrites = ">=1.0" 370 | attrs = ">=17.4.0" 371 | colorama = "*" 372 | iniconfig = "*" 373 | packaging = "*" 374 | pluggy = ">=0.12,<1.0" 375 | py = ">=1.8.2" 376 | toml = "*" 377 | 378 | [package.extras] 379 | checkqa_mypy = ["mypy (0.780)"] 380 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 381 | 382 | [[package]] 383 | category = "main" 384 | description = "Pytest support for asyncio." 385 | name = "pytest-asyncio" 386 | optional = false 387 | python-versions = ">= 3.5" 388 | version = "0.14.0" 389 | 390 | [package.dependencies] 391 | pytest = ">=5.4.0" 392 | 393 | [package.extras] 394 | testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"] 395 | 396 | [[package]] 397 | category = "main" 398 | description = "Add .env support to your django/flask apps in development and deployments" 399 | name = "python-dotenv" 400 | optional = false 401 | python-versions = "*" 402 | version = "0.14.0" 403 | 404 | [package.extras] 405 | cli = ["click (>=5.0)"] 406 | 407 | [[package]] 408 | category = "main" 409 | description = "File type identification using libmagic" 410 | name = "python-magic" 411 | optional = false 412 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 413 | version = "0.4.18" 414 | 415 | [[package]] 416 | category = "main" 417 | description = "A streaming multipart parser for Python" 418 | name = "python-multipart" 419 | optional = false 420 | python-versions = "*" 421 | version = "0.0.5" 422 | 423 | [package.dependencies] 424 | six = ">=1.4.0" 425 | 426 | [[package]] 427 | category = "main" 428 | description = "QR Code image generator" 429 | name = "qrcode" 430 | optional = false 431 | python-versions = "*" 432 | version = "6.1" 433 | 434 | [package.dependencies] 435 | colorama = "*" 436 | six = "*" 437 | 438 | [package.extras] 439 | dev = ["tox", "pytest", "mock"] 440 | maintainer = ["zest.releaser"] 441 | pil = ["pillow"] 442 | test = ["pytest", "pytest-cov", "mock"] 443 | 444 | [[package]] 445 | category = "main" 446 | description = "Python HTTP for Humans." 447 | name = "requests" 448 | optional = false 449 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 450 | version = "2.24.0" 451 | 452 | [package.dependencies] 453 | certifi = ">=2017.4.17" 454 | chardet = ">=3.0.2,<4" 455 | idna = ">=2.5,<3" 456 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 457 | 458 | [package.extras] 459 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 460 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] 461 | 462 | [[package]] 463 | category = "main" 464 | description = "Validating URI References per RFC 3986" 465 | name = "rfc3986" 466 | optional = false 467 | python-versions = "*" 468 | version = "1.4.0" 469 | 470 | [package.dependencies] 471 | [package.dependencies.idna] 472 | optional = true 473 | version = "*" 474 | 475 | [package.extras] 476 | idna2008 = ["idna"] 477 | 478 | [[package]] 479 | category = "main" 480 | description = "Python 2 and 3 compatibility utilities" 481 | name = "six" 482 | optional = false 483 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 484 | version = "1.15.0" 485 | 486 | [[package]] 487 | category = "main" 488 | description = "Sniff out which async library your code is running under" 489 | name = "sniffio" 490 | optional = false 491 | python-versions = ">=3.5" 492 | version = "1.2.0" 493 | 494 | [[package]] 495 | category = "main" 496 | description = "The little ASGI library that shines." 497 | name = "starlette" 498 | optional = false 499 | python-versions = ">=3.6" 500 | version = "0.13.6" 501 | 502 | [package.extras] 503 | full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] 504 | 505 | [[package]] 506 | category = "main" 507 | description = "Python Library for Tom's Obvious, Minimal Language" 508 | name = "toml" 509 | optional = false 510 | python-versions = "*" 511 | version = "0.10.1" 512 | 513 | [[package]] 514 | category = "main" 515 | description = "HTTP library with thread-safe connection pooling, file post, and more." 516 | name = "urllib3" 517 | optional = false 518 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 519 | version = "1.25.10" 520 | 521 | [package.extras] 522 | brotli = ["brotlipy (>=0.6.0)"] 523 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] 524 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 525 | 526 | [[package]] 527 | category = "main" 528 | description = "The lightning-fast ASGI server." 529 | name = "uvicorn" 530 | optional = false 531 | python-versions = "*" 532 | version = "0.11.8" 533 | 534 | [package.dependencies] 535 | click = ">=7.0.0,<8.0.0" 536 | h11 = ">=0.8,<0.10" 537 | httptools = ">=0.1.0,<0.2.0" 538 | uvloop = ">=0.14.0" 539 | websockets = ">=8.0.0,<9.0.0" 540 | 541 | [package.extras] 542 | watchgodreload = ["watchgod (>=0.6,<0.7)"] 543 | 544 | [[package]] 545 | category = "main" 546 | description = "Fast implementation of asyncio event loop on top of libuv" 547 | marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" 548 | name = "uvloop" 549 | optional = false 550 | python-versions = "*" 551 | version = "0.14.0" 552 | 553 | [[package]] 554 | category = "main" 555 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 556 | name = "websockets" 557 | optional = false 558 | python-versions = ">=3.6.1" 559 | version = "8.1" 560 | 561 | [[package]] 562 | category = "dev" 563 | description = "Module for decorators, wrappers and monkey patching." 564 | name = "wrapt" 565 | optional = false 566 | python-versions = "*" 567 | version = "1.12.1" 568 | 569 | [metadata] 570 | content-hash = "8dd2a5a30b0bb71cfad68d92155723ab62e0121eff4fc88e84a743922df4366e" 571 | lock-version = "1.0" 572 | python-versions = "^3.8" 573 | 574 | [metadata.files] 575 | aiofiles = [ 576 | {file = "aiofiles-0.5.0-py3-none-any.whl", hash = "sha256:377fdf7815cc611870c59cbd07b68b180841d2a2b79812d8c218be02448c2acb"}, 577 | {file = "aiofiles-0.5.0.tar.gz", hash = "sha256:98e6bcfd1b50f97db4980e182ddd509b7cc35909e903a8fe50d8849e02d815af"}, 578 | ] 579 | apache-libcloud = [ 580 | {file = "apache-libcloud-3.2.0.tar.gz", hash = "sha256:1b14b1f5f91ceeff5cf228613e76577d7b41e790dccd53a0f647ef816fb5495c"}, 581 | {file = "apache_libcloud-3.2.0-py2.py3-none-any.whl", hash = "sha256:e41b8670b5b2cb9b8bf1557f099a0e75dcb0ed9e9b2d667a3a0c1fbec0fa11bb"}, 582 | ] 583 | astroid = [ 584 | {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, 585 | {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, 586 | ] 587 | atomicwrites = [ 588 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 589 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 590 | ] 591 | attrs = [ 592 | {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, 593 | {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, 594 | ] 595 | certifi = [ 596 | {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, 597 | {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, 598 | ] 599 | cffi = [ 600 | {file = "cffi-1.14.3-2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc"}, 601 | {file = "cffi-1.14.3-2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768"}, 602 | {file = "cffi-1.14.3-2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d"}, 603 | {file = "cffi-1.14.3-2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1"}, 604 | {file = "cffi-1.14.3-2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca"}, 605 | {file = "cffi-1.14.3-2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a"}, 606 | {file = "cffi-1.14.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c"}, 607 | {file = "cffi-1.14.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730"}, 608 | {file = "cffi-1.14.3-cp27-cp27m-win32.whl", hash = "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d"}, 609 | {file = "cffi-1.14.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05"}, 610 | {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b"}, 611 | {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171"}, 612 | {file = "cffi-1.14.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f"}, 613 | {file = "cffi-1.14.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4"}, 614 | {file = "cffi-1.14.3-cp35-cp35m-win32.whl", hash = "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d"}, 615 | {file = "cffi-1.14.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d"}, 616 | {file = "cffi-1.14.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3"}, 617 | {file = "cffi-1.14.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808"}, 618 | {file = "cffi-1.14.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537"}, 619 | {file = "cffi-1.14.3-cp36-cp36m-win32.whl", hash = "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0"}, 620 | {file = "cffi-1.14.3-cp36-cp36m-win_amd64.whl", hash = "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e"}, 621 | {file = "cffi-1.14.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1"}, 622 | {file = "cffi-1.14.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579"}, 623 | {file = "cffi-1.14.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394"}, 624 | {file = "cffi-1.14.3-cp37-cp37m-win32.whl", hash = "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc"}, 625 | {file = "cffi-1.14.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869"}, 626 | {file = "cffi-1.14.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e"}, 627 | {file = "cffi-1.14.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828"}, 628 | {file = "cffi-1.14.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9"}, 629 | {file = "cffi-1.14.3-cp38-cp38-win32.whl", hash = "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522"}, 630 | {file = "cffi-1.14.3-cp38-cp38-win_amd64.whl", hash = "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15"}, 631 | {file = "cffi-1.14.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d"}, 632 | {file = "cffi-1.14.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c"}, 633 | {file = "cffi-1.14.3-cp39-cp39-win32.whl", hash = "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b"}, 634 | {file = "cffi-1.14.3-cp39-cp39-win_amd64.whl", hash = "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3"}, 635 | {file = "cffi-1.14.3.tar.gz", hash = "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"}, 636 | ] 637 | chardet = [ 638 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 639 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 640 | ] 641 | click = [ 642 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 643 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 644 | ] 645 | colorama = [ 646 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 647 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 648 | ] 649 | cryptography = [ 650 | {file = "cryptography-3.1.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:65beb15e7f9c16e15934569d29fb4def74ea1469d8781f6b3507ab896d6d8719"}, 651 | {file = "cryptography-3.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:983c0c3de4cb9fcba68fd3f45ed846eb86a2a8b8d8bc5bb18364c4d00b3c61fe"}, 652 | {file = "cryptography-3.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:e97a3b627e3cb63c415a16245d6cef2139cca18bb1183d1b9375a1c14e83f3b3"}, 653 | {file = "cryptography-3.1.1-cp27-cp27m-win32.whl", hash = "sha256:cb179acdd4ae1e4a5a160d80b87841b3d0e0be84af46c7bb2cd7ece57a39c4ba"}, 654 | {file = "cryptography-3.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b372026ebf32fe2523159f27d9f0e9f485092e43b00a5adacf732192a70ba118"}, 655 | {file = "cryptography-3.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:680da076cad81cdf5ffcac50c477b6790be81768d30f9da9e01960c4b18a66db"}, 656 | {file = "cryptography-3.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5d52c72449bb02dd45a773a203196e6d4fae34e158769c896012401f33064396"}, 657 | {file = "cryptography-3.1.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc"}, 658 | {file = "cryptography-3.1.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a7597ffc67987b37b12e09c029bd1dc43965f75d328076ae85721b84046e9ca7"}, 659 | {file = "cryptography-3.1.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:4549b137d8cbe3c2eadfa56c0c858b78acbeff956bd461e40000b2164d9167c6"}, 660 | {file = "cryptography-3.1.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:89aceb31cd5f9fc2449fe8cf3810797ca52b65f1489002d58fe190bfb265c536"}, 661 | {file = "cryptography-3.1.1-cp35-cp35m-win32.whl", hash = "sha256:559d622aef2a2dff98a892eef321433ba5bc55b2485220a8ca289c1ecc2bd54f"}, 662 | {file = "cryptography-3.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:451cdf60be4dafb6a3b78802006a020e6cd709c22d240f94f7a0696240a17154"}, 663 | {file = "cryptography-3.1.1-cp36-abi3-win32.whl", hash = "sha256:762bc5a0df03c51ee3f09c621e1cee64e3a079a2b5020de82f1613873d79ee70"}, 664 | {file = "cryptography-3.1.1-cp36-abi3-win_amd64.whl", hash = "sha256:b12e715c10a13ca1bd27fbceed9adc8c5ff640f8e1f7ea76416352de703523c8"}, 665 | {file = "cryptography-3.1.1-cp36-cp36m-win32.whl", hash = "sha256:21b47c59fcb1c36f1113f3709d37935368e34815ea1d7073862e92f810dc7499"}, 666 | {file = "cryptography-3.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:48ee615a779ffa749d7d50c291761dc921d93d7cf203dca2db663b4f193f0e49"}, 667 | {file = "cryptography-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:b2bded09c578d19e08bd2c5bb8fed7f103e089752c9cf7ca7ca7de522326e921"}, 668 | {file = "cryptography-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2"}, 669 | {file = "cryptography-3.1.1-cp38-cp38-win32.whl", hash = "sha256:ab010e461bb6b444eaf7f8c813bb716be2d78ab786103f9608ffd37a4bd7d490"}, 670 | {file = "cryptography-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:99d4984aabd4c7182050bca76176ce2dbc9fa9748afe583a7865c12954d714ba"}, 671 | {file = "cryptography-3.1.1.tar.gz", hash = "sha256:9d9fc6a16357965d282dd4ab6531013935425d0dc4950df2e0cf2a1b1ac1017d"}, 672 | ] 673 | fastapi = [ 674 | {file = "fastapi-0.60.2-py3-none-any.whl", hash = "sha256:579c194f78ed3cff1a4e62bbaea79fc1bef05e35294147e41a807f4105a77885"}, 675 | {file = "fastapi-0.60.2.tar.gz", hash = "sha256:7dd1e4380976741a71dec2a7f9035b69268881ca25001cc99f25ca89d2d38ad9"}, 676 | ] 677 | ffmpeg-python = [ 678 | {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"}, 679 | {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"}, 680 | ] 681 | future = [ 682 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, 683 | ] 684 | gunicorn = [ 685 | {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"}, 686 | {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"}, 687 | ] 688 | h11 = [ 689 | {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, 690 | {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, 691 | ] 692 | httpcore = [ 693 | {file = "httpcore-0.11.1-py3-none-any.whl", hash = "sha256:72cfaa461dbdc262943ff4c9abf5b195391a03cdcc152e636adb4239b15e77e1"}, 694 | {file = "httpcore-0.11.1.tar.gz", hash = "sha256:a35dddd1f4cc34ff37788337ef507c0ad0276241ece6daf663ac9e77c0b87232"}, 695 | ] 696 | httptools = [ 697 | {file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"}, 698 | {file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"}, 699 | {file = "httptools-0.1.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6"}, 700 | {file = "httptools-0.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c"}, 701 | {file = "httptools-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a"}, 702 | {file = "httptools-0.1.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f"}, 703 | {file = "httptools-0.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2"}, 704 | {file = "httptools-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009"}, 705 | {file = "httptools-0.1.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"}, 706 | {file = "httptools-0.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d"}, 707 | {file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"}, 708 | {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, 709 | ] 710 | httpx = [ 711 | {file = "httpx-0.15.5-py3-none-any.whl", hash = "sha256:02326f2d3c61133db31e4b88dd3432479b434e52a68d813eab6db930f13611ea"}, 712 | {file = "httpx-0.15.5.tar.gz", hash = "sha256:254b371e3880a8e2387bf9ead6949bac797bd557fda26eba19a6153a0c06bd2b"}, 713 | ] 714 | idna = [ 715 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 716 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 717 | ] 718 | iniconfig = [ 719 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 720 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 721 | ] 722 | isort = [ 723 | {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, 724 | {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, 725 | ] 726 | lazy-object-proxy = [ 727 | {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, 728 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, 729 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, 730 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, 731 | {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, 732 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, 733 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, 734 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, 735 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, 736 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, 737 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, 738 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, 739 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, 740 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, 741 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, 742 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, 743 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, 744 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, 745 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, 746 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, 747 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, 748 | ] 749 | mccabe = [ 750 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 751 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 752 | ] 753 | packaging = [ 754 | {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, 755 | {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, 756 | ] 757 | pillow-simd = [ 758 | {file = "Pillow-SIMD-7.0.0.post3.tar.gz", hash = "sha256:c27907af0e7ede1ceed281719e722e7dbf3e1dbfe561373978654a6b64896cb7"}, 759 | ] 760 | pluggy = [ 761 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 762 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 763 | ] 764 | py = [ 765 | {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, 766 | {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, 767 | ] 768 | pycparser = [ 769 | {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, 770 | {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, 771 | ] 772 | pydantic = [ 773 | {file = "pydantic-1.6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614"}, 774 | {file = "pydantic-1.6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99"}, 775 | {file = "pydantic-1.6.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b"}, 776 | {file = "pydantic-1.6.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e"}, 777 | {file = "pydantic-1.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1"}, 778 | {file = "pydantic-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e"}, 779 | {file = "pydantic-1.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1"}, 780 | {file = "pydantic-1.6.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c"}, 781 | {file = "pydantic-1.6.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df"}, 782 | {file = "pydantic-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"}, 783 | {file = "pydantic-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9"}, 784 | {file = "pydantic-1.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d"}, 785 | {file = "pydantic-1.6.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7"}, 786 | {file = "pydantic-1.6.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20"}, 787 | {file = "pydantic-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633"}, 788 | {file = "pydantic-1.6.1-py36.py37.py38-none-any.whl", hash = "sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d"}, 789 | {file = "pydantic-1.6.1.tar.gz", hash = "sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73"}, 790 | ] 791 | pylint = [ 792 | {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, 793 | {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, 794 | ] 795 | pyparsing = [ 796 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 797 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 798 | ] 799 | pytest = [ 800 | {file = "pytest-6.1.1-py3-none-any.whl", hash = "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9"}, 801 | {file = "pytest-6.1.1.tar.gz", hash = "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92"}, 802 | ] 803 | pytest-asyncio = [ 804 | {file = "pytest-asyncio-0.14.0.tar.gz", hash = "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"}, 805 | {file = "pytest_asyncio-0.14.0-py3-none-any.whl", hash = "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d"}, 806 | ] 807 | python-dotenv = [ 808 | {file = "python-dotenv-0.14.0.tar.gz", hash = "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d"}, 809 | {file = "python_dotenv-0.14.0-py2.py3-none-any.whl", hash = "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"}, 810 | ] 811 | python-magic = [ 812 | {file = "python-magic-0.4.18.tar.gz", hash = "sha256:b757db2a5289ea3f1ced9e60f072965243ea43a2221430048fd8cacab17be0ce"}, 813 | {file = "python_magic-0.4.18-py2.py3-none-any.whl", hash = "sha256:356efa93c8899047d1eb7d3eb91e871ba2f5b1376edbaf4cc305e3c872207355"}, 814 | ] 815 | python-multipart = [ 816 | {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, 817 | ] 818 | qrcode = [ 819 | {file = "qrcode-6.1-py2.py3-none-any.whl", hash = "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5"}, 820 | {file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"}, 821 | ] 822 | requests = [ 823 | {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, 824 | {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, 825 | ] 826 | rfc3986 = [ 827 | {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"}, 828 | {file = "rfc3986-1.4.0.tar.gz", hash = "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d"}, 829 | ] 830 | six = [ 831 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 832 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 833 | ] 834 | sniffio = [ 835 | {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, 836 | {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, 837 | ] 838 | starlette = [ 839 | {file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"}, 840 | {file = "starlette-0.13.6.tar.gz", hash = "sha256:ebe8ee08d9be96a3c9f31b2cb2a24dbdf845247b745664bd8a3f9bd0c977fdbc"}, 841 | ] 842 | toml = [ 843 | {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, 844 | {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, 845 | ] 846 | urllib3 = [ 847 | {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, 848 | {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, 849 | ] 850 | uvicorn = [ 851 | {file = "uvicorn-0.11.8-py3-none-any.whl", hash = "sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef"}, 852 | {file = "uvicorn-0.11.8.tar.gz", hash = "sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26"}, 853 | ] 854 | uvloop = [ 855 | {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, 856 | {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, 857 | {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, 858 | {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, 859 | {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, 860 | {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, 861 | {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, 862 | {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, 863 | {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, 864 | ] 865 | websockets = [ 866 | {file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"}, 867 | {file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"}, 868 | {file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"}, 869 | {file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"}, 870 | {file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"}, 871 | {file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"}, 872 | {file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"}, 873 | {file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"}, 874 | {file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"}, 875 | {file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"}, 876 | {file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"}, 877 | {file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"}, 878 | {file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"}, 879 | {file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"}, 880 | {file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"}, 881 | {file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"}, 882 | {file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"}, 883 | {file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"}, 884 | {file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"}, 885 | {file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"}, 886 | {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, 887 | {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, 888 | ] 889 | wrapt = [ 890 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 891 | ] 892 | -------------------------------------------------------------------------------- /api/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "filemanager-fastapi" 3 | version = "0.0.1" 4 | description = "Fastapi with file management" 5 | authors = ["JEX "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | fastapi = "^0.60.1" 10 | uvicorn = "^0.11.8" 11 | gunicorn = "^20.0.4" 12 | python-multipart = "^0.0.5" 13 | python-dotenv = "^0.14.0" 14 | apache-libcloud = "^3.1.0" 15 | Pillow-SIMD = "^7.0.0" 16 | aiofiles = "^0.5.0" 17 | cryptography = "^3.0" 18 | ffmpeg-python = "^0.2.0" 19 | qrcode = "^6.1" 20 | pytest = "^6.1.0" 21 | pytest-asyncio = "^0.14.0" 22 | httpx = "^0.15.5" 23 | python-magic = "^0.4.18" 24 | 25 | [tool.poetry.dev-dependencies] 26 | pylint = "^2.5.3" 27 | 28 | [build-system] 29 | requires = ["poetry>=0.12"] 30 | build-backend = "poetry.masonry.api" 31 | -------------------------------------------------------------------------------- /api/start-reload.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | set -e 3 | 4 | if [ -f /app/app/main.py ]; then 5 | DEFAULT_MODULE_NAME=app.main 6 | elif [ -f /app/main.py ]; then 7 | DEFAULT_MODULE_NAME=main 8 | fi 9 | MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} 10 | VARIABLE_NAME=${VARIABLE_NAME:-app} 11 | export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} 12 | 13 | HOST=${HOST:-0.0.0.0} 14 | PORT=${PORT:-80} 15 | LOG_LEVEL=${LOG_LEVEL:-info} 16 | 17 | # If there's a prestart.sh script in the /app directory or other path specified, run it before starting 18 | PRE_START_PATH=${PRE_START_PATH:-/app/prestart.sh} 19 | echo "Checking for script in $PRE_START_PATH" 20 | if [ -f $PRE_START_PATH ] ; then 21 | echo "Running script $PRE_START_PATH" 22 | . "$PRE_START_PATH" 23 | else 24 | echo "There is no script $PRE_START_PATH" 25 | fi 26 | 27 | # Start Uvicorn with live reload 28 | exec uvicorn --reload --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE" -------------------------------------------------------------------------------- /api/start.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | set -e 3 | 4 | if [ -f /app/app/main.py ]; then 5 | DEFAULT_MODULE_NAME=app.main 6 | elif [ -f /app/main.py ]; then 7 | DEFAULT_MODULE_NAME=main 8 | fi 9 | MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} 10 | VARIABLE_NAME=${VARIABLE_NAME:-app} 11 | export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} 12 | 13 | DEFAULT_GUNICORN_CONF=/gunicorn_conf.py 14 | 15 | export GUNICORN_CONF=${GUNICORN_CONF:-$DEFAULT_GUNICORN_CONF} 16 | export WORKER_CLASS=${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"} 17 | 18 | # Start Gunicorn 19 | exec gunicorn -b 0.0.0.0:9000 -k "$WORKER_CLASS" -c "$GUNICORN_CONF" "$APP_MODULE" -------------------------------------------------------------------------------- /certbot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM phusion/baseimage:bionic-1.0.0 2 | 3 | COPY run-certbot.sh /root/certbot/run-certbot.sh 4 | 5 | RUN apt-get update 6 | RUN apt-get install -y letsencrypt 7 | 8 | ENTRYPOINT bash -c "bash /root/certbot/run-certbot.sh && sleep infinity" -------------------------------------------------------------------------------- /certbot/run-certbot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | letsencrypt certonly --webroot -w /var/www/letsencrypt -d "$CN" --agree-tos --email "$EMAIL" --non-interactive --text 4 | 5 | IFS=',' read -ra ADDR <<< "$CN" 6 | cp /etc/letsencrypt/archive/"${ADDR[0]}"/cert1.pem /var/certs/cert1.pem 7 | cp /etc/letsencrypt/archive/"${ADDR[0]}"/privkey1.pem /var/certs/privkey1.pem 8 | cp /etc/letsencrypt/live/"${ADDR[0]}"/fullchain.pem /var/certs/fullchain.pem -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | api: 5 | image: 127.0.0.1:5000/filemenager-fastapi 6 | build: 7 | context: ./api 8 | args: 9 | - INSTALL_FFMPEG=${INSTALL_FFMPEG} 10 | volumes: 11 | - './api/app:/app' 12 | command: /start-reload.sh 13 | ports: 14 | - 80:80 15 | expose: 16 | - 80 -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | api: 5 | image: gujadoesdocker/filemanager-fastapi 6 | build: 7 | context: ./api 8 | args: 9 | - INSTALL_FFMPEG=${INSTALL_FFMPEG} 10 | volumes: 11 | - './api/app:/app' 12 | command: /start.sh 13 | ports: 14 | - 9000:9000 15 | expose: 16 | - 9000 17 | 18 | ### Certbot ######################################### 19 | certbot: 20 | image: phusion/baseimage:bionic-1.0.0 21 | build: 22 | context: ./certbot 23 | volumes: 24 | - ./data/certbot/certs/:/var/certs 25 | - ./certbot/letsencrypt/:/var/www/letsencrypt 26 | environment: 27 | - CN=${DOMAINS} 28 | - EMAIL=${SSL_DOMAIN_OWNER} 29 | 30 | ### NGINX Server ######################################### 31 | nginx: 32 | image: nginx:alpine 33 | build: 34 | context: ./nginx 35 | args: 36 | - PYTHON_UPSTREAM_CONTAINER=${NGINX_PYTHON_UPSTREAM_CONTAINER} 37 | - PYTHON_UPSTREAM_PORT=${NGINX_PYTHON_UPSTREAM_PORT} 38 | volumes: 39 | - ${APP_CODE_PATH_HOST}:${APP_CODE_PATH_CONTAINER} 40 | - ${NGINX_HOST_LOG_PATH}:/var/log/nginx 41 | - ${NGINX_SITES_PATH}:/etc/nginx/sites-available 42 | - ./data/certbot/certs/:/var/certs 43 | - ./certbot/letsencrypt/:/var/www/letsencrypt 44 | ports: 45 | - "${NGINX_HOST_HTTP_PORT}:80" 46 | - "${NGINX_HOST_HTTPS_PORT}:443" 47 | depends_on: 48 | - api -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | COPY nginx.conf /etc/nginx/ 4 | 5 | # If you're in China, or you need to change sources, will be set CHANGE_SOURCE to true in .env. 6 | 7 | ARG CHANGE_SOURCE=false 8 | RUN if [ ${CHANGE_SOURCE} = true ]; then \ 9 | # Change application source from dl-cdn.alpinelinux.org to aliyun source 10 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/' /etc/apk/repositories \ 11 | ;fi 12 | 13 | RUN apk update \ 14 | && apk upgrade \ 15 | && apk add --no-cache bash 16 | 17 | RUN set -x ; \ 18 | addgroup -g 82 -S www-data ; \ 19 | adduser -u 82 -D -S -G www-data www-data && exit 0 ; exit 1 20 | 21 | ARG UPSTREAM_CONTAINER=api 22 | ARG UPSTREAM_PORT=9000 23 | 24 | # Set upstream conf and remove the default conf 25 | RUN echo "upstream node-upstream { server ${NODE_UPSTREAM_CONTAINER}:${NODE_UPSTREAM_PORT}; }" > /etc/nginx/conf.d/upstream.conf \ 26 | && rm /etc/nginx/conf.d/default.conf 27 | 28 | CMD ["nginx"] 29 | 30 | EXPOSE 80 443 31 | 32 | FROM nginx:alpine 33 | 34 | LABEL maintainer="Mahmoud Zalt " 35 | 36 | COPY nginx.conf /etc/nginx/ 37 | 38 | # If you're in China, or you need to change sources, will be set CHANGE_SOURCE to true in .env. 39 | 40 | ARG CHANGE_SOURCE=false 41 | RUN if [ ${CHANGE_SOURCE} = true ]; then \ 42 | # Change application source from dl-cdn.alpinelinux.org to aliyun source 43 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/' /etc/apk/repositories \ 44 | ;fi 45 | 46 | RUN apk update \ 47 | && apk upgrade \ 48 | && apk --update add logrotate \ 49 | && apk add --no-cache openssl \ 50 | && apk add --no-cache bash 51 | 52 | RUN apk add --no-cache curl 53 | 54 | RUN set -x ; \ 55 | addgroup -g 82 -S www-data ; \ 56 | adduser -u 82 -D -S -G www-data www-data && exit 0 ; exit 1 57 | 58 | ARG PYTHON_UPSTREAM_CONTAINER=api 59 | ARG PYTHON_UPSTREAM_PORT=9000 60 | 61 | # Create 'messages' file used from 'logrotate' 62 | RUN touch /var/log/messages 63 | 64 | # Copy 'logrotate' config file 65 | COPY logrotate/nginx /etc/logrotate.d/ 66 | 67 | # Set upstream conf and remove the default conf 68 | RUN echo "upstream python-upstream { server ${PYTHON_UPSTREAM_CONTAINER}:${PYTHON_UPSTREAM_PORT}; }" > /etc/nginx/conf.d/upstream.conf \ 69 | && rm /etc/nginx/conf.d/default.conf 70 | 71 | ADD ./startup.sh /opt/startup.sh 72 | RUN sed -i 's/\r//g' /opt/startup.sh 73 | CMD ["/bin/bash", "/opt/startup.sh"] 74 | 75 | EXPOSE 80 81 443 76 | -------------------------------------------------------------------------------- /nginx/logrotate/nginx: -------------------------------------------------------------------------------- 1 | /var/log/nginx/*.log { 2 | daily 3 | missingok 4 | rotate 32 5 | compress 6 | delaycompress 7 | nodateext 8 | notifempty 9 | create 644 www-data root 10 | sharedscripts 11 | postrotate 12 | [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` 13 | endscript 14 | } 15 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 4; 3 | pid /run/nginx.pid; 4 | daemon off; 5 | 6 | events { 7 | worker_connections 2048; 8 | multi_accept on; 9 | use epoll; 10 | } 11 | 12 | http { 13 | server_tokens off; 14 | sendfile on; 15 | tcp_nopush on; 16 | tcp_nodelay on; 17 | keepalive_timeout 15; 18 | types_hash_max_size 2048; 19 | client_max_body_size 10M; 20 | include /etc/nginx/mime.types; 21 | default_type application/octet-stream; 22 | access_log /dev/stdout; 23 | error_log /dev/stderr; 24 | gzip on; 25 | gzip_disable "msie6"; 26 | 27 | ssl_protocols TLSv1.2 TLSv1.3; 28 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; 29 | ssl_prefer_server_ciphers off; 30 | add_header Strict-Transport-Security "max-age=63072000" always; 31 | ssl_stapling on; 32 | ssl_stapling_verify on; 33 | 34 | include /etc/nginx/conf.d/*.conf; 35 | include /etc/nginx/sites-available/*.conf; 36 | open_file_cache off; # Disabled for issue 619 37 | charset UTF-8; 38 | } -------------------------------------------------------------------------------- /nginx/sites/app.local.conf.example: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80 default_server; 4 | listen [::]:80 default_server ipv6only=on; 5 | 6 | server_name localhost; 7 | 8 | # location / { 9 | # try_files $uri $uri/ @fallback; 10 | # } 11 | 12 | location ~ /\.ht { 13 | deny all; 14 | } 15 | 16 | location / { 17 | proxy_set_header Host $host; 18 | proxy_set_header X-Real-IP $remote_addr; 19 | proxy_set_header X-Forwarded-Proto $scheme; 20 | proxy_pass http://python-upstream; 21 | } 22 | 23 | # Needs openning port from docker file and appolo needs to be listening 0.0.0.0 host 24 | location /graphql { 25 | proxy_http_version 1.1; 26 | proxy_set_header Upgrade $http_upgrade; 27 | proxy_set_header Connection 'upgrade'; 28 | proxy_set_header Host $host; 29 | proxy_cache_bypass $http_upgrade; 30 | 31 | proxy_pass http://python-upstream; 32 | } 33 | } -------------------------------------------------------------------------------- /nginx/sites/app.ssl.conf.example: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | return 301 https://$host$request_uri; 5 | } 6 | 7 | server { 8 | server_name ff.etomer.io www.ff.etomer.io; 9 | 10 | listen 443 ssl http2; 11 | listen [::]:443 ssl http2; 12 | 13 | ###### GENERATED CERTS AND KEYS ####### 14 | ssl_certificate /var/certs/fullchain.pem; 15 | ssl_certificate_key /var/certs/privkey1.pem; 16 | ssl_trusted_certificate /var/certs/cert1.pem; 17 | ##################################### 18 | 19 | ssl_session_timeout 1d; 20 | ssl_session_cache shared:MozSSL:10m; # about 40000 sessions 21 | ssl_session_tickets off; 22 | 23 | 24 | # location / { 25 | # try_files $uri $uri/ @fallback; 26 | # } 27 | 28 | location ~ /\.ht { 29 | deny all; 30 | } 31 | 32 | location /.well-known/acme-challenge/ { 33 | root /var/www/letsencrypt/; 34 | log_not_found off; 35 | } 36 | 37 | location / { 38 | proxy_set_header Host $host; 39 | proxy_set_header X-Real-IP $remote_addr; 40 | proxy_set_header X-Forwarded-Proto $scheme; 41 | proxy_pass http://python-upstream; 42 | } 43 | 44 | # Needs openning port from docker file and appolo needs to be listening 0.0.0.0 host 45 | # location /graphql { 46 | # proxy_http_version 1.1; 47 | # proxy_set_header Upgrade $http_upgrade; 48 | # proxy_set_header Connection 'upgrade'; 49 | # proxy_set_header Host $host; 50 | # proxy_cache_bypass $http_upgrade; 51 | 52 | # proxy_pass http://python-upstream; 53 | # } 54 | } -------------------------------------------------------------------------------- /nginx/sites/ssl/.gitignore: -------------------------------------------------------------------------------- 1 | *.crt 2 | *.csr 3 | *.key 4 | *.pem -------------------------------------------------------------------------------- /nginx/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # if [ ! -f /etc/nginx/ssl/default.crt ]; then 4 | # openssl genrsa -out "/etc/nginx/ssl/default.key" 2048 5 | # openssl req -new -key "/etc/nginx/ssl/default.key" -out "/etc/nginx/ssl/default.csr" -subj "/CN=default/O=default/C=UK" 6 | # openssl x509 -req -days 365 -in "/etc/nginx/ssl/default.csr" -signkey "/etc/nginx/ssl/default.key" -out "/etc/nginx/ssl/default.crt" 7 | # fi 8 | 9 | # Start crond in background 10 | crond -l 2 -b 11 | 12 | # Start nginx in foreground 13 | nginx 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | [![Contributors][contributors-shield]][contributors-url] 3 | [![Forks][forks-shield]][forks-url] 4 | [![Stargazers][stars-shield]][stars-url] 5 | [![Issues][issues-shield]][issues-url] 6 | [![MIT License][license-shield]][license-url] 7 | 8 | 9 | 10 |
11 |

12 | 13 | Logo 14 | 15 | 16 |

Filemanager-Fastapi

17 | 18 |

19 | Blazing fast filemanager using fastapi. 20 | 21 | 22 | ## About The Project 23 | 24 | Long story short, I needed microservice that would manage the files, so I ended up with writing Filemanager-Fastapi (FF) and I am not even complaining. Hope you will be able to use it in concrete needs. Have fun, and of course prs are welcome. 25 | 26 | Here's what features FF has at this time: 27 | * Uploading image file/files 28 | * Downloading image file/files, from any image url 29 | * Image file/files optimization/converting using Pillow-SIMD or FFMPEG 30 | - You can have both installed or you can choose any of engines depending your needs 31 | * Uploading video file 32 | * Downloading video file, from any video url 33 | * Video file optimization/converting using FFMPEG 34 | - change in .env INSTALL_FFMPEG=false to INSTALL_FFMPEG=true 35 | - Then run 36 | ```sh 37 | docker-compose build api 38 | ``` 39 | * The bonus one Qrcode generator 40 | * Uploading to local, Google Storage, or S3 41 | * Live reloading on local development 42 | * Self cleaning (temp files, Pillow-SIMD, FFMPEG) 43 | * Serving files from local storage 44 | - The path starts from static folder for example: 45 | ``` 46 | http://localhost/static/pictures/original/dcb8ac79618540688ea36e688a8c3635.png 47 | ``` 48 | * Easy Security using Bearer token 49 | - For security its recommended to make requests from your backend server, not from browser, as your key of FF can be tracked. 50 | 51 | * Out of box documentation thanks to fast-api [/docs && /redoc paths are available] 52 | * SSL secured reverse nginx proxy using gunicorn and uvloop 53 | 54 | 55 | May optimize it a little therefore FF already is really fast, try by yourself :) 56 | 57 | ### Built With 58 | * [FastAPI](https://fastapi.tiangolo.com/) 59 | * [Docker](https://www.docker.com/) 60 | * [pillow-simd](https://github.com/uploadcare/pillow-simd) 61 | * [FFMPEG](https://github.com/FFmpeg/FFmpeg) 62 | * [Libcloud](https://github.com/apache/libcloud) 63 | 64 | 65 | ## Getting Started 66 | 67 | To get a local copy up and running follow these simple example steps. 68 | 69 | #### Before you start steps below make sure that you have created all .env files, .env-example s are provided. 70 | 71 | ### Installation for local development 72 | - Fast reloading included 73 | 1. Change docker-compose.dev.yml to docker-compose.yml 74 | 2. 75 | ```sh 76 | docker-compose build api 77 | ``` 78 | 3. Start the service 79 | ```sh 80 | docker-compose up 81 | ``` 82 | 4. Enter your API at `localhost/docs` 83 | 84 | 5. Now you should be able to see the open api endpoints. 85 | - Don't forget to authorize with FILE_MANAGER_BEARER_TOKEN that you should generate and paste in .env file. 86 | - If you forgot to generate FILE_MANAGER_BEARER_TOKEN here is one of the ways to generate secret key 87 | - ```sh 88 | openssl rand -base64 64 89 | ``` 90 | - p.s You can create as much tokens as you want just separate them with , 91 | 92 | ![](api/app/static/pictures/original/afba38beae434b9fb4691bf8559947aa.png?raw=true) 93 | 94 | ### Installation for docker swarm 95 | - If you’re trying things out on a local development environment, you can put your engine into swarm mode with docker swarm init. 96 | - If you’ve already got a multi-node swarm running, keep in mind that all docker stack and docker service commands must be run from a manager node. 97 | #### Set up a Docker registry 98 | - Start the registry as a service on your swarm: 99 | ```sh 100 | docker service create --name registry --publish published=5000,target=5000 registry:2 101 | ``` 102 | 3. Build the image locally with docker-compose 103 | - Decide wich docker-compose you are going to use there is .dev and .prod, when you decide rename to docker-compose.yml and continue 104 | ```sh 105 | docker-compose build 106 | ``` 107 | 4. Bring the app down 108 | ```sh 109 | docker-compose down --volumes 110 | ``` 111 | 5. Push the generated image to the registry 112 | ```sh 113 | docker-compose push 114 | ``` 115 | 6. Deploy the stack to the swarm 116 | ```sh 117 | docker stack deploy --compose-file docker-compose.yml filemanager-fastapi 118 | ``` 119 | 7. Check 120 | ```sh 121 | docker stack services filemanager-fastapi 122 | ``` 123 | 8. Result 124 | ```sh 125 | ID NAME MODE REPLICAS IMAGE PORTS 126 | kkk5mmkgk6zf filemanager-fastapi_api replicated 1/1 127.0.0.1:5000/filemenager-fastapi:latest *:80->80/tcp 127 | ``` 128 | ### Docker Hub image 129 | Available at (https://hub.docker.com/r/gujadoesdocker/filemanager-fastapi) 130 | 131 | ### You need A+ ssl? 132 | - No problem Filemanager-Fastapi comes with nginx and certbot configuration that guarantees A+ ssl. 133 | 134 | - If you will need help let me know. 135 | 136 | 137 | 147 | 148 | ## Size after development [with ffmpeg and pillow-simd] 149 | - command 150 | ```sh 151 | $ docker ps --size 152 | ``` 153 | ## Result 154 | ```sh 155 | NAMES SIZE 156 | filemanager-fastapi_nginx_1 126B (virtual 28.1MB) 157 | filemanager-fastapi_api_1 310B (virtual 1.12GB) 158 | ``` 159 | 160 | ## FFMPEG 4 161 | - If you like to use ffmpeg in your docker .env file change INSTALL_FFMPEG=false to INSTALL_FFMPEG=true 162 | - Don't forget to change api .env IMAGE_OPTIMIZATION_USING to ffmpeg. 163 | - LTS version at this time: 164 | ```sh 165 | ffmpeg version 4.1.6-1~deb10u1 Copyright (c) 2000-2020 the FFmpeg developers 166 | built with gcc 8 (Debian 8.3.0-6) 167 | libavutil 56. 22.100 / 56. 22.100 168 | libavcodec 58. 35.100 / 58. 35.100 169 | libavformat 58. 20.100 / 58. 20.100 170 | libavdevice 58. 5.100 / 58. 5.100 171 | libavfilter 7. 40.101 / 7. 40.101 172 | libavresample 4. 0. 0 / 4. 0. 0 173 | libswscale 5. 3.100 / 5. 3.100 174 | libswresample 3. 3.100 / 3. 3.100 175 | libpostproc 55. 3.100 / 55. 3.100 176 | ``` 177 | - enjoy :) 178 | 179 | 180 | ## Contributing 181 | 182 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 183 | 184 | 1. Fork the Project 185 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 186 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 187 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 188 | 5. Open a Pull Request 189 | 190 | ## Image optimization result 191 | - Original 192 | 193 | ![](api/app/static/pictures/original/dcb8ac79618540688ea36e688a8c3635.png?raw=true) 194 | 195 | - Thumbnailed usind pillow-SIMD 196 | 197 | ![](api/app/static/pictures/thumbnail/dcb8ac79618540688ea36e688a8c3635.webp?raw=true) 198 | 199 | - Thumbnailed usind FFMPEG 200 | 201 | ![](api/app/static/pictures/thumbnail/72014f9f91ab40c7b8df61ab350bcc71.webp?raw=true) 202 | 203 | 204 | 205 | ## Generated Qrcode example 206 | 207 | 208 | It is possible to edit configuration easily to generate Qrcode image on your needs with or without logo size you need color and etc. (For generating image is used Pillow-simd) 209 | 210 | ![](api/app/static/qr/04de739e41154172b8858146f4d8edfe.png?raw=true) 211 | 212 | 213 | 214 | ## License 215 | 216 | Distributed under the MIT License. See `LICENSE` for more information. 217 | 218 | 219 | 222 | 223 | 224 | ## Contact 225 | Twitter - [@guja_py](https://twitter.com/guja_py) 226 | 227 | 228 | 229 | 230 | 231 | [contributors-shield]: https://img.shields.io/github/contributors/JexPY/filemanager-fastapi.svg?style=flat-square 232 | [contributors-url]: https://github.com/JexPY/filemanager-fastapi/graphs/contributors 233 | [forks-shield]: https://img.shields.io/github/forks/JexPY/filemanager-fastapi.svg?style=flat-square 234 | [forks-url]: https://github.com/othneildrew/Best-README-Template/network/members 235 | [stars-shield]: https://img.shields.io/github/stars/JexPY/filemanager-fastapi.svg?style=flat-square 236 | [stars-url]: https://github.com/JexPY/filemanager-fastapi/stargazers 237 | [issues-shield]: https://img.shields.io/github/issues/JexPY/filemanager-fastapi.svg?style=flat-square 238 | [issues-url]: https://github.com/JexPY/filemanager-fastapi/issues 239 | [license-shield]: https://img.shields.io/github/license/JexPY/filemanager-fastapi.svg?style=flat-square 240 | [license-url]: https://github.com/JexPY/filemanager-fastapi/blob/master/LICENSE.txt --------------------------------------------------------------------------------