Skip to content

Commit

Permalink
Merge pull request #182 from hotosm/develop
Browse files Browse the repository at this point in the history
Production Release
  • Loading branch information
nrjadkry authored Aug 29, 2024
2 parents 7fefb9a + 72a066f commit c9d73e9
Show file tree
Hide file tree
Showing 28 changed files with 390 additions and 71 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ S3_SECRET_KEY=${S3_SECRET_KEY:-SAMPLESECRETACCESSKEYFORMINIOROOT}
# REF:https://min.io/docs/minio/linux/integrations/setup-nginx-proxy-with-minio.html
# ex: https://minio.example.net/minio/ui/
MINIO_ENDPOINT=${MINIO_ENDPOINT}
S3_ENDPOINT=${S3_ENDPOINT:-$MINIO_ENDPOINT}
MINIO_BROWSER_REDIRECT_URL=${MINIO_BROWSER_REDIRECT_URL}

### BACKEND ###
Expand Down
1 change: 0 additions & 1 deletion src/backend/app/db/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ class DbProject(Base):
is_terrain_follow = cast(bool, Column(Boolean, default=False))
dem_url = cast(str, Column(String, nullable=True))
hashtags = cast(list, Column(ARRAY(String))) # Project hashtag

output_orthophoto_url = cast(str, Column(String, nullable=True))
output_pointcloud_url = cast(str, Column(String, nullable=True))
output_raw_url = cast(str, Column(String, nullable=True))
Expand Down
62 changes: 45 additions & 17 deletions src/backend/app/projects/project_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import uuid
from loguru import logger as log
from fastapi import HTTPException, UploadFile
from fmtm_splitter.splitter import split_by_square
from app.tasks.splitter import split_by_square
from fastapi.concurrency import run_in_threadpool
from psycopg import Connection
from app.utils import merge_multipolygon
Expand All @@ -13,43 +13,68 @@
from app.config import settings


async def upload_dem_to_s3(project_id: uuid.UUID, dem_file: UploadFile) -> str:
"""Upload dem into S3.
async def upload_file_to_s3(
project_id: uuid.UUID, file: UploadFile, folder: str, file_extension: str
) -> str:
"""
Upload a file (image or DEM) to S3.
Args:
project_id (int): The organisation id in the database.
dem_file (UploadFile): The logo image uploaded to FastAPI.
project_id (uuid.UUID): The project ID in the database.
file (UploadFile): The file to be uploaded.
folder (str): The folder name in the S3 bucket.
file_extension (str): The file extension (e.g., 'png', 'tif').
Returns:
dem_url(str): The S3 URL for the dem file.
str: The S3 URL for the uploaded file.
"""
dem_path = f"/dem/{project_id}/dem.tif"
# If the folder is 'images', use 'screenshot.png' as the filename
if folder == "images":
file_name = "screenshot.png"
else:
file_name = f"dem.{file_extension}"

# Define the S3 file path
file_path = f"/{folder}/{project_id}/{file_name}"

file_bytes = await dem_file.read()
# Read the file bytes
file_bytes = await file.read()
file_obj = BytesIO(file_bytes)

# Upload the file to the S3 bucket
add_obj_to_bucket(
settings.S3_BUCKET_NAME,
file_obj,
dem_path,
content_type=dem_file.content_type,
file_path,
file.content_type,
)

dem_url = f"{settings.S3_DOWNLOAD_ROOT}/{settings.S3_BUCKET_NAME}{dem_path}"
# Construct the S3 URL for the file
file_url = f"{settings.S3_DOWNLOAD_ROOT}/{settings.S3_BUCKET_NAME}{file_path}"

return dem_url
return file_url


async def update_project_dem_url(db: Connection, project_id: uuid.UUID, dem_url: str):
"""Update the DEM URL for a project."""
async def update_url(db: Connection, project_id: uuid.UUID, url: str, url_type: str):
"""
Update the URL (DEM or image) for a project in the database.
Args:
db (Connection): The database connection.
project_id (uuid.UUID): The project ID in the database.
url (str): The URL to be updated.
url_type (str): The column name for the URL (e.g., 'dem_url', 'image_url').
Returns:
bool: True if the update was successful.
"""
async with db.cursor() as cur:
await cur.execute(
"""
f"""
UPDATE projects
SET dem_url = %(dem_url)s
SET {url_type} = %(url)s
WHERE id = %(project_id)s""",
{"dem_url": dem_url, "project_id": project_id},
{"url": url, "project_id": project_id},
)

