fix(api): Fix S3 backup path-style configuration and improve UX.

- Fix critical S3 client configuration bug for path-style addressing
- Add toast notifications for better user feedback on S3 config operations
- Set up Toaster component in root layout for app-wide notifications
- Clean up unused imports in test files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Elisiário Couto
2025-09-28 22:55:15 +01:00
committed by Elisiário Couto
parent 0122913052
commit 22ec0e36b1
7 changed files with 150 additions and 126 deletions

View File

@@ -38,35 +38,40 @@ class BackupService:
s3_kwargs["endpoint_url"] = current_config.endpoint_url
if current_config.path_style:
s3_kwargs["config"] = boto3.client("s3").meta.config
s3_kwargs["config"].s3 = {"addressing_style": "path"}
from botocore.config import Config
s3_kwargs["config"] = Config(s3={"addressing_style": "path"})
return session.client("s3", **s3_kwargs)
async def test_connection(self, config: S3BackupConfig) -> bool:
"""Test S3 connection with provided configuration.
Args:
config: S3 configuration to test
Returns:
True if connection successful, False otherwise
"""
try:
s3_client = self._get_s3_client(config)
# Try to list objects in the bucket (limited to 1 to minimize cost)
s3_client.list_objects_v2(Bucket=config.bucket_name, MaxKeys=1)
logger.info(f"S3 connection test successful for bucket: {config.bucket_name}")
logger.info(
f"S3 connection test successful for bucket: {config.bucket_name}"
)
return True
except NoCredentialsError:
logger.error("S3 credentials not found or invalid")
return False
except ClientError as e:
error_code = e.response["Error"]["Code"]
logger.error(f"S3 connection test failed: {error_code} - {e.response['Error']['Message']}")
logger.error(
f"S3 connection test failed: {error_code} - {e.response['Error']['Message']}"
)
return False
except Exception as e:
logger.error(f"Unexpected error during S3 connection test: {str(e)}")
@@ -74,10 +79,10 @@ class BackupService:
async def backup_database(self, database_path: Path) -> bool:
"""Backup database file to S3.
Args:
database_path: Path to the SQLite database file
Returns:
True if backup successful, False otherwise
"""
@@ -91,29 +96,27 @@ class BackupService:
try:
s3_client = self._get_s3_client()
# Generate backup filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_key = f"leggen_backups/database_backup_{timestamp}.db"
# Upload database file
logger.info(f"Starting database backup to S3: {backup_key}")
s3_client.upload_file(
str(database_path),
self.s3_config.bucket_name,
backup_key
str(database_path), self.s3_config.bucket_name, backup_key
)
logger.info(f"Database backup completed successfully: {backup_key}")
return True
except Exception as e:
logger.error(f"Database backup failed: {str(e)}")
return False
async def list_backups(self) -> list[dict]:
"""List available backups in S3.
Returns:
List of backup metadata dictionaries
"""
@@ -123,37 +126,38 @@ class BackupService:
try:
s3_client = self._get_s3_client()
# List objects with backup prefix
response = s3_client.list_objects_v2(
Bucket=self.s3_config.bucket_name,
Prefix="leggen_backups/"
Bucket=self.s3_config.bucket_name, Prefix="leggen_backups/"
)
backups = []
for obj in response.get("Contents", []):
backups.append({
"key": obj["Key"],
"last_modified": obj["LastModified"].isoformat(),
"size": obj["Size"],
})
backups.append(
{
"key": obj["Key"],
"last_modified": obj["LastModified"].isoformat(),
"size": obj["Size"],
}
)
# Sort by last modified (newest first)
backups.sort(key=lambda x: x["last_modified"], reverse=True)
return backups
except Exception as e:
logger.error(f"Failed to list backups: {str(e)}")
return []
async def restore_database(self, backup_key: str, restore_path: Path) -> bool:
"""Restore database from S3 backup.
Args:
backup_key: S3 key of the backup to restore
restore_path: Path where to restore the database
Returns:
True if restore successful, False otherwise
"""
@@ -163,28 +167,26 @@ class BackupService:
try:
s3_client = self._get_s3_client()
# Download backup file
logger.info(f"Starting database restore from S3: {backup_key}")
# Create parent directory if it doesn't exist
restore_path.parent.mkdir(parents=True, exist_ok=True)
# Download to temporary file first, then move to final location
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
s3_client.download_file(
self.s3_config.bucket_name,
backup_key,
temp_file.name
self.s3_config.bucket_name, backup_key, temp_file.name
)
# Move temp file to final location
temp_path = Path(temp_file.name)
temp_path.replace(restore_path)
logger.info(f"Database restore completed successfully: {restore_path}")
return True
except Exception as e:
logger.error(f"Database restore failed: {str(e)}")
return False
return False