mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-21 12:39:25 +00:00
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:
committed by
Elisiário Couto
parent
0122913052
commit
22ec0e36b1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user