From c7c8ec23540b0d18a3c7e4a84d422c213d491a63 Mon Sep 17 00:00:00 2001 From: Vraj Patel Date: Mon, 28 Jul 2025 23:52:29 +0530 Subject: [PATCH 1/5] fix(services): fix mismatch between imageUrl and avatar --- backend/app/auth/schemas.py | 2 +- backend/app/auth/service.py | 8 ++++---- backend/app/groups/service.py | 8 ++++---- backend/app/user/service.py | 7 ++++--- backend/tests/auth/test_auth_routes.py | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/backend/app/auth/schemas.py b/backend/app/auth/schemas.py index 3c76dc3e..c6ea1d5b 100644 --- a/backend/app/auth/schemas.py +++ b/backend/app/auth/schemas.py @@ -42,7 +42,7 @@ class UserResponse(BaseModel): id: str = Field(alias="_id") email: str name: str - avatar: Optional[str] = None + imageUrl: Optional[str] = None currency: str = "USD" created_at: datetime diff --git a/backend/app/auth/service.py b/backend/app/auth/service.py index fcd46993..465317ca 100644 --- a/backend/app/auth/service.py +++ b/backend/app/auth/service.py @@ -115,7 +115,7 @@ async def create_user_with_email( "email": email, "hashed_password": get_password_hash(password), "name": name, - "avatar": None, + "imageUrl": None, "currency": "USD", "created_at": datetime.now(timezone.utc), "auth_provider": "email", @@ -202,8 +202,8 @@ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]: update_data = {} if user.get("firebase_uid") != firebase_uid: update_data["firebase_uid"] = firebase_uid - if user.get("avatar") != picture and picture: - update_data["avatar"] = picture + if user.get("imageUrl") != picture and picture: + update_data["imageUrl"] = picture if update_data: await db.users.update_one( @@ -215,7 +215,7 @@ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]: user_doc = { "email": email, "name": name, - "avatar": picture, + "imageUrl": picture, "currency": "USD", "created_at": datetime.now(timezone.utc), "auth_provider": "google", diff --git a/backend/app/groups/service.py b/backend/app/groups/service.py index 6d40a7bc..ef55ce23 100644 --- a/backend/app/groups/service.py +++ b/backend/app/groups/service.py @@ -54,8 +54,8 @@ async def _enrich_members_with_user_details( if user else f"{member_user_id}@example.com" ), - "avatar": ( - user.get("imageUrl") or user.get("avatar") + "imageUrl": ( + user.get("imageUrl") if user else None ), @@ -64,7 +64,7 @@ async def _enrich_members_with_user_details( else { "name": f"User {member_user_id[-4:]}", "email": f"{member_user_id}@example.com", - "avatar": None, + "imageUrl": None, } ), } @@ -79,7 +79,7 @@ async def _enrich_members_with_user_details( "user": { "name": f"User {member_user_id[-4:]}", "email": f"{member_user_id}@example.com", - "avatar": None, + "imageUrl": None, }, } ) diff --git a/backend/app/user/service.py b/backend/app/user/service.py index 589a682a..3dddbecb 100644 --- a/backend/app/user/service.py +++ b/backend/app/user/service.py @@ -41,7 +41,7 @@ def iso(dt): "id": user_id, "name": user.get("name"), "email": user.get("email"), - "imageUrl": user.get("imageUrl") or user.get("avatar"), + "imageUrl": user.get("imageUrl"), "currency": user.get("currency", "USD"), "createdAt": iso(user.get("created_at")), "updatedAt": iso(user.get("updated_at") or user.get("created_at")), @@ -67,7 +67,9 @@ async def update_user_profile(self, user_id: str, updates: dict) -> Optional[dic updates = {k: v for k, v in updates.items() if k in allowed} updates["updated_at"] = datetime.now(timezone.utc) result = await db.users.find_one_and_update( - {"_id": obj_id}, {"$set": updates}, return_document=True + {"_id": obj_id}, + {"$set": updates}, + return_document=True ) return self.transform_user_document(result) @@ -80,5 +82,4 @@ async def delete_user(self, user_id: str) -> bool: result = await db.users.delete_one({"_id": obj_id}) return result.deleted_count > 0 - user_service = UserService() diff --git a/backend/tests/auth/test_auth_routes.py b/backend/tests/auth/test_auth_routes.py index 0610a0fe..aa764f81 100644 --- a/backend/tests/auth/test_auth_routes.py +++ b/backend/tests/auth/test_auth_routes.py @@ -168,7 +168,7 @@ async def test_login_with_email_success(mock_db): "email": user_email, "hashed_password": hashed_password, "name": "Login User", - "avatar": None, + "imageUrl": None, "currency": "USD", # Ensure datetime is used "created_at": datetime.now(timezone.utc), From 10f0d3afd0637f3f66efd2c417cfad5b0e602c19 Mon Sep 17 00:00:00 2001 From: Vraj Patel Date: Tue, 29 Jul 2025 00:28:03 +0530 Subject: [PATCH 2/5] feat(migration-script): add migration, rollback and backup script --- backend/scripts/backup_db.py | 77 ++++++++++ backend/scripts/migrate_avatar_to_imageurl.py | 145 ++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 backend/scripts/backup_db.py create mode 100644 backend/scripts/migrate_avatar_to_imageurl.py diff --git a/backend/scripts/backup_db.py b/backend/scripts/backup_db.py new file mode 100644 index 00000000..ec8acdd7 --- /dev/null +++ b/backend/scripts/backup_db.py @@ -0,0 +1,77 @@ +""" +Database backup script for Splitwiser. +Creates a backup of all collections before performing migrations. +""" +import json +from datetime import datetime +import os +from dotenv import load_dotenv +from pymongo import MongoClient + +# Get the script's directory and backend directory +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +BACKEND_DIR = os.path.dirname(SCRIPT_DIR) + +# Load environment variables from .env file in backend directory +load_dotenv(os.path.join(BACKEND_DIR, '.env')) + +# Get MongoDB connection details from environment +MONGODB_URL = os.getenv('MONGODB_URL') +DATABASE_NAME = os.getenv('DATABASE_NAME') + +def create_backup(): + """Create a backup of all collections.""" + try: + # Create backup directory if it doesn't exist + backup_dir = "backups" + backup_time = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_path = os.path.join(backup_dir, f"backup_{backup_time}") + os.makedirs(backup_path, exist_ok=True) + + # Connect to MongoDB + client = MongoClient(MONGODB_URL) + db = client[DATABASE_NAME] + + # Get all collections + collections = db.list_collection_names() + backup_stats = {} + + for collection_name in collections: + collection = db[collection_name] + documents = list(collection.find({})) + + # Convert ObjectId to string for JSON serialization + for doc in documents: + doc['_id'] = str(doc['_id']) + + # Save to file + backup_file = os.path.join(backup_path, f"{collection_name}.json") + with open(backup_file, 'w') as f: + json.dump(documents, f, indent=2, default=str) + + backup_stats[collection_name] = len(documents) + + # Save backup metadata + metadata = { + 'timestamp': datetime.now().isoformat(), + 'database': DATABASE_NAME, + 'collections': backup_stats, + 'total_documents': sum(backup_stats.values()) + } + + with open(os.path.join(backup_path, "backup_metadata.json"), 'w') as f: + json.dump(metadata, f, indent=2) + + return backup_path, metadata + + except Exception as e: + print(f"Backup failed: {str(e)}") + raise + +if __name__ == "__main__": + backup_path, metadata = create_backup() + print(f"Backup created successfully at: {backup_path}") + print("\nBackup statistics:") + print(f"Total documents: {metadata['total_documents']}") + for coll, count in metadata['collections'].items(): + print(f"{coll}: {count} documents") diff --git a/backend/scripts/migrate_avatar_to_imageurl.py b/backend/scripts/migrate_avatar_to_imageurl.py new file mode 100644 index 00000000..acbb8ef4 --- /dev/null +++ b/backend/scripts/migrate_avatar_to_imageurl.py @@ -0,0 +1,145 @@ +""" +Migration script to standardize user avatar fields to imageUrl. +This script: +1. Identifies users with avatar field but no imageUrl field +2. Copies avatar values to imageUrl field +3. Removes the deprecated avatar field +4. Logs migration statistics +""" +import logging +from datetime import datetime +import os +import json +import sys +from pymongo import MongoClient, UpdateOne +from bson import ObjectId +from dotenv import load_dotenv + +# Add the script's directory to Python path +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_DIR) + +from backup_db import create_backup + +# Load environment variables from the backend directory +BACKEND_DIR = os.path.dirname(SCRIPT_DIR) +load_dotenv(os.path.join(BACKEND_DIR, '.env')) + +# Get MongoDB connection details from environment +MONGODB_URL = os.getenv('MONGODB_URL') +DATABASE_NAME = os.getenv('DATABASE_NAME') + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Set up file logging +log_dir = "logs" +os.makedirs(log_dir, exist_ok=True) +log_file = os.path.join(log_dir, f"migration_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log") +file_handler = logging.FileHandler(log_file) +file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) +logger.addHandler(file_handler) + +def migrate_avatar_to_imageurl(): + """ + Migrate avatar field to imageUrl in users collection. + Returns statistics about the migration. + """ + try: + # First create a backup + logger.info("Creating database backup...") + backup_path, backup_metadata = create_backup() + logger.info(f"Backup created at: {backup_path}") + + # Connect to MongoDB + client = MongoClient(MONGODB_URL) + db = client[DATABASE_NAME] + users = db.users + + # Find users with avatar field + users_with_avatar = users.find({"avatar": {"$exists": True}}) + users_to_update = [] + stats = { + "total_users": users.count_documents({}), + "users_with_avatar": 0, + "users_with_both_fields": 0, + "users_updated": 0, + "conflicts": 0 + } + + for user in users_with_avatar: + stats["users_with_avatar"] += 1 + + # Check for conflicts (users with both fields) + if "imageUrl" in user and user["imageUrl"] is not None: + if user["imageUrl"] != user["avatar"]: + logger.warning(f"Conflict found for user {user['_id']}: " + f"avatar='{user['avatar']}', imageUrl='{user['imageUrl']}'") + stats["conflicts"] += 1 + continue + stats["users_with_both_fields"] += 1 + + # Prepare update + users_to_update.append( + UpdateOne( + {"_id": user["_id"]}, + { + "$set": {"imageUrl": user["avatar"]}, + "$unset": {"avatar": ""} + } + ) + ) + + # Perform bulk update if there are users to update + if users_to_update: + result = users.bulk_write(users_to_update) + stats["users_updated"] = result.modified_count + logger.info(f"Successfully updated {result.modified_count} users") + + return stats + + except Exception as e: + logger.error(f"Migration failed: {str(e)}") + raise + +def rollback_migration(backup_path): + """ + Rollback the migration using a specified backup. + """ + try: + client = MongoClient(MONGODB_URL) + db = client[DATABASE_NAME] + + # Read users collection backup + with open(os.path.join(backup_path, "users.json"), 'r') as f: + users_backup = json.load(f) + + # Convert string IDs back to ObjectId + for user in users_backup: + user['_id'] = ObjectId(user['_id']) + + # Replace current users collection with backup + db.users.drop() + if users_backup: + db.users.insert_many(users_backup) + + logger.info(f"Successfully rolled back to backup: {backup_path}") + return True + + except Exception as e: + logger.error(f"Rollback failed: {str(e)}") + raise + +if __name__ == "__main__": + logger.info("Starting avatar to imageUrl migration...") + stats = migrate_avatar_to_imageurl() + + logger.info("\nMigration completed. Statistics:") + logger.info(f"Total users: {stats['total_users']}") + logger.info(f"Users with avatar field: {stats['users_with_avatar']}") + logger.info(f"Users with both fields: {stats['users_with_both_fields']}") + logger.info(f"Users updated: {stats['users_updated']}") + logger.info(f"Conflicts found: {stats['conflicts']}") + + print("\nMigration completed. Check the log file for details:", log_file) From 119d7f8a7c7b458b46c211b993f13ab587495a58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:07:13 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- backend/app/groups/service.py | 6 +-- backend/app/user/service.py | 5 +- backend/scripts/backup_db.py | 36 +++++++------ backend/scripts/migrate_avatar_to_imageurl.py | 51 +++++++++++-------- 4 files changed, 53 insertions(+), 45 deletions(-) diff --git a/backend/app/groups/service.py b/backend/app/groups/service.py index ef55ce23..0b6637d2 100644 --- a/backend/app/groups/service.py +++ b/backend/app/groups/service.py @@ -54,11 +54,7 @@ async def _enrich_members_with_user_details( if user else f"{member_user_id}@example.com" ), - "imageUrl": ( - user.get("imageUrl") - if user - else None - ), + "imageUrl": (user.get("imageUrl") if user else None), } if user else { diff --git a/backend/app/user/service.py b/backend/app/user/service.py index 3dddbecb..17a71dce 100644 --- a/backend/app/user/service.py +++ b/backend/app/user/service.py @@ -67,9 +67,7 @@ async def update_user_profile(self, user_id: str, updates: dict) -> Optional[dic updates = {k: v for k, v in updates.items() if k in allowed} updates["updated_at"] = datetime.now(timezone.utc) result = await db.users.find_one_and_update( - {"_id": obj_id}, - {"$set": updates}, - return_document=True + {"_id": obj_id}, {"$set": updates}, return_document=True ) return self.transform_user_document(result) @@ -82,4 +80,5 @@ async def delete_user(self, user_id: str) -> bool: result = await db.users.delete_one({"_id": obj_id}) return result.deleted_count > 0 + user_service = UserService() diff --git a/backend/scripts/backup_db.py b/backend/scripts/backup_db.py index ec8acdd7..dd135512 100644 --- a/backend/scripts/backup_db.py +++ b/backend/scripts/backup_db.py @@ -2,9 +2,11 @@ Database backup script for Splitwiser. Creates a backup of all collections before performing migrations. """ + import json -from datetime import datetime import os +from datetime import datetime + from dotenv import load_dotenv from pymongo import MongoClient @@ -13,11 +15,12 @@ BACKEND_DIR = os.path.dirname(SCRIPT_DIR) # Load environment variables from .env file in backend directory -load_dotenv(os.path.join(BACKEND_DIR, '.env')) +load_dotenv(os.path.join(BACKEND_DIR, ".env")) # Get MongoDB connection details from environment -MONGODB_URL = os.getenv('MONGODB_URL') -DATABASE_NAME = os.getenv('DATABASE_NAME') +MONGODB_URL = os.getenv("MONGODB_URL") +DATABASE_NAME = os.getenv("DATABASE_NAME") + def create_backup(): """Create a backup of all collections.""" @@ -39,39 +42,40 @@ def create_backup(): for collection_name in collections: collection = db[collection_name] documents = list(collection.find({})) - + # Convert ObjectId to string for JSON serialization for doc in documents: - doc['_id'] = str(doc['_id']) + doc["_id"] = str(doc["_id"]) # Save to file backup_file = os.path.join(backup_path, f"{collection_name}.json") - with open(backup_file, 'w') as f: + with open(backup_file, "w") as f: json.dump(documents, f, indent=2, default=str) - + backup_stats[collection_name] = len(documents) # Save backup metadata metadata = { - 'timestamp': datetime.now().isoformat(), - 'database': DATABASE_NAME, - 'collections': backup_stats, - 'total_documents': sum(backup_stats.values()) + "timestamp": datetime.now().isoformat(), + "database": DATABASE_NAME, + "collections": backup_stats, + "total_documents": sum(backup_stats.values()), } - - with open(os.path.join(backup_path, "backup_metadata.json"), 'w') as f: + + with open(os.path.join(backup_path, "backup_metadata.json"), "w") as f: json.dump(metadata, f, indent=2) return backup_path, metadata - + except Exception as e: print(f"Backup failed: {str(e)}") raise + if __name__ == "__main__": backup_path, metadata = create_backup() print(f"Backup created successfully at: {backup_path}") print("\nBackup statistics:") print(f"Total documents: {metadata['total_documents']}") - for coll, count in metadata['collections'].items(): + for coll, count in metadata["collections"].items(): print(f"{coll}: {count} documents") diff --git a/backend/scripts/migrate_avatar_to_imageurl.py b/backend/scripts/migrate_avatar_to_imageurl.py index acbb8ef4..24d8df04 100644 --- a/backend/scripts/migrate_avatar_to_imageurl.py +++ b/backend/scripts/migrate_avatar_to_imageurl.py @@ -6,28 +6,30 @@ 3. Removes the deprecated avatar field 4. Logs migration statistics """ + +from backup_db import create_backup +import json import logging -from datetime import datetime import os -import json import sys -from pymongo import MongoClient, UpdateOne +from datetime import datetime + from bson import ObjectId from dotenv import load_dotenv +from pymongo import MongoClient, UpdateOne # Add the script's directory to Python path SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.append(SCRIPT_DIR) -from backup_db import create_backup # Load environment variables from the backend directory BACKEND_DIR = os.path.dirname(SCRIPT_DIR) -load_dotenv(os.path.join(BACKEND_DIR, '.env')) +load_dotenv(os.path.join(BACKEND_DIR, ".env")) # Get MongoDB connection details from environment -MONGODB_URL = os.getenv('MONGODB_URL') -DATABASE_NAME = os.getenv('DATABASE_NAME') +MONGODB_URL = os.getenv("MONGODB_URL") +DATABASE_NAME = os.getenv("DATABASE_NAME") # Configure logging logging.basicConfig(level=logging.INFO) @@ -36,11 +38,16 @@ # Set up file logging log_dir = "logs" os.makedirs(log_dir, exist_ok=True) -log_file = os.path.join(log_dir, f"migration_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log") +log_file = os.path.join( + log_dir, f"migration_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" +) file_handler = logging.FileHandler(log_file) -file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) +file_handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") +) logger.addHandler(file_handler) + def migrate_avatar_to_imageurl(): """ Migrate avatar field to imageUrl in users collection. @@ -65,17 +72,19 @@ def migrate_avatar_to_imageurl(): "users_with_avatar": 0, "users_with_both_fields": 0, "users_updated": 0, - "conflicts": 0 + "conflicts": 0, } for user in users_with_avatar: stats["users_with_avatar"] += 1 - + # Check for conflicts (users with both fields) if "imageUrl" in user and user["imageUrl"] is not None: if user["imageUrl"] != user["avatar"]: - logger.warning(f"Conflict found for user {user['_id']}: " - f"avatar='{user['avatar']}', imageUrl='{user['imageUrl']}'") + logger.warning( + f"Conflict found for user {user['_id']}: " + f"avatar='{user['avatar']}', imageUrl='{user['imageUrl']}'" + ) stats["conflicts"] += 1 continue stats["users_with_both_fields"] += 1 @@ -84,10 +93,8 @@ def migrate_avatar_to_imageurl(): users_to_update.append( UpdateOne( {"_id": user["_id"]}, - { - "$set": {"imageUrl": user["avatar"]}, - "$unset": {"avatar": ""} - } + {"$set": {"imageUrl": user["avatar"]}, "$unset": { + "avatar": ""}}, ) ) @@ -103,6 +110,7 @@ def migrate_avatar_to_imageurl(): logger.error(f"Migration failed: {str(e)}") raise + def rollback_migration(backup_path): """ Rollback the migration using a specified backup. @@ -112,12 +120,12 @@ def rollback_migration(backup_path): db = client[DATABASE_NAME] # Read users collection backup - with open(os.path.join(backup_path, "users.json"), 'r') as f: + with open(os.path.join(backup_path, "users.json"), "r") as f: users_backup = json.load(f) # Convert string IDs back to ObjectId for user in users_backup: - user['_id'] = ObjectId(user['_id']) + user["_id"] = ObjectId(user["_id"]) # Replace current users collection with backup db.users.drop() @@ -131,15 +139,16 @@ def rollback_migration(backup_path): logger.error(f"Rollback failed: {str(e)}") raise + if __name__ == "__main__": logger.info("Starting avatar to imageUrl migration...") stats = migrate_avatar_to_imageurl() - + logger.info("\nMigration completed. Statistics:") logger.info(f"Total users: {stats['total_users']}") logger.info(f"Users with avatar field: {stats['users_with_avatar']}") logger.info(f"Users with both fields: {stats['users_with_both_fields']}") logger.info(f"Users updated: {stats['users_updated']}") logger.info(f"Conflicts found: {stats['conflicts']}") - + print("\nMigration completed. Check the log file for details:", log_file) From 62433da0512a4a126a37404d5779a2579ae759bd Mon Sep 17 00:00:00 2001 From: Vraj Patel Date: Tue, 29 Jul 2025 00:51:53 +0530 Subject: [PATCH 4/5] fix(review): address coderabbit review comments --- backend/scripts/migrate_avatar_to_imageurl.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/scripts/migrate_avatar_to_imageurl.py b/backend/scripts/migrate_avatar_to_imageurl.py index 24d8df04..cc3737f6 100644 --- a/backend/scripts/migrate_avatar_to_imageurl.py +++ b/backend/scripts/migrate_avatar_to_imageurl.py @@ -47,6 +47,14 @@ ) logger.addHandler(file_handler) +# Validate required environment variables +if not MONGODB_URL: + logger.error("MONGODB_URL environment variable is required") + sys.exit(1) +if not DATABASE_NAME: + logger.error("DATABASE_NAME environment variable is required") + sys.exit(1) + def migrate_avatar_to_imageurl(): """ @@ -118,9 +126,13 @@ def rollback_migration(backup_path): try: client = MongoClient(MONGODB_URL) db = client[DATABASE_NAME] + + backup_file_path = os.path.join(backup_path, "users.json") + if not os.path.exists(backup_file_path): + raise FileNotFoundError(f"Backup file not found: {backup_file_path}") # Read users collection backup - with open(os.path.join(backup_path, "users.json"), "r") as f: + with open(backup_file_path, "r") as f: users_backup = json.load(f) # Convert string IDs back to ObjectId From 54124221c86a260716547d2ec8a37f7494c6ecd2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:22:21 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- backend/scripts/migrate_avatar_to_imageurl.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/scripts/migrate_avatar_to_imageurl.py b/backend/scripts/migrate_avatar_to_imageurl.py index cc3737f6..7dce76ad 100644 --- a/backend/scripts/migrate_avatar_to_imageurl.py +++ b/backend/scripts/migrate_avatar_to_imageurl.py @@ -7,13 +7,13 @@ 4. Logs migration statistics """ -from backup_db import create_backup import json import logging import os import sys from datetime import datetime +from backup_db import create_backup from bson import ObjectId from dotenv import load_dotenv from pymongo import MongoClient, UpdateOne @@ -126,10 +126,11 @@ def rollback_migration(backup_path): try: client = MongoClient(MONGODB_URL) db = client[DATABASE_NAME] - + backup_file_path = os.path.join(backup_path, "users.json") if not os.path.exists(backup_file_path): - raise FileNotFoundError(f"Backup file not found: {backup_file_path}") + raise FileNotFoundError( + f"Backup file not found: {backup_file_path}") # Read users collection backup with open(backup_file_path, "r") as f: