diff --git a/backend/app/api/v1/auth.py b/backend/app/api/v1/auth.py index 3477263..e0d18e8 100644 --- a/backend/app/api/v1/auth.py +++ b/backend/app/api/v1/auth.py @@ -1,16 +1,16 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session from db.session import get_db -from core.security import create_access_token -from crud.user_crud import authenticate_user, create_user +from services.auth_service import register_user, login_user from schemas.user_schema import UserCreate router = APIRouter() @router.post("/register") def register(user_data:UserCreate, db: Session = Depends(get_db)): - user = create_user(db, user_data) + """Route to register a new user.""" + user = register_user(db, user_data) if not user: raise HTTPException(status_code=400, detail="Email already registered") return {"message": "User created successfully"} @@ -18,8 +18,8 @@ def register(user_data:UserCreate, db: Session = Depends(get_db)): @router.post("/login") def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): - user = authenticate_user(db, form_data.username, form_data.password) - if not user: + """Route to login a user.""" + token = login_user(db, form_data.username, form_data.password) + if not token: raise HTTPException(status_code=401, detail="Incorrect email or password") - access_token = create_access_token(user_id=user.id, data={"user_id": user.id}) - return {"access_token": access_token, "token_type": "bearer"} + return {"access_token": token, "token_type": "bearer"} diff --git a/backend/app/api/v1/cipher.py b/backend/app/api/v1/cipher.py index d664605..5cc581d 100644 --- a/backend/app/api/v1/cipher.py +++ b/backend/app/api/v1/cipher.py @@ -1,76 +1,31 @@ -from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session +from fastapi import APIRouter, Depends, Query +from services.process_service import encrypt_uploaded_file, decrypt_uploaded_file, get_task_status from db.session import get_db -from models.document_model import Document -from schemas.document_schema import DocumentCreate -from crud.document_crud import create_document -from core.security import get_user_id_from_token -from .tasks import encrypt_file, decrypt_file -from celery.result import AsyncResult -import os router = APIRouter() -@router.post("/encrypt_file") -async def encrypt_file_route(file_path: str, shift: int, token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) - if not os.path.exists(file_path): - raise HTTPException(status_code=404, detail="File not found") - - if token: - task = encrypt_file.delay(file_path, shift) - document = DocumentCreate( - filename=os.path.basename(file_path), - content=open(file_path, "rb").read(), - status="encrypting", - task_id=task.id, - user_id=user_id, - ) - create_document(db, document) - return {"message": "File encryption started", "task_id": task.id} - else: - task = encrypt_file.delay(file_path, shift) - return {"message": "File encryption started", "task_id": task.id} - - -@router.post("/decrypt_file") -async def decrypt_file_route(file_path: str, shift: int, token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) - if not os.path.exists(file_path): - raise HTTPException(status_code=404, detail="File not found") - - if token: - task = decrypt_file.delay(file_path, shift) - document = DocumentCreate( - filename=os.path.basename(file_path), - content=open(file_path, "rb").read(), - status="decrypting", - user_id=user_id, - task_id=task.id - ) - create_document(db, document) - return {"message": "File decryption started", "task_id": task.id} - else: - task = decrypt_file.delay(file_path, shift) - return {"message": "File decryption started", "task_id": task.id} - - -@router.get("/task_status/{task_id}") -async def task_status(task_id: str, db: Session = Depends(get_db)): - task = AsyncResult(task_id) - if task.state == "PENDING": - return {"status": "pending", "details": "Task is being processed"} - elif task.state == "SUCCESS": - file_path = task.result - file_name = os.path.basename(file_path) - - document = db.query(Document).filter(Document.task_id == task_id).first() - if document: - document.status = "completed" - db.commit() - - return {"status": "success", "details": "Task completed", "file_name": file_name} - elif task.state == "FAILURE": - return {"status": "failure", "details": str(task.info)} - else: - return {"status": task.state} \ No newline at end of file +@router.post("/encrypt") +def encrypt_file_route( + file_path: str, + shift: int, + token: str = Query(None), + db: Session = Depends(get_db) +): + """Route to encrypt a file using Caesar cipher.""" + return encrypt_uploaded_file(file_path, shift, token, db) + +@router.post("/decrypt") +def decrypt_file_route( + file_path: str, + shift: int, + token: str = Query(None), + db: Session = Depends(get_db) +): + """Route to decrypt a file using Caesar cipher.""" + return decrypt_uploaded_file(file_path, shift, token, db) + +@router.get("/status/{task_id}") +def get_task_status_route(task_id: str, db: Session = Depends(get_db)): + """Route to get the status of a task.""" + return get_task_status(task_id, db) diff --git a/backend/app/api/v1/file.py b/backend/app/api/v1/file.py new file mode 100644 index 0000000..28beb96 --- /dev/null +++ b/backend/app/api/v1/file.py @@ -0,0 +1,35 @@ +from fastapi import APIRouter, File, UploadFile, HTTPException +from fastapi.responses import StreamingResponse +from io import BytesIO +from services.file_service import upload_file, download_file, delete_file + +router = APIRouter() + +@router.post("/upload") +async def upload_file_route(file: UploadFile = File(...)): + """Route to upload a file to cloud storage.""" + return upload_file(file) + + +@router.get("/download") +async def download_route(file_url: str): + """Route to download a file from cloud storage using its URL.""" + try: + result = download_file(file_url) + file_content = result["file"] + media_type = result["media_type"] + filename = file_url.split('/')[-1] + + return StreamingResponse(BytesIO(file_content), media_type=media_type, headers={ + "Content-Disposition": f"attachment; filename={filename}" + }) + except HTTPException as e: + raise e + except Exception: + raise HTTPException(status_code=500, detail="An error occurred while downloading the file") + + +@router.delete("/delete/{file_name}") +async def delete_file_route(file_name: str): + """Route to delete a file in cloud storage by its name.""" + return delete_file(file_name) diff --git a/backend/app/api/v1/file_handler.py b/backend/app/api/v1/file_handler.py deleted file mode 100644 index 40e89a5..0000000 --- a/backend/app/api/v1/file_handler.py +++ /dev/null @@ -1,38 +0,0 @@ -from fastapi import APIRouter, File, UploadFile, HTTPException -from fastapi.responses import FileResponse -import os - -router = APIRouter() - -TEMP_DIR = "temp" - -if not os.path.exists(TEMP_DIR): - os.makedirs(TEMP_DIR) - - -def get_media_type(file_path: str): - """Get media type based on file extension""" - if file_path.endswith(".pdf"): - return "application/pdf" - elif file_path.endswith(".txt"): - return "text/plain" - elif file_path.endswith(".csv"): - return "text/csv" - return "application/octet-stream" - - -@router.post("/upload_file") -async def upload_file(file: UploadFile = File(...)): - file_path = os.path.join(TEMP_DIR, file.filename) - with open(file_path, "wb") as buffer: - buffer.write(file.file.read()) - return {"message": "File uploaded successfully", "file_path": file_path} - - -@router.get("/download_file/{file_name}") -async def download_file(file_name: str): - file_path = os.path.join(TEMP_DIR, file_name) - if not os.path.exists(file_path): - raise HTTPException(status_code=404, detail="File not found") - media_type = get_media_type(file_path) - return FileResponse(file_path, media_type=media_type, filename=file_name) diff --git a/backend/app/api/v1/tasks.py b/backend/app/api/v1/tasks.py index 4cc383e..82ec5d2 100644 --- a/backend/app/api/v1/tasks.py +++ b/backend/app/api/v1/tasks.py @@ -1,7 +1,10 @@ from celery import Celery +from firebase_admin import storage from core.config import settings +from core import firebase_init from core.ceaser_cipher import caesar_cipher, ReaderFactory -from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Literal import os @@ -11,34 +14,39 @@ backend=settings.CELERY_RESULT_BACKEND ) +bucket = storage.bucket(settings.GCS_BUCKET_NAME) -@celery.task -def encrypt_file(file_path: Path, shift: int): - reader = ReaderFactory.get_reader(file_path) - data = reader.read() - encrypted_data = caesar_cipher(data, shift, decrypt=False) - file_name, file_extension = os.path.splitext(file_path) - encrypted_file_path = f"{file_name}_encrypted{file_extension}" +def process_file(file_path: str, shift: int, operation: Literal["encrypt", "decrypt"]) -> str: + """Process file with Caesar cipher and upload to cloud storage.""" - reader.write(encrypted_data, output_file=encrypted_file_path) + with TemporaryDirectory() as temp_dir: + local_file_path = os.path.join(temp_dir, os.path.basename(file_path)) + blob = bucket.blob(file_path) + blob.download_to_filename(local_file_path) - os.remove(file_path) - - return encrypted_file_path + reader = ReaderFactory.get_reader(local_file_path) + data = reader.read() + processed_data = caesar_cipher(data, shift, decrypt=(operation == "decrypt")) + output_file_name = f"{os.path.splitext(file_path)[0]}_{operation}{os.path.splitext(file_path)[1]}" + processed_file_path = os.path.join(temp_dir, output_file_name) + reader.write(processed_data, output_file=processed_file_path) -@celery.task -def decrypt_file(file_path: str, shift: int): - reader = ReaderFactory.get_reader(file_path) - data = reader.read() - decrypted_data = caesar_cipher(data, shift, decrypt=True) + destination_path = f"{operation}/{os.path.basename(processed_file_path)}" + processed_blob = bucket.blob(destination_path) + processed_blob.upload_from_filename(processed_file_path) - file_name, file_extension = os.path.splitext(file_path) - decrypted_file_path = f"{file_name}_decrypted{file_extension}" + return processed_blob.public_url - reader.write(decrypted_data, output_file=decrypted_file_path) - os.remove(file_path) +@celery.task +def encrypt_file(file_path: str, shift: int) -> str: + """Encrypt a file and upload the result to cloud storage.""" + return process_file(file_path, shift, operation="encrypt") - return decrypted_file_path \ No newline at end of file + +@celery.task +def decrypt_file(file_path: str, shift: int) -> str: + """Decrypt a file and upload the result to cloud storage.""" + return process_file(file_path, shift, operation="decrypt") diff --git a/backend/app/core/ceaser_cipher.py b/backend/app/core/ceaser_cipher.py index 1a3a914..7397f44 100644 --- a/backend/app/core/ceaser_cipher.py +++ b/backend/app/core/ceaser_cipher.py @@ -3,8 +3,8 @@ from pypdf import PdfReader from reportlab.platypus import SimpleDocTemplate, PageBreak, Paragraph from reportlab.lib.styles import getSampleStyleSheet -from reportlab.lib.units import inch from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import inch from abc import ABC, abstractmethod @@ -22,36 +22,14 @@ def write(self, data, output_file=None): pass -class ReaderFactory: - """Factory class to get the appropriate reader based on file type""" - readers = { - ".pdf": lambda file_path: CustomPdfReader(file_path), - ".txt": lambda file_path: CustomTextReader(file_path), - ".csv": lambda file_path: CustomCsvReader(file_path), - } - - @staticmethod - def get_reader(file_path: Path): - file_path = Path(file_path) - reader_class = ReaderFactory.readers.get(file_path.suffix) - if reader_class: - return reader_class(file_path) - raise ValueError("Unsupported file type") - - class CustomPdfReader(BaseReader): """Custom PDF reader class""" def read(self): - text = "" with open(self.file_path, "rb") as f: reader = PdfReader(f) - for page in reader.pages: - text += page.extract_text() - return text - + return "".join(page.extract_text() for page in reader.pages) def write(self, data, output_file=None): - output_file = output_file doc = SimpleDocTemplate( output_file, pagesize=A4, @@ -60,13 +38,8 @@ def write(self, data, output_file=None): rightMargin=0.8 * inch, leftMargin=0.8 * inch, ) - story = [] styles = getSampleStyleSheet() - preformatted_style = styles["Normal"] - - preformatted = Paragraph(data, preformatted_style) - story.append(preformatted) - story.append(PageBreak()) + story = [Paragraph(data if isinstance(data, str) else "\n".join(data), styles["Normal"]), PageBreak()] doc.build(story) @@ -77,7 +50,6 @@ def read(self): return f.read() def write(self, data, output_file=None): - output_file = output_file with open(output_file, "w", encoding="utf-8") as f: f.write(data) @@ -86,45 +58,41 @@ class CustomCsvReader(BaseReader): """Custom CSV reader class""" def read(self): with open(self.file_path, "r", encoding="utf-8") as f: - reader = csv.reader(f) - return [row for row in reader] + return list(csv.reader(f)) def write(self, data, output_file=None): - output_file = output_file with open(output_file, "w", newline="", encoding="utf-8") as f: - writer = csv.writer(f) - writer.writerows(data) + csv.writer(f).writerows(data) + + +class ReaderFactory: + """Factory class to get the appropriate reader based on file type""" + readers = { + ".pdf": CustomPdfReader, + ".txt": CustomTextReader, + ".csv": CustomCsvReader, + } + + @staticmethod + def get_reader(file_path: Path): + file_path = Path(file_path) + reader_class = ReaderFactory.readers.get(file_path.suffix) + if reader_class: + return reader_class(file_path) + raise ValueError("Unsupported file type") def cryptify_string(text, shift): """Encrypt/Decrypt text using Caesar cipher""" - result = [] - for char in text: - if char.isalpha(): - offset = 65 if char.isupper() else 97 - result.append(chr((ord(char) - offset + shift) % 26 + offset)) - else: - result.append(char) - return "".join(result) + return "".join( + chr((ord(char) - (65 if char.isupper() else 97) + shift) % 26 + (65 if char.isupper() else 97)) + if char.isalpha() else char for char in text + ) def caesar_cipher(data, shift, decrypt=False): """Caesar cipher implementation""" - if decrypt: - shift = -shift - - result = [] + shift = -shift if decrypt else shift if isinstance(data, list): - for row in data: - cryptify_row = [] - for item in row: - if isinstance(item, str): - cryptify_row.append(cryptify_string(item, shift)) - else: - cryptify_row.append(item) - result.append(cryptify_row) - else: - result.append(cryptify_string(data, shift)) - return result - - + return [[cryptify_string(str(item), shift) if isinstance(item, str) else item for item in row] for row in data] + return cryptify_string(data, shift) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 66388b1..38d07cc 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -13,5 +13,6 @@ class Setting(BaseSettings): DATABASE_URL: str CELERY_BROKER_URL: str CELERY_RESULT_BACKEND: str + GCS_BUCKET_NAME: str settings = Setting() \ No newline at end of file diff --git a/backend/app/core/firebase_init.py b/backend/app/core/firebase_init.py new file mode 100644 index 0000000..8c629ad --- /dev/null +++ b/backend/app/core/firebase_init.py @@ -0,0 +1,9 @@ +import firebase_admin +from firebase_admin import credentials + + +cred = credentials.Certificate("core/firebase-adminsdk.json") +firebase_admin.initialize_app(cred) + + + diff --git a/backend/app/core/security.py b/backend/app/core/security.py index cdff7c4..77c6164 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -38,6 +38,7 @@ def verify_token(token: str): except Exception as e: return False + def get_user_id_from_token(token: str): payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) - return payload.get("user_id") \ No newline at end of file + return payload.get("user_id") diff --git a/backend/app/crud/document_crud.py b/backend/app/crud/document_crud.py index 5728357..275c376 100644 --- a/backend/app/crud/document_crud.py +++ b/backend/app/crud/document_crud.py @@ -4,11 +4,12 @@ def create_document(db: Session, document: document_schema.DocumentCreate): db_document = document_model.Document( - filename=document.filename, - content=document.content, - status=document.status, task_id=document.task_id, - user_id=document.user_id + original_document_url=document.original_document_url, + processed_document_url=document.processed_document_url, + user_id=document.user_id, + date_created=document.date_created, + date_updated=document.date_updated ) db.add(db_document) db.commit() diff --git a/backend/app/crud/user_crud.py b/backend/app/crud/user_crud.py index e7dea84..9f50f00 100644 --- a/backend/app/crud/user_crud.py +++ b/backend/app/crud/user_crud.py @@ -1,27 +1,20 @@ from sqlalchemy.orm import Session -from fastapi import HTTPException -from core import security -from models import user_model -from schemas import user_schema +from models.user_model import User +from schemas.user_schema import UserCreate -def create_user(db: Session, user: user_schema.UserCreate): - hashed_password = security.get_password_hash(user.password) - db_user = user_model.User(username=user.username, email=user.email, password=hashed_password) +def create_user(db: Session, user: UserCreate): + db_user = User(username=user.username, email=user.email, password=user.password) db.add(db_user) db.commit() db.refresh(db_user) return db_user -def authenticate_user(db: Session, email: str, password: str): - user = db.query(user_model.User).filter(user_model.User.email == email).first() - if not user or not security.verify_password(password, user.password): - return False - return user def get_user(db: Session, user_id: int): - return db.query(user_model.User).filter(user_model.User.id == user_id).first() + return db.query(User).filter(User.id == user_id).first() -def get_current_user(db: Session, token: str): - user_id = security.get_user_id_from_token(token) - return get_user(db, user_id) \ No newline at end of file +def delete_user(db: Session, user_id: int): + db.query(User).filter(User.id == user_id).delete() + db.commit() + return \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index aec005b..ede0187 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from db.session import engine from db.base import Base -from api.v1 import auth, cipher, file_handler +from api.v1 import auth, cipher, file app = FastAPI() @@ -9,7 +9,7 @@ app.include_router(auth.router) app.include_router(cipher.router) -app.include_router(file_handler.router) +app.include_router(file.router) Base.metadata.create_all(bind=engine) @@ -17,4 +17,5 @@ @app.get("/") def read_root(): + """Root route""" return {"message": "Welcome to Cryptify API!"} \ No newline at end of file diff --git a/backend/app/models/document_model.py b/backend/app/models/document_model.py index 3da455a..4cf5173 100644 --- a/backend/app/models/document_model.py +++ b/backend/app/models/document_model.py @@ -3,16 +3,17 @@ from sqlalchemy import String, Column, Integer, ForeignKey, DateTime, LargeBinary from sqlalchemy.orm import relationship + class Document(Base): __tablename__ = "documents" - + id = Column(Integer, primary_key= True, nullable=False) user_id = Column(Integer, ForeignKey("users.id")) - filename = Column(String,index=True) - content = Column(LargeBinary, default=None) - status = Column(String, nullable=False) - task_id = Column(String, nullable=False) - uploaded_at = Column(DateTime, default=datetime.now(timezone.utc)) + task_id = Column(String, nullable=True) + original_document_url = Column(String, nullable=False) + processed_document_url = Column(String, nullable=True) + date_created = Column(DateTime, default=datetime.now(timezone.utc)) + date_updated = Column(DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) - user = relationship("User", back_populates="documents") \ No newline at end of file + user = relationship("User", back_populates="documents") diff --git a/backend/app/requirements.txt b/backend/app/requirements.txt index 6789490..0fc70d0 100644 --- a/backend/app/requirements.txt +++ b/backend/app/requirements.txt @@ -3,22 +3,40 @@ annotated-types==0.7.0 anyio==4.6.0 bcrypt==3.2.0 billiard==4.2.1 +CacheControl==0.14.0 +cachetools==5.5.0 celery==5.4.0 certifi==2024.8.30 cffi==1.17.1 chardet==5.2.0 +charset-normalizer==3.4.0 click==8.1.7 click-didyoumean==0.3.1 click-plugins==1.1.1 click-repl==0.3.0 colorama==0.4.6 +cryptography==43.0.3 dnspython==2.7.0 email_validator==2.2.0 fastapi==0.115.0 fastapi-cli==0.0.5 +firebase-admin==6.5.0 +google-api-core==2.21.0 +google-api-python-client==2.149.0 +google-auth==2.35.0 +google-auth-httplib2==0.2.0 +google-cloud-core==2.4.1 +google-cloud-firestore==2.19.0 +google-cloud-storage==2.18.2 +google-crc32c==1.6.0 +google-resumable-media==2.7.2 +googleapis-common-protos==1.65.0 greenlet==3.1.1 +grpcio==1.67.1 +grpcio-status==1.67.1 h11==0.14.0 httpcore==1.0.6 +httplib2==0.22.0 httptools==0.6.1 httpx==0.27.2 idna==3.10 @@ -27,15 +45,21 @@ kombu==5.4.2 markdown-it-py==3.0.0 MarkupSafe==2.1.5 mdurl==0.1.2 +msgpack==1.1.0 passlib==1.7.4 pillow==10.4.0 prompt_toolkit==3.0.48 +proto-plus==1.25.0 +protobuf==5.28.3 +pyasn1==0.6.1 +pyasn1_modules==0.4.1 pycparser==2.22 pydantic==2.9.2 pydantic-settings==2.5.2 pydantic_core==2.23.4 Pygments==2.18.0 PyJWT==2.9.0 +pyparsing==3.2.0 pypdf==5.0.1 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 @@ -43,7 +67,9 @@ python-multipart==0.0.12 PyYAML==6.0.2 redis==5.1.1 reportlab==4.2.5 +requests==2.32.3 rich==13.9.2 +rsa==4.9 shellingham==1.5.4 six==1.16.0 sniffio==1.3.1 @@ -52,6 +78,8 @@ starlette==0.38.6 typer==0.12.5 typing_extensions==4.12.2 tzdata==2024.2 +uritemplate==4.1.1 +urllib3==2.2.3 uvicorn==0.31.0 vine==5.1.0 watchfiles==0.24.0 diff --git a/backend/app/schemas/ceaser_schema.py b/backend/app/schemas/ceaser_schema.py deleted file mode 100644 index e9a41cf..0000000 --- a/backend/app/schemas/ceaser_schema.py +++ /dev/null @@ -1,16 +0,0 @@ -from pydantic import BaseModel - -class CeaserCipherBase(BaseModel): - """Ceaser Cipher Model""" - text : str - shift : int - -class CeaserCipherCreate(CeaserCipherBase): - """extends CeaserCipherBase""" - decrypt : bool - -class CeaserCipherResponse(CeaserCipherBase): - id: int - - class Config: - from_attributes = True \ No newline at end of file diff --git a/backend/app/schemas/document_schema.py b/backend/app/schemas/document_schema.py index 114b808..67e0c70 100644 --- a/backend/app/schemas/document_schema.py +++ b/backend/app/schemas/document_schema.py @@ -1,11 +1,14 @@ from pydantic import BaseModel +from typing import Optional +from datetime import datetime class DocumentBase(BaseModel): """Document Model""" - filename : str - content : bytes - status : str task_id : str + original_document_url : str + processed_document_url : str + date_created : Optional[datetime] + date_updated : Optional[datetime] class DocumentCreate(DocumentBase): """extends DocumentBase""" diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py new file mode 100644 index 0000000..7352e1b --- /dev/null +++ b/backend/app/services/auth_service.py @@ -0,0 +1,18 @@ +from sqlalchemy.orm import Session +from core.security import create_access_token +from services.user_service import authenticate_user, create_user_with_password + + +def register_user(db: Session, user_data): + user = create_user_with_password(db, user_data) + if not user: + return None + return user + + +def login_user(db: Session, username, password): + user = authenticate_user(db, username, password) + if not user: + return None + token = create_access_token(user_id=user.id, data={"user_id": user.id}) + return token diff --git a/backend/app/services/file_service.py b/backend/app/services/file_service.py new file mode 100644 index 0000000..a3b0626 --- /dev/null +++ b/backend/app/services/file_service.py @@ -0,0 +1,63 @@ +from fastapi import HTTPException, UploadFile +from firebase_admin import storage +from urllib.parse import urlparse +from core.config import settings +from uuid import uuid4 + + +bucket = storage.bucket(settings.GCS_BUCKET_NAME) + +def get_media_type(file_path: str) -> str: + """Determine media type based on file extension.""" + media_types = { + ".pdf": "application/pdf", + ".txt": "text/plain", + ".csv": "text/csv", + } + return media_types.get(file_path[file_path.rfind('.'):], "application/octet-stream") + + +def upload_file(file: UploadFile): + """Upload a file to cloud storage, adjusting for spaces in file name.""" + sanitized_filename = file.filename.replace(" ", "_") + + unique_suffix = uuid4().hex[:8] + sanitized_filename = f"{sanitized_filename.rsplit('.', 1)[0]}_{unique_suffix}.{sanitized_filename.rsplit('.', 1)[-1]}" + + blob = bucket.blob(sanitized_filename) + blob.upload_from_file(file.file) + + return { + "message": "File uploaded successfully", + "file_path": blob.public_url, + "file_name": blob.name + } + + +def download_file(file_url: str): + """Download a file from cloud storage based on its URL.""" + parsed_url = urlparse(file_url) + file_path = parsed_url.path.lstrip("/") + + bucket_name = bucket.name + if file_path.startswith(f"{bucket_name}/"): + file_path = file_path[len(bucket_name) + 1:] + + blob = bucket.blob(file_path) + if not blob.exists(): + raise HTTPException(status_code=404, detail="File not found") + + media_type = get_media_type(file_path) + return { + "file": blob.download_as_string(), + "media_type": media_type + } + + +def delete_file(file_name: str): + """Delete a file in cloud storage by its name.""" + blob = bucket.blob(file_name) + if not blob.exists(): + raise HTTPException(status_code=404, detail="File not found") + blob.delete() + return {"message": "File deleted successfully"} diff --git a/backend/app/services/process_service.py b/backend/app/services/process_service.py new file mode 100644 index 0000000..3a0c967 --- /dev/null +++ b/backend/app/services/process_service.py @@ -0,0 +1,95 @@ +from fastapi import HTTPException +from sqlalchemy.orm import Session +from celery.result import AsyncResult +from firebase_admin import storage +from schemas.document_schema import DocumentCreate +from models.document_model import Document +from crud.document_crud import create_document +from core.security import get_user_id_from_token +from core.config import settings +from api.v1.tasks import encrypt_file, decrypt_file +from datetime import datetime, timezone +from typing import Optional, Literal, Dict + + +bucket = storage.bucket(settings.GCS_BUCKET_NAME) + +def get_current_utc_time() -> datetime: + """Helper function to get the current UTC time.""" + return datetime.now(timezone.utc) + + +def initiate_file_task( + file_path: str, + shift: int, + token: Optional[str], + db: Session, + operation: Literal["encrypt", "decrypt"] +) -> Dict[str, str]: + + """Initiate file encryption or decryption task, optionally creating a document record if authenticated.""" + user_id = get_user_id_from_token(token) if token else None + blob = bucket.blob(file_path) + + if not blob.exists(): + raise HTTPException(status_code=404, detail="File not found") + + task = encrypt_file.delay(file_path, shift) if operation == "encrypt" else decrypt_file.delay(file_path, shift) + + if token and user_id: + document = DocumentCreate( + task_id=task.id, + original_document_url=file_path, + processed_document_url="", + user_id=user_id, + date_created=get_current_utc_time(), + date_updated=get_current_utc_time(), + ) + create_document(db, document) + + return {"message": f"File {operation}ion started", "task_id": task.id} + + +def encrypt_uploaded_file( + file_path: str, + shift: int, + token: Optional[str], + db: Session + ) -> Dict[str, str]: + + """Handle file encryption initiation, creating a document record if authenticated.""" + return initiate_file_task(file_path, shift, token, db, operation="encrypt") + + +def decrypt_uploaded_file( + file_path: str, + shift: int, + token: Optional[str], + db: Session + ) -> Dict[str, str]: + + """Handle file decryption initiation, creating a document record if authenticated.""" + return initiate_file_task(file_path, shift, token, db, operation="decrypt") + + +def update_document_with_result(task_id: str, file_path: str, db: Session) -> None: + """Update the document record with the processed file path upon task completion, if authenticated.""" + document = db.query(Document).filter(Document.task_id == task_id).first() + if document: + document.processed_document_url = file_path + document.date_updated = get_current_utc_time() + db.commit() + + +def get_task_status(task_id: str, db: Session) -> Dict[str, str]: + """Retrieve task status and update document record if task is successful.""" + task = AsyncResult(task_id) + + if task.state == "PENDING": + return {"status": "pending", "details": "Task is being processed"} + elif task.state == "SUCCESS": + file_path = task.result + update_document_with_result(task_id, file_path, db) + return {"status": "success", "details": "completed", "file_path": file_path} + else: + return {"status": "failed", "details": str(task.info)} diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py new file mode 100644 index 0000000..4e16be2 --- /dev/null +++ b/backend/app/services/user_service.py @@ -0,0 +1,23 @@ +from sqlalchemy.orm import Session +from core.security import verify_password, get_password_hash, get_user_id_from_token +from models.user_model import User +from schemas.user_schema import UserCreate +from crud.user_crud import create_user, get_user + + +def create_user_with_password(db: Session, user: UserCreate): + hashed_password = get_password_hash(user.password) + user.password = hashed_password + return create_user(db, user) + + +def authenticate_user(db: Session, email: str, password: str): + user = db.query(User).filter(User.email == email).first() + if not user or not verify_password(password, user.password): + return False + return user + + +def get_current_user(db: Session, token: str): + user_id = get_user_id_from_token(token) + return get_user(db, user_id) \ No newline at end of file