mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-30 13:48:07 +02:00
Basic Fallback for Validation, Parsing errors
This commit is contained in:
parent
5d524e8060
commit
065df720d1
@ -34,6 +34,14 @@ services:
|
||||
- ./config:/config
|
||||
- ./debug:/media/frigate
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
ports:
|
||||
- "8971:8971"
|
||||
- "5000:5000"
|
||||
- "5001:5001"
|
||||
- "5173:5173"
|
||||
- "8554:8554"
|
||||
- "8555:8555"
|
||||
|
||||
mqtt:
|
||||
container_name: mqtt
|
||||
image: eclipse-mosquitto:1.6
|
||||
|
@ -3,6 +3,7 @@ import os
|
||||
import sys
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.scanner import ScannerError
|
||||
|
||||
sys.path.insert(0, "/opt/frigate")
|
||||
from frigate.const import (
|
||||
@ -29,7 +30,7 @@ try:
|
||||
config: dict[str, any] = yaml.load(raw_config)
|
||||
elif config_file.endswith(".json"):
|
||||
config: dict[str, any] = json.loads(raw_config)
|
||||
except FileNotFoundError:
|
||||
except (FileNotFoundError, ScannerError):
|
||||
config: dict[str, any] = {}
|
||||
|
||||
path = config.get("ffmpeg", {}).get("path", "default")
|
||||
|
@ -6,6 +6,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.scanner import ScannerError
|
||||
|
||||
sys.path.insert(0, "/opt/frigate")
|
||||
from frigate.const import (
|
||||
@ -44,7 +45,7 @@ try:
|
||||
config: dict[str, any] = yaml.load(raw_config)
|
||||
elif config_file.endswith(".json"):
|
||||
config: dict[str, any] = json.loads(raw_config)
|
||||
except FileNotFoundError:
|
||||
except (FileNotFoundError, ScannerError):
|
||||
config: dict[str, any] = {}
|
||||
|
||||
go2rtc_config: dict[str, any] = config.get("go2rtc", {})
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.scanner import ScannerError
|
||||
|
||||
yaml = YAML()
|
||||
|
||||
@ -22,7 +22,7 @@ try:
|
||||
config: dict[str, any] = yaml.load(raw_config)
|
||||
elif config_file.endswith(".json"):
|
||||
config: dict[str, any] = json.loads(raw_config)
|
||||
except FileNotFoundError:
|
||||
except (FileNotFoundError, ScannerError):
|
||||
config: dict[str, any] = {}
|
||||
|
||||
tls_config: dict[str, any] = config.get("tls", {"enabled": True})
|
||||
|
@ -7,12 +7,28 @@ from typing import Union
|
||||
|
||||
import ruamel.yaml
|
||||
from pydantic import ValidationError
|
||||
from ruamel.yaml.scanner import ScannerError
|
||||
|
||||
from frigate.app import FrigateApp
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.log import setup_logging
|
||||
from frigate.util.config import find_config_file
|
||||
|
||||
minimal_config = {
|
||||
"mqtt": {"enabled": "false"},
|
||||
"environment_vars": {
|
||||
"INVALID_CONFIG": "true",
|
||||
},
|
||||
"cameras": {
|
||||
"null": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{"path": "/dev/null"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def main() -> None:
|
||||
faulthandler.enable()
|
||||
@ -47,53 +63,59 @@ def main() -> None:
|
||||
print("*** Config Validation Errors ***")
|
||||
print("*************************************************************\n")
|
||||
# Attempt to get the original config file for line number tracking
|
||||
config_path = find_config_file()
|
||||
with open(config_path, "r") as f:
|
||||
yaml_config = ruamel.yaml.YAML()
|
||||
yaml_config.preserve_quotes = True
|
||||
full_config = yaml_config.load(f)
|
||||
if e.__class__ == ValidationError:
|
||||
|
||||
for error in e.errors():
|
||||
error_path = error["loc"]
|
||||
config_path = find_config_file()
|
||||
with open(config_path, "r") as f:
|
||||
yaml_config = ruamel.yaml.YAML()
|
||||
yaml_config.preserve_quotes = True
|
||||
full_config = yaml_config.load(f)
|
||||
|
||||
current = full_config
|
||||
line_number = "Unknown"
|
||||
last_line_number = "Unknown"
|
||||
for error in e.errors():
|
||||
error_path = error["loc"]
|
||||
|
||||
try:
|
||||
for i, part in enumerate(error_path):
|
||||
key: Union[int, str] = (
|
||||
int(part) if isinstance(part, str) and part.isdigit() else part
|
||||
)
|
||||
current = full_config
|
||||
line_number = "Unknown"
|
||||
last_line_number = "Unknown"
|
||||
|
||||
if isinstance(current, ruamel.yaml.comments.CommentedMap):
|
||||
current = current[key]
|
||||
elif isinstance(current, list):
|
||||
if isinstance(key, int):
|
||||
try:
|
||||
for i, part in enumerate(error_path):
|
||||
key: Union[int, str] = (
|
||||
int(part) if isinstance(part, str) and part.isdigit() else part
|
||||
)
|
||||
|
||||
if isinstance(current, ruamel.yaml.comments.CommentedMap):
|
||||
current = current[key]
|
||||
elif isinstance(current, list):
|
||||
if isinstance(key, int):
|
||||
current = current[key]
|
||||
|
||||
if hasattr(current, "lc"):
|
||||
last_line_number = current.lc.line
|
||||
|
||||
if i == len(error_path) - 1:
|
||||
if hasattr(current, "lc"):
|
||||
line_number = current.lc.line
|
||||
else:
|
||||
line_number = last_line_number
|
||||
last_line_number = current.lc.line
|
||||
|
||||
except Exception as traverse_error:
|
||||
print(f"Could not determine exact line number: {traverse_error}")
|
||||
if i == len(error_path) - 1:
|
||||
if hasattr(current, "lc"):
|
||||
line_number = current.lc.line
|
||||
else:
|
||||
line_number = last_line_number
|
||||
|
||||
if current != full_config:
|
||||
print(f"Line # : {line_number}")
|
||||
print(f"Key : {' -> '.join(map(str, error_path))}")
|
||||
print(f"Value : {error.get('input', '-')}")
|
||||
print(f"Message : {error.get('msg', error.get('type', 'Unknown'))}\n")
|
||||
except Exception as traverse_error:
|
||||
print(f"Could not determine exact line number: {traverse_error}")
|
||||
|
||||
if current != full_config:
|
||||
print(f"Line # : {line_number}")
|
||||
print(f"Key : {' -> '.join(map(str, error_path))}")
|
||||
print(f"Value : {error.get('input', '-')}")
|
||||
print(f"Message : {error.get('msg', error.get('type', 'Unknown'))}\n")
|
||||
else:
|
||||
print(f"Failed to parse config: {e}")
|
||||
|
||||
print("*************************************************************")
|
||||
print("*** End Config Validation Errors ***")
|
||||
print("*************************************************************")
|
||||
sys.exit(1)
|
||||
|
||||
FrigateApp(FrigateConfig(**minimal_config)).start_config_editor()
|
||||
sys.exit(0)
|
||||
if args.validate_config:
|
||||
print("*************************************************************")
|
||||
print("*** Your config file is valid. ***")
|
||||
|
@ -130,3 +130,42 @@ def create_fastapi_app(
|
||||
app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
|
||||
def create_config_editor_app(
|
||||
frigate_config: FrigateConfig,
|
||||
database: SqliteQueueDatabase,
|
||||
) -> FastAPI:
|
||||
|
||||
app = FastAPI(
|
||||
debug=True,
|
||||
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
middleware.ContextMiddleware,
|
||||
plugins=(plugins.ForwardedForPlugin(),),
|
||||
)
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
logger.info("FastAPI started")
|
||||
|
||||
# Rate limiter (used for login endpoint)
|
||||
if frigate_config.auth.failed_login_rate_limit is None:
|
||||
limiter.enabled = False
|
||||
else:
|
||||
auth.rateLimiter.set_limit(frigate_config.auth.failed_login_rate_limit)
|
||||
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
|
||||
# Routes
|
||||
app.include_router(auth.router)
|
||||
app.include_router(main_app.router)
|
||||
app.frigate_config = frigate_config
|
||||
app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
|
||||
|
||||
return app
|
||||
|
@ -15,7 +15,7 @@ from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
|
||||
import frigate.util as util
|
||||
from frigate.api.auth import hash_password
|
||||
from frigate.api.fastapi_app import create_fastapi_app
|
||||
from frigate.api.fastapi_app import create_config_editor_app, create_fastapi_app
|
||||
from frigate.camera import CameraMetrics, PTZMetrics
|
||||
from frigate.comms.base_communicator import Communicator
|
||||
from frigate.comms.config_updater import ConfigPublisher
|
||||
@ -624,6 +624,37 @@ class FrigateApp:
|
||||
logger.info("********************************************************")
|
||||
logger.info("********************************************************")
|
||||
|
||||
|
||||
def start_config_editor(self) -> None:
|
||||
logger.info(f"Starting Frigate in Config Editor Mode ({VERSION})")
|
||||
self.ensure_dirs()
|
||||
self.init_database()
|
||||
self.bind_database()
|
||||
self.init_auth()
|
||||
|
||||
def stop_config_editor() -> None:
|
||||
logger.info("Stopping...")
|
||||
os._exit(os.EX_OK)
|
||||
|
||||
try:
|
||||
uvicorn.run(
|
||||
create_config_editor_app(
|
||||
self.config,
|
||||
self.db,
|
||||
),
|
||||
host="127.0.0.1",
|
||||
port=5001,
|
||||
log_level="error",
|
||||
)
|
||||
finally:
|
||||
stop_config_editor()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def start(self) -> None:
|
||||
logger.info(f"Starting Frigate ({VERSION})")
|
||||
|
||||
|
@ -7,6 +7,7 @@ import shutil
|
||||
from typing import Optional, Union
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.scanner import ScannerError
|
||||
|
||||
from frigate.const import CONFIG_DIR, EXPORT_DIR
|
||||
from frigate.util.services import get_video_properties
|
||||
@ -37,7 +38,11 @@ def migrate_frigate_config(config_file: str):
|
||||
yaml = YAML()
|
||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||
with open(config_file, "r") as f:
|
||||
config: dict[str, dict[str, any]] = yaml.load(f)
|
||||
try:
|
||||
config: dict[str, dict[str, any]] = yaml.load(f)
|
||||
except ScannerError:
|
||||
logger.debug(f"Failed to parse config at {config_file}")
|
||||
return
|
||||
|
||||
if config is None:
|
||||
logger.error(f"Failed to load config at {config_file}")
|
||||
|
@ -46,6 +46,17 @@ function App() {
|
||||
>
|
||||
<Suspense>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<ProtectedRoute
|
||||
requiredRoles={["admin"]}
|
||||
configGuard={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Route path="/config" element={<ConfigEditor />} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={["viewer", "admin"]} />
|
||||
@ -61,7 +72,6 @@ function App() {
|
||||
element={<ProtectedRoute requiredRoles={["admin"]} />}
|
||||
>
|
||||
<Route path="/system" element={<System />} />
|
||||
<Route path="/config" element={<ConfigEditor />} />
|
||||
<Route path="/logs" element={<Logs />} />
|
||||
<Route path="/faces" element={<FaceLibrary />} />
|
||||
<Route path="/playground" element={<UIPlayground />} />
|
||||
|
@ -9,6 +9,7 @@ import { FaCheck } from "react-icons/fa";
|
||||
import { IoIosWarning } from "react-icons/io";
|
||||
import { MdCircle } from "react-icons/md";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useConfigValidator } from "@/hooks/use-config-validator"; // <-- new import
|
||||
|
||||
export default function Statusbar() {
|
||||
const { messages, addMessage, clearMessages } = useContext(
|
||||
@ -28,6 +29,7 @@ export default function Statusbar() {
|
||||
}, [stats]);
|
||||
|
||||
const { potentialProblems } = useStats(stats);
|
||||
const invalidConfig = useConfigValidator(); // <-- use the hook
|
||||
|
||||
useEffect(() => {
|
||||
clearMessages("stats");
|
||||
@ -40,7 +42,10 @@ export default function Statusbar() {
|
||||
problem.relevantLink,
|
||||
);
|
||||
});
|
||||
}, [potentialProblems, addMessage, clearMessages]);
|
||||
if (invalidConfig) {
|
||||
addMessage("stats", "Invalid config - fix config only", "text-danger");
|
||||
}
|
||||
}, [potentialProblems, invalidConfig, addMessage, clearMessages]);
|
||||
|
||||
const { payload: reindexState } = useEmbeddingsReindexProgress();
|
||||
|
||||
@ -60,7 +65,11 @@ export default function Statusbar() {
|
||||
}, [reindexState, addMessage, clearMessages]);
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-0 left-0 right-0 z-10 flex h-8 w-full items-center justify-between border-t border-secondary-highlight bg-background_alt px-4 dark:text-secondary-foreground">
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 z-10 flex h-8 w-full items-center justify-between border-t border-secondary-highlight ${
|
||||
invalidConfig ? "bg-red-500 bg-opacity-75" : "bg-background_alt"
|
||||
} px-4 dark:text-secondary-foreground`}
|
||||
>
|
||||
<div className="flex h-full items-center gap-2">
|
||||
{cpuPercent && (
|
||||
<Link to="/system#general">
|
||||
|
@ -2,13 +2,19 @@ import { useContext } from "react";
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { AuthContext } from "@/context/auth-context";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { useConfigValidator } from "@/hooks/use-config-validator";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
requiredRoles: ("admin" | "viewer")[];
|
||||
configGuard?: boolean;
|
||||
}
|
||||
|
||||
export default function ProtectedRoute({
|
||||
requiredRoles,
|
||||
}: {
|
||||
requiredRoles: ("admin" | "viewer")[];
|
||||
}) {
|
||||
configGuard = true,
|
||||
}: ProtectedRouteProps) {
|
||||
const { auth } = useContext(AuthContext);
|
||||
const invalidConfig = useConfigValidator(); // Use the hook to check config validity
|
||||
|
||||
if (auth.isLoading) {
|
||||
return (
|
||||
@ -16,18 +22,14 @@ export default function ProtectedRoute({
|
||||
);
|
||||
}
|
||||
|
||||
// Unauthenticated mode
|
||||
if (!auth.isAuthenticated) {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
// Authenticated mode (8971): require login
|
||||
if (!auth.user) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
// If role is null (shouldn’t happen if isAuthenticated, but type safety), fallback
|
||||
// though isAuthenticated should catch this
|
||||
if (auth.user.role === null) {
|
||||
return <Outlet />;
|
||||
}
|
||||
@ -36,5 +38,9 @@ export default function ProtectedRoute({
|
||||
return <Navigate to="/unauthorized" replace />;
|
||||
}
|
||||
|
||||
if (configGuard && invalidConfig) {
|
||||
return <Navigate to="/config" replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
|
11
web/src/components/settings/ConfigProtection.tsx
Normal file
11
web/src/components/settings/ConfigProtection.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { useConfigValidator } from "@/hooks/use-config-validator";
|
||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
|
||||
export default function ConfigProtection() {
|
||||
const invalidConfig = useConfigValidator();
|
||||
const location = useLocation();
|
||||
if (invalidConfig && location.pathname !== "/config") {
|
||||
return <Navigate to="/config" replace />;
|
||||
}
|
||||
return <Outlet />;
|
||||
}
|
13
web/src/hooks/use-config-validator.ts
Normal file
13
web/src/hooks/use-config-validator.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
|
||||
export function useConfigValidator() {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const invalidConfig = useMemo(() => {
|
||||
return config?.environment_vars?.INVALID_CONFIG;
|
||||
}, [config]);
|
||||
|
||||
return invalidConfig;
|
||||
}
|
Loading…
Reference in New Issue
Block a user