"""Per-project upload storage. Each project gets its OWN folder -> no shared, rescanned "current uploads" directory. A project's document set is exactly what's in its own folder, nothing else, so a new project can never see another project's (or a prior project's leftover) files. Duplication across projects is an accepted tradeoff for that isolation. """ import os import shutil from typing import List PROJECT_UPLOADS_ROOT = os.path.expanduser("~/.studybuddy/projects/uploads") _SUPPORTED_EXTS = {".pdf", ".docx", ".txt"} def project_upload_dir(project_id: str) -> str: d = os.path.join(PROJECT_UPLOADS_ROOT, project_id) os.makedirs(d, exist_ok=True) return d def delete_project_uploads(project_id: str) -> None: folder = os.path.join(PROJECT_UPLOADS_ROOT, project_id) shutil.rmtree(folder, ignore_errors=True) def list_project_files(project_id: str) -> List[str]: folder = project_upload_dir(project_id) return sorted( os.path.join(folder, f) for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f)) and os.path.splitext(f)[1].lower() in _SUPPORTED_EXTS ) def add_file_to_project(project_id: str, file_path: str) -> str: folder = project_upload_dir(project_id) filename = os.path.basename(file_path) dest = os.path.join(folder, filename) shutil.copy2(file_path, dest) return dest async def remove_file_from_project(project_id: str, filename: str) -> bool: from app.rag.chromadb_client import ChromaDBClient from app.rag.evidence_ingestion import EvidenceIngestionService folder = project_upload_dir(project_id) file_path = os.path.join(folder, filename) if not os.path.exists(file_path): return False try: with open(file_path, "rb") as f: content = f.read() document_id = ChromaDBClient.file_hash(content) except Exception: document_id = None # Remove derived state first. If this fails, keep the irreplaceable source # PDF so the operation can be retried safely. if document_id: EvidenceIngestionService().remove_document(project_id, document_id) from app.services.paper_metadata import PaperMetadataService PaperMetadataService().forget_file_metadata(document_id) os.remove(file_path) # Cognee is cross-project student memory, not a mirror of project papers; # paper deletion therefore does not mutate the durable learner profile. return True