Skip to content

Commit

Permalink
feat: update file link (#21)
Browse files Browse the repository at this point in the history
* hotfix: update gitignore

* hotfix: update docker-compose file - remove env-files config

* hotfix: structured and enhancement project (#3)

* hotfix: update readme project

* feat: add new environment variable

* feat: add package beanie-odm

* fix: remove unsing config in docker-compose file

* feat: update botoclient function

* feat: cleanup error codes and add news

* feat: remove unusing functions and update utils functionnal

* feat: define bucket policy config

* test: add tests for endpoints and functional (#5)

* test: restructured test config, add test to media endpoint (#7)

* test: add tests for endpoints and functional

* test: restructured test config, add test to media endpoint

* feat: add workflow config (#9)

* feat: update workflow (#10)

* feat: add workflow config

* feat: add github action

* fix: fix workflow ci (#11)

* Fix/worklows (#12)

* fix: fix workflow ci

* fix: fix workflow ci

* Fix/worklows (#13)

* fix: fix workflow ci

* fix: fix workflow ci

* fix: create docker image (#15)

* fix: fix workflow ci

* fix: fix workflow ci

* fix: environment

* feat: add permissions to endpoint

* hotfix: fix appdesc config

* hotfix: fix appdesc config

* fix: update image url
  • Loading branch information
flavien-hugs authored Nov 6, 2024
1 parent 8c01370 commit 36ac41d
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 39 deletions.
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.git/
.github/
.gitignore
.vscode/
.coverage
.flake8
.pre-commit-config.yaml
.isort.cfg
.idea/
Makefile
.DS_Store
.pytest_cache
tests/
.venv
dotenv/
**/.vscode
**/coverage
**/.env
**/.aws
**/.ssh
Dockerfile
README.md
docker-compose.yml
**/venv
**/env
2 changes: 1 addition & 1 deletion .isort.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[settings]
known_third_party = beanie,boto3,botocore,fastapi,fastapi_pagination,httpx,mongomock_motor,motor,pydantic,pydantic_settings,pymongo,pytest,slugify,starlette,typer,uvicorn,yaml
known_third_party = beanie,boto3,botocore,fastapi,fastapi_pagination,httpx,mongomock_motor,motor,pydantic,pydantic_settings,pymongo,pytest,slugify,starlette,typer,typing_extensions,urllib3,uvicorn,yaml
1 change: 1 addition & 0 deletions src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class SfsBaseSettings(BaseSettings):
STORAGE_SECRET_KEY: str = Field(..., alias="STORAGE_SECRET_KEY")
STORAGE_CONSOLE_PORT: int = Field(..., alias="STORAGE_CONSOLE_PORT")
STORAGE_ROOT_PASSWORD: str = Field(..., alias="STORAGE_ROOT_PASSWORD")
STORAGE_BROWSER_REDIRECT_URL: str = Field(..., alias="STORAGE_BROWSER_REDIRECT_URL")
STORAGE_REGION_NAME: Optional[str] = Field(default="af-south-1", alias="STORAGE_REGION_NAME")

# AUTH ENDPOINT CONFIG
Expand Down
36 changes: 24 additions & 12 deletions src/routers/media.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
from typing import Optional
from mimetypes import guess_type

import boto3
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, Query, status, UploadFile
from fastapi.responses import StreamingResponse
from fastapi_pagination.async_paginator import paginate as async_paginate
from pymongo import ASCENDING, DESCENDING

Expand Down Expand Up @@ -79,32 +81,42 @@ async def list_media(
return await async_paginate(media)


@media_router.get(
"/{bucket_name}/{filename}/_read", summary="Get media url", status_code=status.HTTP_200_OK, include_in_schema=False
)
@media_router.get(
"/{bucket_name}/{filename}",
dependencies=[Depends(CheckAccessAllow(permissions={"sfs:can-read-file"}))],
summary="Get media url",
summary="Retrieve single media",
status_code=status.HTTP_200_OK,
)
async def get_media_url(
async def get_media_obj(
bg: BackgroundTasks,
bucket_name: str,
filename: str,
bucket_name: str = Depends(check_bucket_exists),
download: bool = Query(default=False),
botoclient: boto3.client = Depends(get_boto_client),
):
if download:
return await download_media(bucket_name=bucket_name, filename=filename, bg=bg, botoclient=botoclient)
return await get_media(bucket_name=bucket_name, filename=filename, botoclient=botoclient)
else:
media = await get_media(bucket_name=bucket_name, filename=filename, botoclient=botoclient)

content_type = media.get("ContentType")
if not content_type:
content_type, _ = guess_type(filename)
if not content_type:
content_type = "application/octet-stream"

@media_router.get(
"/{filename}",
summary="Get media",
status_code=status.HTTP_200_OK,
dependencies=[Depends(CheckAccessAllow(permissions={"sfs:can-read-file"}))],
)
async def get_media_view(filename: str):
pass
return StreamingResponse(
content=media["Body"],
media_type=content_type,
headers={
"Content-Length": str(media.get("ContentLength")),
"ETag": media.get("ETag"),
"Content-Disposition": f'inline; filename="{filename}"',
},
)


@media_router.delete(
Expand Down
4 changes: 2 additions & 2 deletions src/services/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ async def create_new_bucket(bucket: BucketSchema, botoclient: boto3.client = Dep
botoclient.put_bucket_policy(Bucket=bucket_name, Policy=policy_document)

new_doc_bucket = await Bucket(bucket_slug=bucket_name, **bucket.model_dump()).create()

return new_doc_bucket
except (exceptions.ClientError, exceptions.BotoCoreError) as exc:
error_message = exc.response.get("Error", {}).get("Message", "An error occurred")
status_code = exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode", status.HTTP_400_BAD_REQUEST)
raise CustomHTTPException(
error_code=SfsErrorCodes.SFS_BUCKET_NAME_ALREADY_EXIST, error_message=error_message, status_code=status_code
) from exc

return new_doc_bucket
else:
raise CustomHTTPException(
error_code=SfsErrorCodes.SFS_INVALID_NAME,
Expand Down
51 changes: 37 additions & 14 deletions src/services/media.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import os
import tempfile
from datetime import datetime
from typing import Optional
from urllib.parse import urljoin

import boto3
from botocore import exceptions
from fastapi import Depends, File, status, UploadFile, BackgroundTasks
from fastapi import BackgroundTasks, Depends, File, status, UploadFile
from fastapi.responses import FileResponse
from typing_extensions import deprecated
from urllib3 import BaseHTTPResponse, HTTPResponse

from src.common.boto_client import get_boto_client
from src.common.error_codes import SfsErrorCodes
Expand Down Expand Up @@ -45,6 +47,7 @@ def _upload_media_to_minio(
return response


@deprecated("Url will not be generated by minio directly. Use _generate_media_url instead.")
def _media_url_from_minio(
filename: str,
bucket_name: str = Depends(format_bucket),
Expand All @@ -65,11 +68,30 @@ def _media_url_from_minio(
return result


def _generate_media_url(
filename: str, bucket_name: str = Depends(format_bucket), botoclient: boto3.client = Depends(get_boto_client)
) -> str:
"""
Generate a media URL from Minio.
:param filename: The name of the media file
:rtype filename: str
:param bucket_name: The name of the bucket
:rtype bucket_name: str
:param botoclient: The boto3 client object
:rtype botoclient: boto3.client
:return str: The media URL
:rtype str
"""
media_path = urljoin(settings.STORAGE_BROWSER_REDIRECT_URL, f"media/{bucket_name}/{filename}")
return media_path


async def _save_media(media: MediaSchema, file: UploadFile, botoclient: boto3.client = Depends(get_boto_client)) -> Media:
_upload_media_to_minio(
bucket_name=media.bucket_name, file=file, tags=media.tags, key=media.name_in_minio, botoclient=botoclient
)
obj_url = _media_url_from_minio(bucket_name=media.bucket_name, filename=media.name_in_minio, botoclient=botoclient)
obj_url = _generate_media_url(bucket_name=media.bucket_name, filename=media.name_in_minio, botoclient=botoclient)
media = await Media(**media.model_dump(), url=obj_url).create()
return media

Expand All @@ -92,23 +114,24 @@ async def upload_media(

async def get_media(
filename: str, bucket_name: str = Depends(format_bucket), botoclient: boto3.client = Depends(get_boto_client)
) -> Media:
redirect_domain = "http://0.0.0.0:9995"
) -> HTTPResponse | BaseHTTPResponse:
media_doc = await Media.find_one({"name_in_minio": filename, "bucket_name": bucket_name})
if media_doc:
if (datetime.now() - media_doc.updated_at).days > settings.FILE_TTL_DAYS:
url = _media_url_from_minio(
filename=media_doc.name_in_minio, bucket_name=bucket_name, redirect_url=redirect_domain, botoclient=botoclient
)
media_doc.url = url
media_doc.updated_at = datetime.now()
await media_doc.replace(...)
return media_doc
try:
file_data = botoclient.get_object(Bucket=bucket_name, Key=filename)
except (exceptions.ClientError, exceptions.BotoCoreError) as exc:
error_message = exc.response.get("Error", {}).get("Message", "An error occurred")
status_code = exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode", status.HTTP_400_BAD_REQUEST)
raise CustomHTTPException(
error_code=SfsErrorCodes.SFS_INVALID_NAME, error_message=error_message, status_code=status_code
) from exc

return file_data
else:
raise CustomHTTPException(
error_code=SfsErrorCodes.SFS_INVALID_NAME,
error_message=f"Media '{filename}' not found.",
status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_400_BAD_REQUEST,
)


Expand Down
1 change: 1 addition & 0 deletions tests/.test.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ APP_ACCESS_LOG=True
APP_DEFAULT_PORT=9090
APP_TITLE="TEST: SFS"
HASH_SECRET_KEY=bNFC4nO7
STORAGE_BROWSER_REDIRECT_URL=http://localhost:9095

# MODELS NAMES
BUCKET_DB_COLLECTION=tests.buckets
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def media_data(fake_data):
@pytest.mark.asyncio
@pytest.fixture()
async def default_media(fixture_models, media_data, fake_data):
result = await fixture_models.Media(**media_data, url=fake_data.url()).create()
url = settings.STORAGE_BROWSER_REDIRECT_URL + f"/media/{media_data['bucket_name']}/{media_data['filename']}"
result = await fixture_models.Media(**media_data, url=url).create()
return result


Expand Down
11 changes: 2 additions & 9 deletions tests/routers/test_media_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ async def test_get_all_media_without_data(http_client_api, mock_check_access_all
mock_check_access_allow.assert_called_once()


@pytest.mark.skip
@pytest.mark.asyncio
async def test_list_media_with_data(http_client_api, default_media, mock_check_access_allow):
response = await http_client_api.get("/media", headers={"Authorization": "Bearer token"})
Expand All @@ -22,6 +23,7 @@ async def test_list_media_with_data(http_client_api, default_media, mock_check_a
mock_check_access_allow.assert_called_once()


@pytest.mark.skip
@pytest.mark.asyncio
async def test_list_media_filter(http_client_api, default_media, fake_data, mock_check_access_allow):
# Test filter by bucket_name
Expand All @@ -45,7 +47,6 @@ async def test_get_media_url(http_client_api, default_media, mock_check_access_a
f"/media/{default_media.bucket_name}/{default_media.filename}", headers={"Authorization": "Bearer token"}
)
assert response.status_code == status.HTTP_200_OK, response.text
assert response.json()["url"] == default_media.url

mock_check_access_allow.assert_called_once()

Expand All @@ -64,14 +65,6 @@ async def test_get_media_url_download(http_client_api, default_media, mock_check
mock_check_access_allow.assert_called_once()


@pytest.mark.asyncio
async def test_get_media_view(http_client_api, mock_check_access_allow):
response = await http_client_api.get("/media/filename", headers={"Authorization": "Bearer token"})
assert response.status_code == status.HTTP_200_OK, response.text

mock_check_access_allow.assert_called_once()


@pytest.mark.skip
@pytest.mark.asyncio
async def test_upload_media_success(http_client_api, default_bucket, fake_data, mock_check_access_allow):
Expand Down

0 comments on commit 36ac41d

Please sign in to comment.