diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e783fc1..73a6280 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,16 +85,18 @@ git pull ``` ├── ui # Frontend application │ └── src # Source code -| ├── app +│ ├── app │ ├── authentication │ ├── components │ ├── conf -| ├── context +│ ├── context │ └── lib ├── middeware # Backend application │ └── app # Source code -| ├── api # API routes -| ├── database # Database code +│ ├── api # APIs +│ │ ├── routes # API endpoint +│ │ └── services # API business logic +│ ├── database # Database code │ └── storage # Storage code └── infrastructure # Terraform scripts for cloud and local(docker version) ├── cloud # For clouds diff --git a/middleware/app/api/routes/download.py b/middleware/app/api/routes/download.py index 9afba8b..8465161 100644 --- a/middleware/app/api/routes/download.py +++ b/middleware/app/api/routes/download.py @@ -1,24 +1,14 @@ -from datetime import datetime, timezone from enum import Enum as PythonEnum +import api.services.download as download_service import utils.logger as logger -from database.db import DynamoDBManager -from fastapi import APIRouter, HTTPException -from storage.cloudflare_r2 import CloudflareR2Manager +from fastapi import APIRouter router = APIRouter() # Logger instance log = logger.get_logger() -# Storage -BUCKET_NAME = "byteshare-blob" -storage = CloudflareR2Manager(BUCKET_NAME) - -# DynamoDB -table_name = "byteshare-upload-metadata" -dynamodb = DynamoDBManager(table_name) - class StatusEnum(PythonEnum): initiated = "initiated" @@ -41,85 +31,7 @@ def get_file_url_return_name_link(upload_id: str, user_id: str | None = None): FUNCTION_NAME = "get_file_url_return_name_link()" log.info("Entering {}".format(FUNCTION_NAME)) - file_data = {} - time_now = datetime.now(timezone.utc).isoformat() - upload_metadata = dynamodb.read_item({"upload_id": upload_id}) - if upload_metadata == None: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "Upload ID not valid." - ) - ) - raise HTTPException(status_code=400, detail="Upload ID not valid") - if upload_metadata["status"] != StatusEnum.uploaded.name: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "Incomplete upload." - ) - ) - raise HTTPException(status_code=400, detail="Incomplete upload") - - expires_at = upload_metadata["expires_at"] - if time_now > expires_at: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "Link is expired." - ) - ) - raise HTTPException(status_code=400, detail="Link is expired") - - download_count = upload_metadata["download_count"] - max_count = upload_metadata["max_download"] - if user_id == None or upload_metadata["creator_id"] != user_id: - if download_count >= max_count: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "Download limit exceeded" - ) - ) - raise HTTPException(status_code=400, detail="Download limit exceeded") - - file_names = set(upload_metadata["storage_file_names"]) - for file_name in file_names: - file_path = upload_id + "/" + file_name - - file_format = _get_file_extension(file_name) - file_size = storage.get_file_info(file_path) - - download_expiration_time = 21600 # 6 hours - # Generate share download link - file_url = storage.generate_download_url(file_path, download_expiration_time) - if upload_metadata["share_email_as_source"]: - file_data["user_email"] = upload_metadata["creator_email"] - else: - file_data["user_email"] = None - file_data[file_name] = {} - file_data[file_name]["format"] = file_format - file_data[file_name]["size"] = _format_size(file_size) - file_data[file_name]["download_url"] = file_url - - if user_id == None or upload_metadata["creator_id"] != user_id: - keys = {"upload_id": upload_id} - update_data = { - "download_count": download_count + 1, - "updated_at": time_now, - } - dynamodb.update_item(keys, update_data) + response = download_service.get_file_url_return_name_link(upload_id, user_id) log.info("Exiting {}".format(FUNCTION_NAME)) - return file_data - - -def _get_file_extension(file_name): - return file_name.split(".")[-1] - - -def _format_size(byte_size): - if byte_size < 1024: - return f"{byte_size} B" - elif byte_size < 1024**2: - return f"{byte_size / 1024:.2f} KB" - elif byte_size < 1024**3: - return f"{byte_size / (1024 ** 2):.2f} MB" - else: - return f"{byte_size / (1024 ** 3):.2f} GB" + return response diff --git a/middleware/app/api/routes/feedback.py b/middleware/app/api/routes/feedback.py index 2ecd74f..f638541 100644 --- a/middleware/app/api/routes/feedback.py +++ b/middleware/app/api/routes/feedback.py @@ -1,7 +1,5 @@ -import uuid - +import api.services.feedback as feedback_service import utils.logger as logger -from database.db import DynamoDBManager from fastapi import APIRouter from pydantic import BaseModel @@ -10,10 +8,6 @@ # Logger instance log = logger.get_logger() -# DynamoDB -feedback_table_name = "byteshare-feedback" -feedback_dynamodb = DynamoDBManager(feedback_table_name) - class Feedback(BaseModel): name: str @@ -38,12 +32,6 @@ def post_feedback_return_none(body: Feedback): FUNCTION_NAME = "post_feedback_return_none()" log.info("Entering {}".format(FUNCTION_NAME)) - feedback = { - "feedback_id": uuid.uuid4().hex, - "email": body.email, - "name": body.name, - "message": body.message, - } - feedback_dynamodb.create_item(feedback) + feedback_service.post_feedback_return_none(body) log.info("Exiting {}".format(FUNCTION_NAME)) diff --git a/middleware/app/api/routes/health.py b/middleware/app/api/routes/health.py index 942c429..fb699c4 100644 --- a/middleware/app/api/routes/health.py +++ b/middleware/app/api/routes/health.py @@ -1,21 +1,12 @@ +import api.services.health as health_service import utils.logger as logger -from database.db import DynamoDBManager from fastapi import APIRouter -from storage.cloudflare_r2 import CloudflareR2Manager router = APIRouter() # Logger instance log = logger.get_logger() -# DynamoDB -table_name = "byteshare-upload-metadata" -dynamodb = DynamoDBManager(table_name) - -# Storage -BUCKET_NAME = "byteshare-blob" -storage = CloudflareR2Manager(BUCKET_NAME) - @router.get("/") def health_check(): @@ -32,8 +23,7 @@ def health_check(): FUNCTION_NAME = "health_check()" log.info("Entering {}".format(FUNCTION_NAME)) - dynamodb.health_check() - storage.health_check() + response = health_service.health_check() log.info("Exiting {}".format(FUNCTION_NAME)) - return {"status": "ok", "details": "Service is running"} + return response diff --git a/middleware/app/api/routes/scan.py b/middleware/app/api/routes/scan.py index 0ee8caa..ea2c567 100644 --- a/middleware/app/api/routes/scan.py +++ b/middleware/app/api/routes/scan.py @@ -1,9 +1,6 @@ -from datetime import datetime, timezone - -import resend +import api.services.scan as scan_service import utils.logger as logger -from database.db import DynamoDBManager -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter from pydantic import BaseModel router = APIRouter() @@ -11,10 +8,6 @@ # Logger instance log = logger.get_logger() -# DynamoDB -table_name = "byteshare-upload-metadata" -dynamodb = DynamoDBManager(table_name) - class CompleteScan(BaseModel): safe: bool @@ -37,63 +30,6 @@ def finalise_scan_return_none(body: CompleteScan, upload_id: str): FUNCTION_NAME = "finalise_scan_return_none()" log.info("Entering {}".format(FUNCTION_NAME)) - safe = body.safe - - try: - upload_metadata = dynamodb.read_item({"upload_id": upload_id}) - except Exception as e: - log.error("EXCEPTION occurred connecting to DB.\nERROR: {}".format(str(e))) - raise HTTPException(status_code=500, detail="Cannot connect to DB") - if not upload_metadata: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "Upload ID not valid." - ) - ) - raise HTTPException(status_code=400, detail="Upload ID not valid") - - if safe: - time_now = datetime.now(timezone.utc).isoformat() - keys = {"upload_id": upload_id} - update_data = { - "scanned": True, - "updated_at": time_now, - } - dynamodb.update_item(keys, update_data) - else: - keys = {"upload_id": upload_id} - dynamodb.delete_item(keys) - - user_email = upload_metadata["creator_email"] - params = { - "from": "ByteShare ", - "to": [user_email], - "subject": "Important: Security Alert Regarding Your Uploaded File", - "html": """ - -

Hey,

- -

- -

We hope this email finds you well.

- -

We regret to inform you that upon scanning the upload(Upload ID: {}) you recently uploaded, our system has detected several issues that require immediate attention. As a security measure, we have taken the necessary steps to remove the file from our servers to prevent any potential risks or threats to our system and users.

- -

We kindly request your cooperation in resolving the identified issues. We understand that this might inconvenience you, and we apologize for any disruption this may cause.

-

To ensure the safety and integrity of our platform, we advise you to review the content of the file and address any issues or vulnerabilities it may contain. Once resolved, you are welcome to re-upload the file for further processing.

- -

If you require any assistance or have any questions regarding this matter, please do not hesitate to contact our support team.

- -

Thank you for your prompt attention to this matter.

-

Best regards,
- ByteShare Team

- - - """.format( - upload_id - ), - } - - resend.Emails.send(params) + scan_service.finalise_scan_return_none(body, upload_id) log.info("Exiting {}".format(FUNCTION_NAME)) diff --git a/middleware/app/api/routes/subscribe.py b/middleware/app/api/routes/subscribe.py index 4a0cf96..01f6969 100644 --- a/middleware/app/api/routes/subscribe.py +++ b/middleware/app/api/routes/subscribe.py @@ -1,7 +1,5 @@ -from datetime import datetime, timezone - +import api.services.subscribe as subscribe_service import utils.logger as logger -from database.db import DynamoDBManager from fastapi import APIRouter from pydantic import BaseModel @@ -10,10 +8,6 @@ # Logger instance log = logger.get_logger() -# DynamoDB -subscriber_table_name = "byteshare-subscriber" -subscriber_dynamodb = DynamoDBManager(subscriber_table_name) - class Subscribe(BaseModel): email: str @@ -33,11 +27,7 @@ def add_subscriber_return_done(body: Subscribe): FUNCTION_NAME = "add_subscriber_return_done()" log.info("Entering {}".format(FUNCTION_NAME)) - subscriber = { - "email": body.email, - "created_at": datetime.now(timezone.utc).isoformat(), - } - subscriber_dynamodb.create_item(subscriber) + response = subscribe_service.add_subscriber_return_done(body) log.info("Exiting {}".format(FUNCTION_NAME)) - return {"status": "Done"} + return response diff --git a/middleware/app/api/routes/upload.py b/middleware/app/api/routes/upload.py index 90008ea..a31e470 100644 --- a/middleware/app/api/routes/upload.py +++ b/middleware/app/api/routes/upload.py @@ -1,32 +1,17 @@ -import concurrent.futures -import os -import uuid -from datetime import datetime, timedelta, timezone from enum import Enum as PythonEnum from typing import List -import pika -import qrcode -import resend +import api.services.upload as upload_service import utils.logger as logger from api.auth import authenticate -from database.db import DynamoDBManager -from dotenv import load_dotenv -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, Request from pydantic import BaseModel -from storage.cloudflare_r2 import CloudflareR2Manager router = APIRouter() # Logger instance log = logger.get_logger() -# Load Environment variables -load_dotenv() - -# Resend -resend.api_key = str(os.getenv("RESEND_API_KEY")) - class StatusEnum(PythonEnum): initiated = "initiated" @@ -56,24 +41,6 @@ class DeleteUpload(BaseModel): user_id: str -# Storage -BUCKET_NAME = "byteshare-blob" -storage = CloudflareR2Manager(BUCKET_NAME) - -# DynamoDB -table_name = "byteshare-upload-metadata" -dynamodb = DynamoDBManager(table_name) - -# RabbitMQ -if os.getenv("ENVIRONMENT") == "production": - params = pika.URLParameters(os.getenv("RABBITMQ_URL")) - connection = pika.BlockingConnection(params) - channel = connection.channel() - channel.queue_declare(queue=os.getenv("RABBITMQ_QUEUE")) - -web_base_url = str(os.getenv("WEB_BASE_URL")) - - @router.post("/initiate") def initiate_upload( body: InitiateUpload, @@ -97,79 +64,10 @@ def initiate_upload( FUNCTION_NAME = "initiate_upload()" log.info("Entering {}".format(FUNCTION_NAME)) - client_ip = request.headers.get("x-forwarded-for") or request.client.host - content_length = int(request.headers.get("File-Length")) - if content_length is None: - log.warning("BAD REQUEST\nERROR: {}".format("file-Length header not found.")) - raise HTTPException(status_code=400, detail="file-Length header not found.") - - max_file_size = 2 * 1024 * 1024 * 1024 # 2GB - - if len(body.file_names) == 0: - log.warning("BAD REQUEST\nERROR: {}".format("No files present.")) - raise HTTPException(status_code=400, detail="No files present.") - - if int(content_length) > max_file_size: - log.warning("BAD REQUEST\nERROR: {}".format("File size exceeds the limit.")) - raise HTTPException(status_code=400, detail="File size exceeds the limit.") - - file_names = body.file_names - share_email_as_source = body.share_email_as_source - upload_id = uuid.uuid4().hex - continue_id = uuid.uuid4().hex - - result = {} - result["upload_id"] = upload_id - result["upload_urls"] = {} - - with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: - future_to_file_name = { - executor.submit(_generate, upload_id, file_name): file_name - for file_name in file_names - } - - responses = [] - for future in concurrent.futures.as_completed(future_to_file_name): - file_name = future_to_file_name[future] - try: - response = future.result() - responses.append(response) - except Exception as e: - log.error( - "EXCEPTION occurred for Upload ID: {}\nFile {}: \nERROR:{}".format( - upload_id, file_name, str(e) - ) - ) - - for response in responses: - result["upload_urls"][response["file_name"]] = response["upload_url"] - - time_now = datetime.now(timezone.utc) - - upload_metadata = { - "upload_id": upload_id, - "status": StatusEnum.initiated.name, - "title": "Upload with " + file_names[0], - "scanned": False, - "creator_id": body.creator_id, - "creator_email": body.creator_email, - "creator_ip": client_ip, - "receiver_email": "", - "share_email_as_source": share_email_as_source, - "download_count": 0, - "max_download": 5, - "continue_id": continue_id, - "total_size": content_length, - "storage_file_names": body.file_names, - "storage_qr_name": "", - "expires_at": "", - "updated_at": "", - "created_at": time_now.isoformat(), - } - dynamodb.create_item(upload_metadata) + response = upload_service.initiate_upload(body, request) log.info("Exiting {}".format(FUNCTION_NAME)) - return result + return response @router.post("/finalise/{upload_id}") @@ -192,137 +90,10 @@ def post_upload_return_link_qr( FUNCTION_NAME = "post_upload_return_link_qr()" log.info("Entering {}".format(FUNCTION_NAME)) - upload_metadata = dynamodb.read_item({"upload_id": upload_id}) - if upload_metadata == None: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "Upload ID not valid." - ) - ) - raise HTTPException(status_code=400, detail="Upload ID not valid") - if upload_metadata["status"] == StatusEnum.uploaded.name: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "Upload already completed." - ) - ) - raise HTTPException(status_code=400, detail="Upload already completed") - - file_names = body.file_names - for file_name in file_names: - file_path = upload_id + "/" + file_name - - # Check for file present in Storage - is_file_present = storage.is_file_present(file_path) - if not is_file_present: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "Upload not found." - ) - ) - raise HTTPException(status_code=400, detail="Upload not found") - - # Generate share link - file_url = web_base_url + "/share/" + upload_id - - time_now = datetime.now(timezone.utc) - upload_expiration_time = 604800 # 7 days - expires_at = time_now + timedelta(seconds=upload_expiration_time) - - # Generate QR code - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=4, - ) - qr.add_data(file_url) - qr.make(fit=True) - - img = qr.make_image(fill_color="black", back_color="white") - - temp_qr_path = "/tmp/" + "{}.png".format(upload_id) - img.save(temp_qr_path) - - qr_name = "QRCode_" + upload_id + ".png" - qr_storage_file_name = upload_id + "/" + qr_name - - # Upload the QR code to Storage - storage.upload_file(temp_qr_path, qr_storage_file_name) - - # Remove the local file - os.remove(temp_qr_path) - - # Generate Download URL for the uploaded QR code - qr_download_url = storage.generate_download_url( - qr_storage_file_name, upload_expiration_time - ) - - keys = {"upload_id": upload_id} - if body.receiver_email: - update_data = { - "status": StatusEnum.uploaded.name, - "receiver_email": body.receiver_email, - "storage_qr_name": qr_name, - "expires_at": expires_at.isoformat(), - "updated_at": time_now.isoformat(), - } - else: - update_data = { - "status": StatusEnum.uploaded.name, - "storage_qr_name": qr_name, - "expires_at": expires_at.isoformat(), - "updated_at": time_now.isoformat(), - } - dynamodb.update_item(keys, update_data) - - # Send the share link to email, if given - if body.receiver_email: - name = body.sender_name - params = { - "from": "ByteShare ", - "to": [body.receiver_email], - "subject": "You've Received a File from {}".format(name), - "html": """ - -

Hey,

- -

- -

You have received a file via ByteShare, a secure file sharing platform.

- -

{} has sent you a file. You can download it using the link below:

- -

{}

- -

Please note that this link will expire after {}, so be sure to download the file promptly.

- -

If you have any questions or concerns, feel free to contact us at contact@byteshare.io

-

Thank you for using ByteShare!

- -

Best regards,
- The ByteShare Team

- - - """.format( - name, file_url, expires_at.isoformat() - ), - } - - resend.Emails.send(params) - - if os.getenv("ENVIRONMENT") == "production": - channel.basic_publish( - exchange="", routing_key=os.getenv("RABBITMQ_QUEUE"), body=upload_id - ) + response = upload_service.post_upload_return_link_qr(body, upload_id) log.info("Exiting {}".format(FUNCTION_NAME)) - return { - "url": file_url, - "QR": qr_download_url, - "expiration_date": expires_at.isoformat(), - "downloads_allowed": str(upload_metadata["max_download"]), - } + return response @router.delete("/{upload_id}") @@ -343,22 +114,10 @@ def delete_upload_return_done( FUNCTION_NAME = "delete_upload_return_done()" log.info("Entering {}".format(FUNCTION_NAME)) - upload_metadata = dynamodb.read_item({"upload_id": upload_id}) - if upload_metadata["creator_id"] != body.user_id: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "User is not the owner of the upload." - ) - ) - raise HTTPException( - status_code=400, detail="User is not the owner of the upload" - ) - - keys = {"upload_id": upload_id} - dynamodb.delete_item(keys) + response = upload_service.delete_upload_return_done(upload_id, body) log.info("Exiting {}".format(FUNCTION_NAME)) - return {"status": "Done"} + return response @router.put("/{upload_id}/title") @@ -380,35 +139,10 @@ def update_upload_title_return_done( FUNCTION_NAME = "update_upload_title_return_done()" log.info("Entering {}".format(FUNCTION_NAME)) - upload_metadata = dynamodb.read_item({"upload_id": upload_id}) - if upload_metadata["creator_id"] != body.user_id: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "User is not the owner of the upload." - ) - ) - raise HTTPException( - status_code=400, detail="User is not the owner of the upload" - ) - if not body.title: - log.warning( - "BAD REQUEST for UploadID: {}\nERROR: {}".format( - upload_id, "Title is not valid." - ) - ) - raise HTTPException(status_code=400, detail="Title is not valid") - - time_now = datetime.now(timezone.utc) - - keys = {"upload_id": upload_id} - update_data = { - "title": body.title, - "updated_at": time_now.isoformat(), - } - dynamodb.update_item(keys, update_data) + response = upload_service.update_upload_title_return_done(body, upload_id) log.info("Exiting {}".format(FUNCTION_NAME)) - return {"status": "Done"} + return response @router.get("/history/{user_id}") @@ -428,59 +162,7 @@ def get_history_return_all_shares_list( FUNCTION_NAME = "get_history_return_all_shares_list()" log.info("Entering {}".format(FUNCTION_NAME)) - history = [] - - # Note: will be uncommented later - # user = user_dynamodb.read_item({"user_id": user_id}) - # if(user==None): - # raise HTTPException(status_code=400, detail="User does not exist") - - upload_metadatas = dynamodb.read_items("creator_id", user_id) - for upload_metadata in upload_metadatas: - upload = { - "upload_id": upload_metadata["upload_id"], - "title": upload_metadata["title"], - "created_at": upload_metadata["created_at"], - "downloaded": upload_metadata["download_count"], - "max_download": upload_metadata["max_download"], - "total_size": _format_size(upload_metadata["total_size"]), - } - - history.append(upload) - - # Sort the history by date in place - history.sort(key=_sort_by_date_desc, reverse=True) - - log.info("Exiting {}".format(FUNCTION_NAME)) - return history - - -def _generate(upload_id, file_name): - FUNCTION_NAME = "_generate()" - log.info("Entering {}".format(FUNCTION_NAME)) - - expiration_time = 10800 - file_path = upload_id + "/" + file_name - upload_url = storage.generate_upload_url(file_path, expiration_time) - - response_url = {"file_name": file_name, "upload_url": upload_url} - - log.info("File name: {} completed".format(file_name)) + response = upload_service.get_history_return_all_shares_list(user_id) log.info("Exiting {}".format(FUNCTION_NAME)) - return response_url - - -def _format_size(byte_size): - if byte_size < 1024: - return f"{byte_size} B" - elif byte_size < 1024**2: - return f"{byte_size / 1024:.2f} KB" - elif byte_size < 1024**3: - return f"{byte_size / (1024 ** 2):.2f} MB" - else: - return f"{byte_size / (1024 ** 3):.2f} GB" - - -def _sort_by_date_desc(upload): - return upload["created_at"] + return response diff --git a/middleware/app/api/routes/user.py b/middleware/app/api/routes/user.py index cee374b..c71d114 100644 --- a/middleware/app/api/routes/user.py +++ b/middleware/app/api/routes/user.py @@ -1,9 +1,5 @@ -import os - -import resend +import api.services.user as user_service import utils.logger as logger -from database.db import DynamoDBManager -from dotenv import load_dotenv from fastapi import APIRouter from pydantic import BaseModel, Field @@ -12,16 +8,6 @@ # Logger instance log = logger.get_logger() -# Load Environment variables -load_dotenv() - -# Resend -resend.api_key = str(os.getenv("RESEND_API_KEY")) - -# DynamoDB -user_table_name = "byteshare-user" -user_dynamodb = DynamoDBManager(user_table_name) - class AddUser(BaseModel): id: str = Field(..., alias="$id") @@ -47,43 +33,6 @@ def webhook_post_user_send_email(body: AddUser): FUNCTION_NAME = "webhook_post_user_send_email()" log.info("Entering {}".format(FUNCTION_NAME)) - user = { - "user_id": body.id, - "name": body.name, - "email": body.email, - "created_at": body.registration, - } - user_dynamodb.create_item(user) - - params = { - "from": "ByteShare ", - "to": [body.email], - "subject": "Welcome to ByteShare", - "html": """ - -

Hey {},

- -

- -

I'm Ambuj, the founder of ByteShare.io, and I'd like to personally thank you for signing up to our service.

- -

We established ByteShare to make file sharing easy, hassle-free and secure.

- -

I’d love to hear what you think of our product. Is there anything we should work on or improve? Let us know.

-

You can also star us on Github

- -

I'm always happy to help and read our customers' suggestions.

- -

Thanks

-

Ambuj Raj
- ByteShare.io

- - -""".format( - body.name.split(" ")[0] - ), - } - - resend.Emails.send(params) + user_service.webhook_post_user_send_email(body) log.info("Exiting {}".format(FUNCTION_NAME)) diff --git a/middleware/app/api/services/.gitkeep b/middleware/app/api/services/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/middleware/app/api/services/download.py b/middleware/app/api/services/download.py new file mode 100644 index 0000000..967e37d --- /dev/null +++ b/middleware/app/api/services/download.py @@ -0,0 +1,97 @@ +from datetime import datetime, timezone +from enum import Enum as PythonEnum + +import utils.helper as helper +import utils.logger as logger +from database.db import DynamoDBManager +from fastapi import HTTPException +from storage.cloudflare_r2 import CloudflareR2Manager + +# Logger instance +log = logger.get_logger() + +# Storage +BUCKET_NAME = "byteshare-blob" +storage = CloudflareR2Manager(BUCKET_NAME) + +# DynamoDB +table_name = "byteshare-upload-metadata" +dynamodb = DynamoDBManager(table_name) + + +class StatusEnum(PythonEnum): + initiated = "initiated" + uploaded = "uploaded" + + +def get_file_url_return_name_link(upload_id: str, user_id: str | None = None): + FUNCTION_NAME = "get_file_url_return_name_link()" + log.info("Entering {}".format(FUNCTION_NAME)) + + file_data = {} + time_now = datetime.now(timezone.utc).isoformat() + upload_metadata = dynamodb.read_item({"upload_id": upload_id}) + if upload_metadata == None: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "Upload ID not valid." + ) + ) + raise HTTPException(status_code=400, detail="Upload ID not valid") + if upload_metadata["status"] != StatusEnum.uploaded.name: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "Incomplete upload." + ) + ) + raise HTTPException(status_code=400, detail="Incomplete upload") + + expires_at = upload_metadata["expires_at"] + if time_now > expires_at: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "Link is expired." + ) + ) + raise HTTPException(status_code=400, detail="Link is expired") + + download_count = upload_metadata["download_count"] + max_count = upload_metadata["max_download"] + if user_id == None or upload_metadata["creator_id"] != user_id: + if download_count >= max_count: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "Download limit exceeded" + ) + ) + raise HTTPException(status_code=400, detail="Download limit exceeded") + + file_names = set(upload_metadata["storage_file_names"]) + for file_name in file_names: + file_path = upload_id + "/" + file_name + + file_format = helper.get_file_extension(file_name) + file_size = storage.get_file_info(file_path) + + download_expiration_time = 21600 # 6 hours + # Generate share download link + file_url = storage.generate_download_url(file_path, download_expiration_time) + if upload_metadata["share_email_as_source"]: + file_data["user_email"] = upload_metadata["creator_email"] + else: + file_data["user_email"] = None + file_data[file_name] = {} + file_data[file_name]["format"] = file_format + file_data[file_name]["size"] = helper.format_size(file_size) + file_data[file_name]["download_url"] = file_url + + if user_id == None or upload_metadata["creator_id"] != user_id: + keys = {"upload_id": upload_id} + update_data = { + "download_count": download_count + 1, + "updated_at": time_now, + } + dynamodb.update_item(keys, update_data) + + log.info("Exiting {}".format(FUNCTION_NAME)) + return file_data diff --git a/middleware/app/api/services/feedback.py b/middleware/app/api/services/feedback.py new file mode 100644 index 0000000..89eb433 --- /dev/null +++ b/middleware/app/api/services/feedback.py @@ -0,0 +1,33 @@ +import uuid + +import utils.logger as logger +from database.db import DynamoDBManager +from pydantic import BaseModel + +# Logger instance +log = logger.get_logger() + +# DynamoDB +feedback_table_name = "byteshare-feedback" +feedback_dynamodb = DynamoDBManager(feedback_table_name) + + +class Feedback(BaseModel): + name: str + email: str + message: str + + +def post_feedback_return_none(body: Feedback): + FUNCTION_NAME = "post_feedback_return_none()" + log.info("Entering {}".format(FUNCTION_NAME)) + + feedback = { + "feedback_id": uuid.uuid4().hex, + "email": body.email, + "name": body.name, + "message": body.message, + } + feedback_dynamodb.create_item(feedback) + + log.info("Exiting {}".format(FUNCTION_NAME)) diff --git a/middleware/app/api/services/health.py b/middleware/app/api/services/health.py new file mode 100644 index 0000000..b390d16 --- /dev/null +++ b/middleware/app/api/services/health.py @@ -0,0 +1,25 @@ +import utils.logger as logger +from database.db import DynamoDBManager +from storage.cloudflare_r2 import CloudflareR2Manager + +# Logger instance +log = logger.get_logger() + +# DynamoDB +table_name = "byteshare-upload-metadata" +dynamodb = DynamoDBManager(table_name) + +# Storage +BUCKET_NAME = "byteshare-blob" +storage = CloudflareR2Manager(BUCKET_NAME) + + +def health_check(): + FUNCTION_NAME = "health_check()" + log.info("Entering {}".format(FUNCTION_NAME)) + + dynamodb.health_check() + storage.health_check() + + log.info("Exiting {}".format(FUNCTION_NAME)) + return {"status": "ok", "details": "Service is running"} diff --git a/middleware/app/api/services/scan.py b/middleware/app/api/services/scan.py new file mode 100644 index 0000000..232ccaf --- /dev/null +++ b/middleware/app/api/services/scan.py @@ -0,0 +1,84 @@ +from datetime import datetime, timezone + +import resend +import utils.logger as logger +from database.db import DynamoDBManager +from fastapi import HTTPException +from pydantic import BaseModel + +# Logger instance +log = logger.get_logger() + +# DynamoDB +table_name = "byteshare-upload-metadata" +dynamodb = DynamoDBManager(table_name) + + +class CompleteScan(BaseModel): + safe: bool + + +def finalise_scan_return_none(body: CompleteScan, upload_id: str): + FUNCTION_NAME = "finalise_scan_return_none()" + log.info("Entering {}".format(FUNCTION_NAME)) + + safe = body.safe + + try: + upload_metadata = dynamodb.read_item({"upload_id": upload_id}) + except Exception as e: + log.error("EXCEPTION occurred connecting to DB.\nERROR: {}".format(str(e))) + raise HTTPException(status_code=500, detail="Cannot connect to DB") + if not upload_metadata: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "Upload ID not valid." + ) + ) + raise HTTPException(status_code=400, detail="Upload ID not valid") + + if safe: + time_now = datetime.now(timezone.utc).isoformat() + keys = {"upload_id": upload_id} + update_data = { + "scanned": True, + "updated_at": time_now, + } + dynamodb.update_item(keys, update_data) + else: + keys = {"upload_id": upload_id} + dynamodb.delete_item(keys) + + user_email = upload_metadata["creator_email"] + params = { + "from": "ByteShare ", + "to": [user_email], + "subject": "Important: Security Alert Regarding Your Uploaded File", + "html": """ + +

Hey,

+ +

+ +

We hope this email finds you well.

+ +

We regret to inform you that upon scanning the upload(Upload ID: {}) you recently uploaded, our system has detected several issues that require immediate attention. As a security measure, we have taken the necessary steps to remove the file from our servers to prevent any potential risks or threats to our system and users.

+ +

We kindly request your cooperation in resolving the identified issues. We understand that this might inconvenience you, and we apologize for any disruption this may cause.

+

To ensure the safety and integrity of our platform, we advise you to review the content of the file and address any issues or vulnerabilities it may contain. Once resolved, you are welcome to re-upload the file for further processing.

+ +

If you require any assistance or have any questions regarding this matter, please do not hesitate to contact our support team.

+ +

Thank you for your prompt attention to this matter.

+

Best regards,
+ ByteShare Team

+ + + """.format( + upload_id + ), + } + + resend.Emails.send(params) + + log.info("Exiting {}".format(FUNCTION_NAME)) diff --git a/middleware/app/api/services/subscribe.py b/middleware/app/api/services/subscribe.py new file mode 100644 index 0000000..8605b02 --- /dev/null +++ b/middleware/app/api/services/subscribe.py @@ -0,0 +1,30 @@ +from datetime import datetime, timezone + +import utils.logger as logger +from database.db import DynamoDBManager +from pydantic import BaseModel + +# Logger instance +log = logger.get_logger() + +# DynamoDB +subscriber_table_name = "byteshare-subscriber" +subscriber_dynamodb = DynamoDBManager(subscriber_table_name) + + +class Subscribe(BaseModel): + email: str + + +def add_subscriber_return_done(body: Subscribe): + FUNCTION_NAME = "add_subscriber_return_done()" + log.info("Entering {}".format(FUNCTION_NAME)) + + subscriber = { + "email": body.email, + "created_at": datetime.now(timezone.utc).isoformat(), + } + subscriber_dynamodb.create_item(subscriber) + + log.info("Exiting {}".format(FUNCTION_NAME)) + return {"status": "Done"} diff --git a/middleware/app/api/services/upload.py b/middleware/app/api/services/upload.py new file mode 100644 index 0000000..ff6b423 --- /dev/null +++ b/middleware/app/api/services/upload.py @@ -0,0 +1,399 @@ +import concurrent.futures +import os +import uuid +from datetime import datetime, timedelta, timezone +from enum import Enum as PythonEnum +from typing import List + +import pika +import qrcode +import resend +import utils.helper as helper +import utils.logger as logger +from database.db import DynamoDBManager +from dotenv import load_dotenv +from fastapi import HTTPException, Request +from pydantic import BaseModel +from storage.cloudflare_r2 import CloudflareR2Manager + +# Logger instance +log = logger.get_logger() + +# Load Environment variables +load_dotenv() + +# Resend +resend.api_key = str(os.getenv("RESEND_API_KEY")) + + +class StatusEnum(PythonEnum): + initiated = "initiated" + uploaded = "uploaded" + + +class InitiateUpload(BaseModel): + file_names: List[str] + creator_id: str + creator_email: str + creator_ip: str + share_email_as_source: bool + + +class FinaliseUpload(BaseModel): + file_names: list + receiver_email: str + sender_name: str + + +class EditTitle(BaseModel): + title: str + user_id: str + + +class DeleteUpload(BaseModel): + user_id: str + + +# Storage +BUCKET_NAME = "byteshare-blob" +storage = CloudflareR2Manager(BUCKET_NAME) + +# DynamoDB +table_name = "byteshare-upload-metadata" +dynamodb = DynamoDBManager(table_name) + +# RabbitMQ +if os.getenv("ENVIRONMENT") == "production": + params = pika.URLParameters(os.getenv("RABBITMQ_URL")) + connection = pika.BlockingConnection(params) + channel = connection.channel() + channel.queue_declare(queue=os.getenv("RABBITMQ_QUEUE")) + +web_base_url = str(os.getenv("WEB_BASE_URL")) + + +def initiate_upload( + body: InitiateUpload, + request: Request, +): + FUNCTION_NAME = "initiate_upload()" + log.info("Entering {}".format(FUNCTION_NAME)) + + client_ip = request.headers.get("x-forwarded-for") or request.client.host + content_length = int(request.headers.get("File-Length")) + if content_length is None: + log.warning("BAD REQUEST\nERROR: {}".format("file-Length header not found.")) + raise HTTPException(status_code=400, detail="file-Length header not found.") + + max_file_size = 2 * 1024 * 1024 * 1024 # 2GB + + if len(body.file_names) == 0: + log.warning("BAD REQUEST\nERROR: {}".format("No files present.")) + raise HTTPException(status_code=400, detail="No files present.") + + if int(content_length) > max_file_size: + log.warning("BAD REQUEST\nERROR: {}".format("File size exceeds the limit.")) + raise HTTPException(status_code=400, detail="File size exceeds the limit.") + + file_names = body.file_names + share_email_as_source = body.share_email_as_source + upload_id = uuid.uuid4().hex + continue_id = uuid.uuid4().hex + + result = {} + result["upload_id"] = upload_id + result["upload_urls"] = {} + + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + future_to_file_name = { + executor.submit(_generate, upload_id, file_name): file_name + for file_name in file_names + } + + responses = [] + for future in concurrent.futures.as_completed(future_to_file_name): + file_name = future_to_file_name[future] + try: + response = future.result() + responses.append(response) + except Exception as e: + log.error( + "EXCEPTION occurred for Upload ID: {}\nFile {}: \nERROR:{}".format( + upload_id, file_name, str(e) + ) + ) + + for response in responses: + result["upload_urls"][response["file_name"]] = response["upload_url"] + + time_now = datetime.now(timezone.utc) + + upload_metadata = { + "upload_id": upload_id, + "status": StatusEnum.initiated.name, + "title": "Upload with " + file_names[0], + "scanned": False, + "creator_id": body.creator_id, + "creator_email": body.creator_email, + "creator_ip": client_ip, + "receiver_email": "", + "share_email_as_source": share_email_as_source, + "download_count": 0, + "max_download": 5, + "continue_id": continue_id, + "total_size": content_length, + "storage_file_names": body.file_names, + "storage_qr_name": "", + "expires_at": "", + "updated_at": "", + "created_at": time_now.isoformat(), + } + dynamodb.create_item(upload_metadata) + + log.info("Exiting {}".format(FUNCTION_NAME)) + return result + + +def post_upload_return_link_qr(body: FinaliseUpload, upload_id: str): + FUNCTION_NAME = "post_upload_return_link_qr()" + log.info("Entering {}".format(FUNCTION_NAME)) + + upload_metadata = dynamodb.read_item({"upload_id": upload_id}) + if upload_metadata == None: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "Upload ID not valid." + ) + ) + raise HTTPException(status_code=400, detail="Upload ID not valid") + if upload_metadata["status"] == StatusEnum.uploaded.name: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "Upload already completed." + ) + ) + raise HTTPException(status_code=400, detail="Upload already completed") + + file_names = body.file_names + for file_name in file_names: + file_path = upload_id + "/" + file_name + + # Check for file present in Storage + is_file_present = storage.is_file_present(file_path) + if not is_file_present: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "Upload not found." + ) + ) + raise HTTPException(status_code=400, detail="Upload not found") + + # Generate share link + file_url = web_base_url + "/share/" + upload_id + + time_now = datetime.now(timezone.utc) + upload_expiration_time = 604800 # 7 days + expires_at = time_now + timedelta(seconds=upload_expiration_time) + + # Generate QR code + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(file_url) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + + temp_qr_path = "/tmp/" + "{}.png".format(upload_id) + img.save(temp_qr_path) + + qr_name = "QRCode_" + upload_id + ".png" + qr_storage_file_name = upload_id + "/" + qr_name + + # Upload the QR code to Storage + storage.upload_file(temp_qr_path, qr_storage_file_name) + + # Remove the local file + os.remove(temp_qr_path) + + # Generate Download URL for the uploaded QR code + qr_download_url = storage.generate_download_url( + qr_storage_file_name, upload_expiration_time + ) + + keys = {"upload_id": upload_id} + if body.receiver_email: + update_data = { + "status": StatusEnum.uploaded.name, + "receiver_email": body.receiver_email, + "storage_qr_name": qr_name, + "expires_at": expires_at.isoformat(), + "updated_at": time_now.isoformat(), + } + else: + update_data = { + "status": StatusEnum.uploaded.name, + "storage_qr_name": qr_name, + "expires_at": expires_at.isoformat(), + "updated_at": time_now.isoformat(), + } + dynamodb.update_item(keys, update_data) + + # Send the share link to email, if given + if body.receiver_email: + name = body.sender_name + params = { + "from": "ByteShare ", + "to": [body.receiver_email], + "subject": "You've Received a File from {}".format(name), + "html": """ + +

Hey,

+ +

+ +

You have received a file via ByteShare, a secure file sharing platform.

+ +

{} has sent you a file. You can download it using the link below:

+ +

{}

+ +

Please note that this link will expire after {}, so be sure to download the file promptly.

+ +

If you have any questions or concerns, feel free to contact us at contact@byteshare.io

+

Thank you for using ByteShare!

+ +

Best regards,
+ The ByteShare Team

+ + + """.format( + name, file_url, expires_at.isoformat() + ), + } + + resend.Emails.send(params) + + if os.getenv("ENVIRONMENT") == "production": + channel.basic_publish( + exchange="", routing_key=os.getenv("RABBITMQ_QUEUE"), body=upload_id + ) + + log.info("Exiting {}".format(FUNCTION_NAME)) + return { + "url": file_url, + "QR": qr_download_url, + "expiration_date": expires_at.isoformat(), + "downloads_allowed": str(upload_metadata["max_download"]), + } + + +def delete_upload_return_done(upload_id: str, body: DeleteUpload): + FUNCTION_NAME = "delete_upload_return_done()" + log.info("Entering {}".format(FUNCTION_NAME)) + + upload_metadata = dynamodb.read_item({"upload_id": upload_id}) + if upload_metadata["creator_id"] != body.user_id: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "User is not the owner of the upload." + ) + ) + raise HTTPException( + status_code=400, detail="User is not the owner of the upload" + ) + + keys = {"upload_id": upload_id} + dynamodb.delete_item(keys) + + log.info("Exiting {}".format(FUNCTION_NAME)) + return {"status": "Done"} + + +def update_upload_title_return_done(body: EditTitle, upload_id: str): + FUNCTION_NAME = "update_upload_title_return_done()" + log.info("Entering {}".format(FUNCTION_NAME)) + + upload_metadata = dynamodb.read_item({"upload_id": upload_id}) + if upload_metadata["creator_id"] != body.user_id: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "User is not the owner of the upload." + ) + ) + raise HTTPException( + status_code=400, detail="User is not the owner of the upload" + ) + if not body.title: + log.warning( + "BAD REQUEST for UploadID: {}\nERROR: {}".format( + upload_id, "Title is not valid." + ) + ) + raise HTTPException(status_code=400, detail="Title is not valid") + + time_now = datetime.now(timezone.utc) + + keys = {"upload_id": upload_id} + update_data = { + "title": body.title, + "updated_at": time_now.isoformat(), + } + dynamodb.update_item(keys, update_data) + + log.info("Exiting {}".format(FUNCTION_NAME)) + return {"status": "Done"} + + +def get_history_return_all_shares_list(user_id: str): + FUNCTION_NAME = "get_history_return_all_shares_list()" + log.info("Entering {}".format(FUNCTION_NAME)) + + history = [] + + # Note: will be uncommented later + # user = user_dynamodb.read_item({"user_id": user_id}) + # if(user==None): + # raise HTTPException(status_code=400, detail="User does not exist") + + upload_metadatas = dynamodb.read_items("creator_id", user_id) + for upload_metadata in upload_metadatas: + upload = { + "upload_id": upload_metadata["upload_id"], + "title": upload_metadata["title"], + "created_at": upload_metadata["created_at"], + "downloaded": upload_metadata["download_count"], + "max_download": upload_metadata["max_download"], + "total_size": helper.format_size(upload_metadata["total_size"]), + } + + history.append(upload) + + # Sort the history by date in place + history.sort(key=_sort_by_date_desc, reverse=True) + + log.info("Exiting {}".format(FUNCTION_NAME)) + return history + + +def _generate(upload_id, file_name): + FUNCTION_NAME = "_generate()" + log.info("Entering {}".format(FUNCTION_NAME)) + + expiration_time = 10800 + file_path = upload_id + "/" + file_name + upload_url = storage.generate_upload_url(file_path, expiration_time) + + response_url = {"file_name": file_name, "upload_url": upload_url} + + log.info("File name: {} completed".format(file_name)) + + log.info("Exiting {}".format(FUNCTION_NAME)) + return response_url + + +def _sort_by_date_desc(upload): + return upload["created_at"] diff --git a/middleware/app/api/services/user.py b/middleware/app/api/services/user.py new file mode 100644 index 0000000..1760a12 --- /dev/null +++ b/middleware/app/api/services/user.py @@ -0,0 +1,73 @@ +import os + +import resend +import utils.logger as logger +from database.db import DynamoDBManager +from dotenv import load_dotenv +from pydantic import BaseModel, Field + +# Logger instance +log = logger.get_logger() + +# Load Environment variables +load_dotenv() + +# Resend +resend.api_key = str(os.getenv("RESEND_API_KEY")) + +# DynamoDB +user_table_name = "byteshare-user" +user_dynamodb = DynamoDBManager(user_table_name) + + +class AddUser(BaseModel): + id: str = Field(..., alias="$id") + name: str + registration: str + email: str + + +def webhook_post_user_send_email(body: AddUser): + FUNCTION_NAME = "webhook_post_user_send_email()" + log.info("Entering {}".format(FUNCTION_NAME)) + + user = { + "user_id": body.id, + "name": body.name, + "email": body.email, + "created_at": body.registration, + } + user_dynamodb.create_item(user) + + params = { + "from": "ByteShare ", + "to": [body.email], + "subject": "Welcome to ByteShare", + "html": """ + +

Hey {},

+ +

+ +

I'm Ambuj, the founder of ByteShare.io, and I'd like to personally thank you for signing up to our service.

+ +

We established ByteShare to make file sharing easy, hassle-free and secure.

+ +

I’d love to hear what you think of our product. Is there anything we should work on or improve? Let us know.

+

You can also star us on Github

+ +

I'm always happy to help and read our customers' suggestions.

+ +

Thanks

+

Ambuj Raj
+ ByteShare.io

+ + +""".format( + body.name.split(" ")[0] + ), + } + + resend.Emails.send(params) + + log.info("Exiting {}".format(FUNCTION_NAME)) diff --git a/middleware/app/utils/helper.py b/middleware/app/utils/helper.py new file mode 100644 index 0000000..a194f7e --- /dev/null +++ b/middleware/app/utils/helper.py @@ -0,0 +1,13 @@ +def get_file_extension(file_name): + return file_name.split(".")[-1] + + +def format_size(byte_size): + if byte_size < 1024: + return f"{byte_size} B" + elif byte_size < 1024**2: + return f"{byte_size / 1024:.2f} KB" + elif byte_size < 1024**3: + return f"{byte_size / (1024 ** 2):.2f} MB" + else: + return f"{byte_size / (1024 ** 3):.2f} GB"