mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 06:45:09 +00:00
fix(core): font zip cache, monotonic timers, resolver back-off, and other core/common fixes (#654)
* fix(core): font zip cache, monotonic timers, resolver back-off, and other core/common fixes - font_manager: a .zip font URL is served as its extracted font after a restart (the cached-file check returned the archive first); downloads use requests with a 30s timeout into a temp file + os.replace. - api_helper / sync_manager: rate-limit and heartbeat/leader timeouts use time.monotonic(); last_request_time and the status file's ts stay wall-clock. set_on_new_cycle docstring no longer claims core uses it. - logo_helper: the placeholder uses the same scaled box as a real logo. - permission_utils: one _sudo_bash_candidates() helper (with the sudoers exact-argv rationale) shared by sudo_remove_directory, which now retries the next bash path on a sudo refusal, and install_requirements_file. - dynamic_team_resolver: failed/empty fetch backs off 5 min; duplicate INFO log and contradictory docstring example fixed. - element_style: scale default looked up through element aliases. - background_data_service: cache-hit callback runs outside the lock. - config_arrays: union-aware type check (["array","null"]); stale dotToNested() reference removed. - auto_update_setup: non-dict auto_update reads as off; temp result file unlinked when the write fails. - exceptions: constructors copy the caller's context dict. - logging_config: StructuredFormatter json.dumps(default=str). - error_aggregator: removed unused export_path/export_to_file/_auto_export. - Docstrings: validate_file_upload max_size_mb, raise_on_errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): retry the status-file rename like the other atomic writers On Windows os.replace can fail with "Access is denied" while a scanner briefly holds the target open; config_manager_atomic._replace already retries that (and re-raises at once on other platforms). The sync status writer called os.replace directly, which made test_concurrent_writers_each_use_their_own_temp_file flaky on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,29 @@
|
||||
|
||||
A list reaches a plugin-config save keyed by position more often than as a
|
||||
list. The settings form posts one field per element (``feeds.custom_feeds.0.name``),
|
||||
which ``_set_nested_value`` stores as ``{"0": {"name": ...}}``, and the JSON
|
||||
path's dotToNested() in the browser builds the same dict. Validation expects
|
||||
an array there, so the save converts them first.
|
||||
which ``_set_nested_value`` stores as ``{"0": {"name": ...}}``, and a JSON
|
||||
save built by flattening then re-nesting dotted keys carries the same dict.
|
||||
Validation expects an array there, so the save converts them first.
|
||||
"""
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def _schema_type_is(prop: Any, wanted: str) -> bool:
|
||||
"""Whether a schema property is of ``wanted`` type, unions included.
|
||||
|
||||
Mirrors ``_schema_type_is`` in ``web_interface/blueprints/api_v3`` (kept
|
||||
here so src/ doesn't import the Flask blueprint). A union such as
|
||||
``["array", "null"]`` -- the per-element style overrides, where null means
|
||||
"inherit" -- is still an array for recombining position-keyed inputs.
|
||||
"""
|
||||
if not isinstance(prop, dict):
|
||||
return False
|
||||
declared = prop.get('type')
|
||||
if isinstance(declared, list):
|
||||
return wanted in declared
|
||||
return declared == wanted
|
||||
|
||||
|
||||
def _is_index_dict(value: Any) -> bool:
|
||||
"""True for a dict keyed only by list positions ("0", "1", ...), or empty."""
|
||||
return isinstance(value, dict) and all(str(k).isdigit() for k in value)
|
||||
@@ -35,10 +51,9 @@ def coerce_array_shapes(config: Dict[str, Any], schema_props: Dict[str, Any],
|
||||
for key, prop_schema in schema_props.items():
|
||||
if key not in config or not isinstance(prop_schema, dict):
|
||||
continue
|
||||
prop_type = prop_schema.get('type')
|
||||
value = config[key]
|
||||
|
||||
if prop_type == 'array':
|
||||
if _schema_type_is(prop_schema, 'array'):
|
||||
if _is_index_dict(value):
|
||||
value = config[key] = [value[k] for k in sorted(value, key=lambda k: int(str(k)))]
|
||||
if not isinstance(value, list):
|
||||
@@ -50,11 +65,11 @@ def coerce_array_shapes(config: Dict[str, Any], schema_props: Dict[str, Any],
|
||||
and isinstance(default, list) and len(default) >= min_items):
|
||||
value = config[key] = list(default)
|
||||
items_schema = prop_schema.get('items')
|
||||
if (isinstance(items_schema, dict) and items_schema.get('type') == 'object'
|
||||
if (_schema_type_is(items_schema, 'object')
|
||||
and 'properties' in items_schema):
|
||||
for element in value:
|
||||
coerce_array_shapes(element, items_schema['properties'],
|
||||
short_lists_take_default)
|
||||
|
||||
elif prop_type == 'object' and 'properties' in prop_schema:
|
||||
elif _schema_type_is(prop_schema, 'object') and 'properties' in prop_schema:
|
||||
coerce_array_shapes(value, prop_schema['properties'], short_lists_take_default)
|
||||
|
||||
@@ -8,11 +8,13 @@ from pathlib import Path
|
||||
def validate_file_upload(filename: str, max_size_mb: int = 10,
|
||||
allowed_extensions: Optional[List[str]] = None) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate file upload parameters.
|
||||
Validate an upload's filename (not its contents or size).
|
||||
|
||||
Args:
|
||||
filename: Name of the file
|
||||
max_size_mb: Maximum file size in MB
|
||||
max_size_mb: Unused. Kept so existing callers keep working; this
|
||||
function only sees the filename, so callers must enforce the
|
||||
size limit themselves on the uploaded stream.
|
||||
allowed_extensions: List of allowed file extensions (e.g., ['.ttf', '.otf'])
|
||||
|
||||
Returns:
|
||||
|
||||
Reference in New Issue
Block a user