return True
Expand All @@ -73,10 +98,13 @@ async def create_tasks_from_geojson(
log.debug(f"Processing {len(polygons)} task geometries")
for index, polygon in enumerate(polygons):
try:
if not polygon["geometry"]:
continue
# If the polygon is a MultiPolygon, convert it to a Polygon
if polygon["geometry"]["type"] == "MultiPolygon":
log.debug("Converting MultiPolygon to Polygon")
polygon["geometry"]["type"] = "Polygon"

polygon["geometry"]["coordinates"] = polygon["geometry"][
"coordinates"
][0]
Expand Down
19 changes: 14 additions & 5 deletions src/backend/app/projects/project_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,29 @@ async def create_project(
db: Annotated[Connection, Depends(database.get_db)],
user_data: Annotated[AuthUser, Depends(login_required)],
dem: UploadFile = File(None),
image: UploadFile = File(None),
):
"""Create a project in database."""
"""Create a project in the database."""
project_id = await project_schemas.DbProject.create(db, project_info, user_data.id)

# Upload DEM to S3
dem_url = await project_logic.upload_dem_to_s3(project_id, dem) if dem else None
# Upload DEM and Image to S3
dem_url = (
await project_logic.upload_file_to_s3(project_id, dem, "dem", "tif")
if dem
else None
)
await project_logic.upload_file_to_s3(
project_id, image, "images", "png"
) if image else None

# Update dem url to database
await project_logic.update_project_dem_url(db, project_id, dem_url)
# Update DEM and Image URLs in the database
await project_logic.update_url(db, project_id, dem_url, "dem_url")

if not project_id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Project creation failed"
)

return {"message": "Project successfully created", "project_id": project_id}


Expand Down
16 changes: 14 additions & 2 deletions src/backend/app/projects/project_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
from datetime import datetime, date
import geojson
from loguru import logger as log
from pydantic import BaseModel, computed_field, Field
from pydantic import BaseModel, computed_field, Field, model_validator
from pydantic.functional_validators import AfterValidator
from pydantic.functional_serializers import PlainSerializer
from geojson_pydantic import Feature, FeatureCollection, Polygon, Point, MultiPolygon
from fastapi import HTTPException
from psycopg import Connection
from psycopg.rows import class_row
from slugify import slugify
from pydantic import model_validator
from app.models.enums import FinalOutput, ProjectVisibility
from app.models.enums import (
IntEnum,
Expand All @@ -23,6 +22,8 @@
merge_multipolygon,
)
from psycopg.rows import dict_row
from app.config import settings
from app.s3 import get_image_dir_url


def validate_geojson(
Expand Down Expand Up @@ -154,6 +155,7 @@ class DbProject(BaseModel):
gsd_cm_px: Optional[float] = None
altitude_from_ground: Optional[float] = None
is_terrain_follow: bool = False
image_url: Optional[str] = None

async def one(db: Connection, project_id: uuid.UUID):
"""Get a single project & all associated tasks by ID."""
Expand Down Expand Up @@ -410,6 +412,16 @@ class ProjectOut(BaseModel):
requires_approval_from_manager_for_locking: bool
task_count: int = 0
tasks: Optional[list[TaskOut]] = []
image_url: Optional[str] = None

@model_validator(mode="after")
def set_image_url(cls, values):
"""Set image_url before rendering the model."""
project_id = values.id
if project_id:
image_dir = f"images/{project_id}/screenshot.png"
values.image_url = get_image_dir_url(settings.S3_BUCKET_NAME, image_dir)
return values


class PresignedUrlRequest(BaseModel):
Expand Down
34 changes: 34 additions & 0 deletions src/backend/app/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,37 @@ def get_obj_from_bucket(bucket_name: str, s3_path: str) -> BytesIO:
if response:
response.close()
response.release_conn()


def get_image_dir_url(bucket_name: str, image_dir: str):
"""Generate the full URL for the image directory in an S3 bucket.
Args:
bucket_name (str): The name of the S3 bucket.
image_dir (str): The directory path within the bucket where images are stored.
Returns:
str: The full URL to access the image directory.
"""
minio_url, is_secure = is_connection_secure(settings.S3_ENDPOINT)

# Ensure image_dir starts with a forward slash
if not image_dir.startswith("/"):
image_dir = f"/{image_dir}"

# Construct the full URL
protocol = "https" if is_secure else "http"

url = f"{protocol}://{minio_url}/{bucket_name}{image_dir}"

minio_client = s3_client()
try:
# List objects with a prefix to check if the directory exists
objects = minio_client.list_objects(bucket_name, prefix=image_dir.lstrip("/"))
if any(objects):
return url
else:
return None

except Exception as e:
log.error(f"Error checking directory existence: {str(e)}")
Loading

0 comments on commit c9d73e9

Please sign in to comment.