mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 11:22:21 +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
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Cloud, TestTube } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { apiClient } from "../lib/api";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
@@ -55,9 +56,12 @@ export default function S3BackupConfigDrawer({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["backupSettings"] });
|
||||
setOpen(false);
|
||||
toast.success("S3 backup configuration saved successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (error: any) => {
|
||||
console.error("Failed to update S3 backup configuration:", error);
|
||||
const message = error?.response?.data?.detail || "Failed to save S3 configuration. Please check your settings and try again.";
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,9 +73,12 @@ export default function S3BackupConfigDrawer({
|
||||
}),
|
||||
onSuccess: () => {
|
||||
console.log("S3 connection test successful");
|
||||
toast.success("S3 connection test successful! Your configuration is working correctly.");
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (error: any) => {
|
||||
console.error("Failed to test S3 connection:", error);
|
||||
const message = error?.response?.data?.detail || "S3 connection test failed. Please verify your credentials and settings.";
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PWAInstallPrompt, PWAUpdatePrompt } from "../components/PWAPrompts";
|
||||
import { usePWA } from "../hooks/usePWA";
|
||||
import { useVersionCheck } from "../hooks/useVersionCheck";
|
||||
import { SidebarInset, SidebarProvider } from "../components/ui/sidebar";
|
||||
import { Toaster } from "../components/ui/sonner";
|
||||
|
||||
function RootLayout() {
|
||||
const { updateAvailable, updateSW, forceReload } = usePWA();
|
||||
@@ -48,6 +49,9 @@ function RootLayout() {
|
||||
updateAvailable={updateAvailable}
|
||||
onUpdate={handlePWAUpdate}
|
||||
/>
|
||||
|
||||
{/* Toast Notifications */}
|
||||
<Toaster />
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from loguru import logger
|
||||
|
||||
from leggen.api.routes import accounts, banks, backup, notifications, sync, transactions
|
||||
from leggen.api.routes import accounts, backup, banks, notifications, sync, transactions
|
||||
from leggen.background.scheduler import scheduler
|
||||
from leggen.utils.config import config
|
||||
from leggen.utils.paths import path_manager
|
||||
|
||||
@@ -38,8 +38,9 @@ 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)
|
||||
|
||||
@@ -58,7 +59,9 @@ class BackupService:
|
||||
# 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:
|
||||
@@ -66,7 +69,9 @@ class BackupService:
|
||||
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)}")
|
||||
@@ -99,9 +104,7 @@ class BackupService:
|
||||
# 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}")
|
||||
@@ -126,17 +129,18 @@ class BackupService:
|
||||
|
||||
# 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)
|
||||
@@ -173,9 +177,7 @@ class BackupService:
|
||||
# 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
|
||||
|
||||
@@ -89,5 +89,5 @@ markers = [
|
||||
]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["apscheduler.*", "discord_webhook.*"]
|
||||
module = ["apscheduler.*", "discord_webhook.*", "botocore.*", "boto3.*"]
|
||||
ignore_missing_imports = true
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"""Tests for backup API endpoints."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from leggen.api.models.backup import BackupSettings, S3Config
|
||||
from leggen.models.config import S3BackupConfig
|
||||
|
||||
|
||||
@pytest.mark.api
|
||||
class TestBackupAPI:
|
||||
@@ -56,7 +53,9 @@ class TestBackupAPI:
|
||||
assert s3_config["enabled"] is True
|
||||
|
||||
@patch("leggen.services.backup_service.BackupService.test_connection")
|
||||
def test_update_backup_settings_success(self, mock_test_connection, api_client, mock_config):
|
||||
def test_update_backup_settings_success(
|
||||
self, mock_test_connection, api_client, mock_config
|
||||
):
|
||||
"""Test successful backup settings update."""
|
||||
mock_test_connection.return_value = True
|
||||
mock_config._config["backup"] = {}
|
||||
@@ -85,7 +84,9 @@ class TestBackupAPI:
|
||||
mock_test_connection.assert_called_once()
|
||||
|
||||
@patch("leggen.services.backup_service.BackupService.test_connection")
|
||||
def test_update_backup_settings_connection_failure(self, mock_test_connection, api_client, mock_config):
|
||||
def test_update_backup_settings_connection_failure(
|
||||
self, mock_test_connection, api_client, mock_config
|
||||
):
|
||||
"""Test backup settings update with connection test failure."""
|
||||
mock_test_connection.return_value = False
|
||||
mock_config._config["backup"] = {}
|
||||
@@ -124,7 +125,7 @@ class TestBackupAPI:
|
||||
"endpoint_url": None,
|
||||
"path_style": False,
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
response = api_client.post("/api/v1/backup/test", json=request_data)
|
||||
@@ -152,7 +153,7 @@ class TestBackupAPI:
|
||||
"endpoint_url": None,
|
||||
"path_style": False,
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
response = api_client.post("/api/v1/backup/test", json=request_data)
|
||||
@@ -173,7 +174,7 @@ class TestBackupAPI:
|
||||
"endpoint_url": None,
|
||||
"path_style": False,
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
response = api_client.post("/api/v1/backup/test", json=request_data)
|
||||
@@ -215,7 +216,10 @@ class TestBackupAPI:
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 2
|
||||
assert data["data"][0]["key"] == "leggen_backups/database_backup_20250101_120000.db"
|
||||
assert (
|
||||
data["data"][0]["key"]
|
||||
== "leggen_backups/database_backup_20250101_120000.db"
|
||||
)
|
||||
|
||||
def test_list_backups_no_config(self, api_client, mock_config):
|
||||
"""Test backup listing with no configuration."""
|
||||
@@ -231,7 +235,9 @@ class TestBackupAPI:
|
||||
|
||||
@patch("leggen.services.backup_service.BackupService.backup_database")
|
||||
@patch("leggen.utils.paths.path_manager.get_database_path")
|
||||
def test_backup_operation_success(self, mock_get_db_path, mock_backup_db, api_client, mock_config):
|
||||
def test_backup_operation_success(
|
||||
self, mock_get_db_path, mock_backup_db, api_client, mock_config
|
||||
):
|
||||
"""Test successful backup operation."""
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError, NoCredentialsError
|
||||
@@ -97,8 +97,12 @@ class TestBackupService:
|
||||
with patch("boto3.Session") as mock_session:
|
||||
mock_client = MagicMock()
|
||||
mock_session.return_value.client.return_value = mock_client
|
||||
error_response = {"Error": {"Code": "NoSuchBucket", "Message": "Bucket not found"}}
|
||||
mock_client.list_objects_v2.side_effect = ClientError(error_response, "ListObjectsV2")
|
||||
error_response = {
|
||||
"Error": {"Code": "NoSuchBucket", "Message": "Bucket not found"}
|
||||
}
|
||||
mock_client.list_objects_v2.side_effect = ClientError(
|
||||
error_response, "ListObjectsV2"
|
||||
)
|
||||
|
||||
result = await service.test_connection(s3_config)
|
||||
assert result is False
|
||||
@@ -196,6 +200,7 @@ class TestBackupService:
|
||||
mock_session.return_value.client.return_value = mock_client
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
mock_response = {
|
||||
"Contents": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user