From 3d311e02d584e592a83843b74c508c830bff9ffc Mon Sep 17 00:00:00 2001 From: Abhiram <127508564+abhiramtilakiiit@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:30:31 +0530 Subject: [PATCH] storagefiles: add a storagefile support in the db (#13) * storagefiles: add a storagefile support in the db * storagefile: remove the need for extra base64str validation, move the ReturnType def to otypes * Fix variable error in queries * New document creation check should be case insensitive * Fix create and edit function object creation error. Removed extra class for inputs of storage, and remove extra check for base64 string * Chnage from Base64Str to Base64Bytes * storagefile: otypes: using underscore for consistency * storagefiles: query: add optional argument filetype * storagefiles: switch back from base64 to using str * Minor fix * Pass inter communication secret to signed-url API * Remove unwanted query and fix the storageFiles query * re-add single storagefile API * storagefiles: fix conflicts when rebasing master * storagefiles: fix conflicts when rebasing master * Static file delete, change variable names --------- Co-authored-by: notpua Co-authored-by: Bhav Beri Co-authored-by: Bhav Beri <43399374+bhavberi@users.noreply.github.com> --- db.py | 1 + models.py | 26 ++++++++++- mutations.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++++-- otypes.py | 21 ++++++++- queries.py | 46 +++++++++++++++++-- utils.py | 20 +++++++++ 6 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 utils.py diff --git a/db.py b/db.py index 13f51c8..8273510 100644 --- a/db.py +++ b/db.py @@ -16,3 +16,4 @@ # get database db = client[MONGO_DATABASE] ccdb = db.cc +docsstoragedb = db.docsstorage diff --git a/models.py b/models.py index 2709a7d..5e71777 100644 --- a/models.py +++ b/models.py @@ -4,7 +4,14 @@ import strawberry from bson import ObjectId -from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator +from pydantic import ( + BaseModel, + ConfigDict, + EmailStr, + Field, + TypeAdapter, + field_validator, +) from pydantic_core import core_schema from pytz import timezone @@ -107,3 +114,20 @@ class CCRecruitment(BaseModel): str_strip_whitespace=True, validate_assignment=True, ) + + +class StorageFile(BaseModel): + id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") + title: str + filename: str + filetype: str = "pdf" + modified_time: str = "" + creation_time: str = "" + + model_config = ConfigDict( + populate_by_name=True, + arbitrary_types_allowed=True, + extra="forbid", + str_strip_whitespace=True, + validate_assignment=True, + ) diff --git a/mutations.py b/mutations.py index 5d18531..aae36d4 100644 --- a/mutations.py +++ b/mutations.py @@ -1,9 +1,12 @@ import os +import re +from datetime import datetime +import pytz import strawberry from fastapi.encoders import jsonable_encoder -from db import ccdb +from db import ccdb, docsstoragedb from mailing import send_mail from mailing_templates import ( APPLICANT_CONFIRMATION_BODY, @@ -11,12 +14,20 @@ CC_APPLICANT_CONFIRMATION_BODY, CC_APPLICANT_CONFIRMATION_SUBJECT, ) -from models import CCRecruitment +from models import CCRecruitment, StorageFile # import all models and types -from otypes import CCRecruitmentInput, Info, MailInput +from otypes import ( + CCRecruitmentInput, + Info, + MailInput, + StorageFileInput, + StorageFileType, +) +from utils import delete_file inter_communication_secret_global = os.getenv("INTER_COMMUNICATION_SECRET") +ist = pytz.timezone("Asia/Kolkata") # sample mutation @@ -119,8 +130,115 @@ def ccApply(ccRecruitmentInput: CCRecruitmentInput, info: Info) -> bool: return True +# StorageFile related mutations +@strawberry.mutation +def createStorageFile( + details: StorageFileInput, info: Info +) -> StorageFileType: + """ + Create a new storagefile + returns the created storagefile + + Allowed Roles: ["cc"] + """ + user = info.context.user + + if user is None or user.get("role") != "cc": + raise ValueError("You do not have permission to access this resource.") + + # get time info + current_time = datetime.now(ist) + time_str = current_time.strftime("%d-%m-%Y %I:%M %p IST") + + storagefile = StorageFile( + title=details.title, + filename=details.filename, + filetype=details.filetype, + modified_time=time_str, + creation_time=time_str, + ) + + # Check if any storagefile with same title already exists + if docsstoragedb.find_one( + {"title": {"$regex": f"^{re.escape(details.title)}$", "$options": "i"}} + ): + raise ValueError("A storagefile already exists with this name.") + + created_id = docsstoragedb.insert_one( + jsonable_encoder(storagefile) + ).inserted_id + created_storagefile = docsstoragedb.find_one({"_id": created_id}) + + return StorageFileType.from_pydantic( + StorageFile.model_validate(created_storagefile) + ) + + +@strawberry.mutation +def updateStorageFile(id: str, info: Info) -> bool: + """ + Update an existing storagefile + returns the updated storagefile + + Allowed Roles: ["cc"] + """ + user = info.context.user + + if user is None or user.get("role") != "cc": + raise ValueError("You do not have permission to access this resource.") + + # get time info + current_time = datetime.now(ist) + time_str = current_time.strftime("%d-%m-%Y %I:%M %p IST") + + storagefile = docsstoragedb.find_one({"_id": id}) + if storagefile is None: + raise ValueError("StorageFile not found.") + + updated_storagefile = StorageFile( + _id=id, + title=storagefile["title"], + filename=storagefile["filename"], + filetype=storagefile["filetype"], + modified_time=time_str, + creation_time=storagefile["creation_time"], + ) + + docsstoragedb.find_one_and_update( + {"_id": id}, {"$set": jsonable_encoder(updated_storagefile)} + ) + return True + + +@strawberry.mutation +def deleteStorageFile(id: str, info: Info) -> bool: + """ + Delete an existing storagefile + returns a boolean indicating success + + Allowed Roles: ["cc"] + """ + user = info.context.user + + if user is None or user.get("role") != "cc": + raise ValueError("You do not have permission to access this resource.") + + storagefile = docsstoragedb.find_one({"_id": id}) + if storagefile is None: + raise ValueError("StorageFile not found.") + + # delete the file from storage + delete_file(storagefile["filename"]) + + docsstoragedb.delete_one({"_id": id}) + return True + + # register all mutations mutations = [ sendMail, ccApply, + createStorageFile, + updateStorageFile, + deleteStorageFile, ] diff --git a/otypes.py b/otypes.py index 08ad883..1feeaf8 100644 --- a/otypes.py +++ b/otypes.py @@ -7,7 +7,7 @@ from strawberry.types import Info as _Info from strawberry.types.info import RootValueType -from models import CCRecruitment, Mails, PyObjectId +from models import CCRecruitment, Mails, PyObjectId, StorageFile # custom context class @@ -80,3 +80,22 @@ class CCRecruitmentType: @strawberry.type class SignedURL: url: str + + +@strawberry.input +class SignedURLInput: + static_file: bool = False + filename: str | None = None + + +# StorageFile Types +@strawberry.experimental.pydantic.input( + model=StorageFile, fields=["title", "filename", "filetype"] +) +class StorageFileInput: + pass + + +@strawberry.experimental.pydantic.type(model=StorageFile, all_fields=True) +class StorageFileType: + pass diff --git a/queries.py b/queries.py index f8893e5..2b6d3d6 100644 --- a/queries.py +++ b/queries.py @@ -5,18 +5,24 @@ import requests import strawberry -from db import ccdb -from models import CCRecruitment +from db import ccdb, docsstoragedb +from models import CCRecruitment, StorageFile # import all models and types -from otypes import CCRecruitmentType, Info, SignedURL +from otypes import ( + CCRecruitmentType, + Info, + SignedURL, + SignedURLInput, + StorageFileType, +) inter_communication_secret = os.getenv("INTER_COMMUNICATION_SECRET") # fetch signed url from the files service @strawberry.field -def signedUploadURL(info: Info) -> SignedURL: +def signedUploadURL(details: SignedURLInput, info: Info) -> SignedURL: user = info.context.user if not user: raise Exception("Not logged in!") @@ -26,6 +32,8 @@ def signedUploadURL(info: Info) -> SignedURL: "http://files/signed-url", params={ "user": json.dumps(user), + "static_file": "true" if details.static_file else "false", + "filename": details.filename, "inter_communication_secret": inter_communication_secret, }, ) @@ -70,9 +78,39 @@ def haveAppliedForCC(info: Info) -> bool: return False +# Storagefile queries + + +@strawberry.field +def storagefiles(filetype: str) -> List[StorageFileType]: + """ + Get all storage files + Returns a list of storage files with basic info (id and title) + """ + storage_files = docsstoragedb.find({"filetype": filetype}) + return [ + StorageFileType.from_pydantic(StorageFile.model_validate(storage_file)) + for storage_file in storage_files + ] + + +@strawberry.field +def storagefile(file_id: str) -> StorageFileType: + """ + Get a single storage file by id + Returns a single storage file with all info + """ + storage_file = docsstoragedb.find_one({"_id": file_id}) + return StorageFileType.from_pydantic( + StorageFile.model_validate(storage_file) + ) + + # register all queries queries = [ signedUploadURL, ccApplications, haveAppliedForCC, + storagefiles, + storagefile, ] diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..1f2b752 --- /dev/null +++ b/utils.py @@ -0,0 +1,20 @@ +import os + +import requests + +inter_communication_secret = os.getenv("INTER_COMMUNICATION_SECRET") + +def delete_file(filename): + response = requests.post( + "http://files/delete-file", + params={ + "filename": filename, + "inter_communication_secret": inter_communication_secret, + "static_file": "true", + }, + ) + + if response.status_code != 200: + raise Exception(response.text) + + return response.text \ No newline at end of file