Skip to content

operations

appimage_updater.config.operations

Configuration operations for AppImage updater.

console = Console(no_color=(bool(os.environ.get('NO_COLOR')))) module-attribute

apply_basic_config_updates(app, updates)

Apply basic configuration updates (URL, directory, pattern, status).

Source code in src/appimage_updater/config/operations.py
def apply_basic_config_updates(app: Any, updates: dict[str, Any]) -> list[str]:
    """Apply basic configuration updates (URL, directory, pattern, status)."""
    changes = []
    changes.extend(_apply_simple_string_updates(app, updates))
    changes.extend(_apply_directory_updates(app, updates))
    changes.extend(_apply_boolean_field_updates(app, updates))
    changes.extend(_apply_source_type_updates(app, updates))
    return changes

apply_checksum_updates(app, updates)

Apply checksum-related updates.

Source code in src/appimage_updater/config/operations.py
def apply_checksum_updates(app: Any, updates: dict[str, Any]) -> list[str]:
    """Apply checksum-related updates."""
    changes: list[str] = []

    _apply_checksum_enabled_update(app, updates, changes)
    _apply_checksum_algorithm_update(app, updates, changes)
    _apply_checksum_pattern_update(app, updates, changes)
    _apply_checksum_required_update(app, updates, changes)

    return changes

apply_configuration_updates(app, updates)

Apply the updates to the application configuration object.

Returns:

Type Description
list[str]

List of change descriptions for display.

Source code in src/appimage_updater/config/operations.py
def apply_configuration_updates(app: Any, updates: dict[str, Any]) -> list[str]:
    """Apply the updates to the application configuration object.

    Returns:
        List of change descriptions for display.
    """
    # Apply different categories of updates
    changes = []
    changes.extend(apply_basic_config_updates(app, updates))
    changes.extend(apply_rotation_updates(app, updates))
    changes.extend(apply_checksum_updates(app, updates))

    return changes

apply_rotation_updates(app, updates)

Apply rotation-related updates.

Source code in src/appimage_updater/config/operations.py
def apply_rotation_updates(app: Any, updates: dict[str, Any]) -> list[str]:
    """Apply rotation-related updates."""
    changes: list[str] = []

    _apply_rotation_enabled_update(app, updates, changes)
    _apply_symlink_path_update(app, updates, changes)
    _apply_retain_count_update(app, updates, changes)

    return changes

collect_checksum_edit_updates(checksum, checksum_algorithm, checksum_pattern, checksum_required)

Collect checksum-related configuration updates.

Source code in src/appimage_updater/config/operations.py
def collect_checksum_edit_updates(
    checksum: bool | None,
    checksum_algorithm: str | None,
    checksum_pattern: str | None,
    checksum_required: bool | None,
) -> dict[str, Any]:
    """Collect checksum-related configuration updates."""
    updates: dict[str, Any] = {}

    if checksum is not None:
        updates["checksum_enabled"] = checksum
    if checksum_algorithm is not None:
        updates["checksum_algorithm"] = checksum_algorithm
    if checksum_pattern is not None:
        updates["checksum_pattern"] = checksum_pattern
    if checksum_required is not None:
        updates["checksum_required"] = checksum_required

    return updates

collect_edit_updates(url, download_dir, basename, pattern, enable, prerelease, rotation, symlink_path, retain_count, checksum, checksum_algorithm, checksum_pattern, checksum_required, force=False, direct=None, auto_subdir=None, app=None)

Collect all configuration updates for edit command.

Source code in src/appimage_updater/config/operations.py
def collect_edit_updates(
    url: str | None,
    download_dir: str | None,
    basename: str | None,
    pattern: str | None,
    enable: bool | None,
    prerelease: bool | None,
    rotation: bool | None,
    symlink_path: str | None,
    retain_count: int | None,
    checksum: bool | None,
    checksum_algorithm: str | None,
    checksum_pattern: str | None,
    checksum_required: bool | None,
    force: bool = False,
    direct: bool | None = None,
    auto_subdir: bool | None = None,
    app: Any = None,
) -> dict[str, Any]:
    """Collect all configuration updates for edit command."""
    updates: dict[str, Any] = {}

    # Collect basic updates
    if url is not None:
        _add_url_update(updates, url, force)

    _add_basic_field_updates(updates, download_dir, basename, pattern, enable, prerelease)

    if direct is not None:
        _add_source_type_update(updates, direct, app)

    if auto_subdir is not None:
        updates["auto_subdir"] = auto_subdir

    # Collect rotation updates
    updates.update(collect_rotation_edit_updates(rotation, symlink_path, retain_count))

    # Collect checksum updates
    updates.update(collect_checksum_edit_updates(checksum, checksum_algorithm, checksum_pattern, checksum_required))

    return updates

collect_rotation_edit_updates(rotation, symlink_path, retain_count)

Collect rotation-related configuration updates.

Source code in src/appimage_updater/config/operations.py
def collect_rotation_edit_updates(
    rotation: bool | None,
    symlink_path: str | None,
    retain_count: int | None,
) -> dict[str, Any]:
    """Collect rotation-related configuration updates."""
    updates: dict[str, Any] = {}

    if rotation is not None:
        updates["rotation_enabled"] = rotation
    if symlink_path is not None:
        updates["symlink_path"] = symlink_path
    if retain_count is not None:
        updates["retain_count"] = retain_count

    return updates

Expand and make symlink path absolute if needed.

Source code in src/appimage_updater/config/operations.py
def expand_symlink_path(symlink_path: str) -> Path:
    """Expand and make symlink path absolute if needed."""
    try:
        expanded_path = Path(symlink_path).expanduser()
    except (ValueError, OSError) as e:
        raise ValueError(f"Invalid symlink path '{symlink_path}': {e}") from e

    # Make it absolute if it's a relative path without explicit relative indicators
    if not expanded_path.is_absolute() and not str(expanded_path).startswith(("./", "../", "~")):
        expanded_path = Path.cwd() / expanded_path

    return expanded_path

generate_default_config(name, url, download_dir=None, rotation=None, retain=None, symlink=None, prerelease=None, checksum=None, checksum_algorithm=None, checksum_pattern=None, checksum_required=None, pattern=None, direct=None, global_config=None) async

Generate a default application configuration.

Returns:

Name Type Description
tuple tuple[dict[str, Any], bool]

(config_dict, prerelease_auto_enabled)

Source code in src/appimage_updater/config/operations.py
async def generate_default_config(
    name: str,
    url: str,
    download_dir: str | None = None,
    rotation: bool | None = None,
    retain: int | None = None,
    symlink: str | None = None,
    prerelease: bool | None = None,
    checksum: bool | None = None,
    checksum_algorithm: str | None = None,
    checksum_pattern: str | None = None,
    checksum_required: bool | None = None,
    pattern: str | None = None,
    direct: bool | None = None,
    global_config: Any = None,
) -> tuple[dict[str, Any], bool]:
    """Generate a default application configuration.

    Returns:
        tuple: (config_dict, prerelease_auto_enabled)
    """
    defaults = global_config.defaults if global_config else None

    # Apply global defaults for basic settings
    download_dir = _get_effective_download_dir(download_dir, defaults, name)
    checksum_config = _get_effective_checksum_config(
        checksum, checksum_algorithm, checksum_pattern, checksum_required, defaults
    )
    prerelease_final, prerelease_auto_enabled = await _get_effective_prerelease_config(prerelease, defaults, url)

    # Generate pattern with fallback
    final_pattern = await _get_effective_pattern(pattern, name, url)

    config = {
        "name": name,
        "source_type": "direct" if direct is True else detect_source_type(url),
        "url": url,
        "download_dir": download_dir,
        "pattern": final_pattern,
        "enabled": True,
        "prerelease": prerelease_final,
        "checksum": checksum_config,
    }

    # Apply rotation settings
    _apply_rotation_config(config, rotation, retain, symlink, defaults, name)

    # Apply symlink path independently of rotation
    _apply_symlink_config(config, symlink, defaults, name)

    return config, prerelease_auto_enabled

handle_add_directory_creation(download_dir, create_dir, yes=False, no=False)

Handle download directory path expansion and creation for add command.

Source code in src/appimage_updater/config/operations.py
def handle_add_directory_creation(
    download_dir: str, create_dir: bool | None, yes: bool = False, no: bool = False
) -> str:
    """Handle download directory path expansion and creation for add command."""
    expanded_download_dir = str(Path(download_dir).expanduser())
    download_path = Path(expanded_download_dir)

    if download_path.exists():
        return expanded_download_dir

    _handle_missing_directory(download_path, create_dir, yes, no)
    return expanded_download_dir

handle_directory_creation(updates, create_dir, yes=False)

Handle download directory creation if needed.

Source code in src/appimage_updater/config/operations.py
def handle_directory_creation(updates: dict[str, Any], create_dir: bool, yes: bool = False) -> None:
    """Handle download directory creation if needed."""
    if "download_dir" not in updates:
        return

    download_dir = updates["download_dir"]
    expanded_path = _get_expanded_download_path(download_dir)

    if not expanded_path.exists():
        should_create = _should_create_directory(create_dir, yes, expanded_path)
        _create_directory_if_needed(expanded_path, should_create)

    # Update with expanded path
    updates["download_dir"] = str(expanded_path)

handle_path_expansions(updates)

Handle path expansion for download directory.

Source code in src/appimage_updater/config/operations.py
def handle_path_expansions(updates: dict[str, Any]) -> None:
    """Handle path expansion for download directory."""
    if "download_dir" in updates:
        updates["download_dir"] = str(Path(updates["download_dir"]).expanduser())

Normalize path and validate parent directory and extension.

Source code in src/appimage_updater/config/operations.py
def normalize_and_validate_symlink_path(expanded_path: Path, original_path: str) -> Path:
    """Normalize path and validate parent directory and extension."""
    # Normalize path to remove redundant separators and resolve .. but don't follow symlinks
    # We need to handle the case where the symlink itself might exist, but we want to validate
    # the intended path, not the target it points to
    normalized_path = _normalize_symlink_path(expanded_path, original_path)
    _validate_symlink_parent_directory(normalized_path, original_path)
    _validate_symlink_extension(normalized_path, original_path)

    return normalized_path

validate_add_rotation_config(rotation, symlink)

Validate rotation and symlink combination for add command.

Returns:

Type Description
bool

True if valid, False if invalid

Source code in src/appimage_updater/config/operations.py
def validate_add_rotation_config(rotation: bool | None, symlink: str | None) -> bool:
    """Validate rotation and symlink combination for add command.

    Returns:
        True if valid, False if invalid
    """
    if rotation is True and symlink is None:
        console.print("[red]Error: --rotation requires a symlink path")
        console.print("[yellow]File rotation needs a managed symlink to work properly.")
        console.print("[yellow]Either provide --symlink PATH or use --no-rotation to disable rotation.")
        console.print("[yellow]Example: --rotation --symlink ~/bin/myapp.AppImage")
        return False
    return True

validate_and_normalize_add_url(url, direct=None)

Validate and normalize URL for add command.

Parameters:

Name Type Description Default
url str

The URL to validate

required
direct bool | None

If True, treat as direct download URL and skip repository validation

None

Returns:

Type Description
str | None

Normalized URL if valid, None if invalid

Source code in src/appimage_updater/config/operations.py
def validate_and_normalize_add_url(url: str, direct: bool | None = None) -> str | None:
    """Validate and normalize URL for add command.

    Args:
        url: The URL to validate
        direct: If True, treat as direct download URL and skip repository validation

    Returns:
        Normalized URL if valid, None if invalid
    """
    if direct:
        return _validate_direct_url(url)

    return _validate_and_normalize_repository_url(url)

validate_basic_field_updates(updates)

Validate basic field updates.

Source code in src/appimage_updater/config/operations.py
def validate_basic_field_updates(updates: dict[str, Any]) -> None:
    """Validate basic field updates."""
    # Validate pattern if provided
    if "pattern" in updates:
        try:
            re.compile(updates["pattern"])
        except re.error as e:
            raise ValueError(f"Invalid regex pattern: {e}") from e

    # Validate checksum algorithm if provided
    if "checksum_algorithm" in updates:
        valid_algorithms = ["sha256", "sha1", "md5"]
        if updates["checksum_algorithm"] not in valid_algorithms:
            raise ValueError(f"Invalid checksum algorithm. Must be one of: {', '.join(valid_algorithms)}")

validate_edit_updates(app, updates, create_dir, yes=False)

Validate the proposed updates before applying them.

Source code in src/appimage_updater/config/operations.py
def validate_edit_updates(app: Any, updates: dict[str, Any], create_dir: bool, yes: bool = False) -> None:
    """Validate the proposed updates before applying them."""
    validate_url_update(updates)
    validate_basic_field_updates(updates)
    validate_symlink_path(updates)
    validate_rotation_consistency(app, updates)
    handle_directory_creation(updates, create_dir, yes)
    handle_path_expansions(updates)

validate_rotation_consistency(app, updates)

Validate rotation configuration consistency.

Source code in src/appimage_updater/config/operations.py
def validate_rotation_consistency(app: Any, updates: dict[str, Any]) -> None:
    """Validate rotation configuration consistency."""
    # Check if rotation is being enabled without a symlink path
    rotation_enabled = updates.get("rotation_enabled")
    symlink_path = updates.get("symlink_path")

    # Also check current app state for symlink
    current_symlink = getattr(app, "symlink_path", None)

    if rotation_enabled is True and symlink_path is None and current_symlink is None:
        raise ValueError("File rotation requires a symlink path. Use --symlink-path to specify one.")

Validate symlink path if provided.

Source code in src/appimage_updater/config/operations.py
def validate_symlink_path(updates: dict[str, Any]) -> None:
    """Validate symlink path if provided."""
    if "symlink_path" not in updates:
        return

    symlink_path = updates["symlink_path"]

    validate_symlink_path_exists(symlink_path)
    expanded_path = expand_symlink_path(symlink_path)
    validate_symlink_path_characters(expanded_path, symlink_path)
    normalized_path = normalize_and_validate_symlink_path(expanded_path, symlink_path)

    # Update with the normalized path
    updates["symlink_path"] = str(normalized_path)

Check if path contains invalid characters.

Source code in src/appimage_updater/config/operations.py
def validate_symlink_path_characters(expanded_path: Path, original_path: str) -> None:
    """Check if path contains invalid characters."""
    path_str = str(expanded_path)
    if any(char in path_str for char in ["\x00", "\n", "\r"]):
        raise ValueError(f"Symlink path contains invalid characters: {original_path}")

Check if symlink path is not empty or whitespace-only.

Source code in src/appimage_updater/config/operations.py
def validate_symlink_path_exists(symlink_path: str) -> None:
    """Check if symlink path is not empty or whitespace-only."""
    if not symlink_path or not symlink_path.strip():
        raise ValueError("Symlink path cannot be empty. Provide a valid file path.")

validate_url_update(updates)

Validate URL update if provided.

Source code in src/appimage_updater/config/operations.py
def validate_url_update(updates: dict[str, Any]) -> None:
    """Validate URL update if provided."""
    if "url" not in updates:
        return

    url = updates["url"]
    force = updates.get("force", False)

    if force:
        # Skip validation and normalization when --force is used
        console.print("[yellow]Warning: Using --force: Skipping URL validation and normalization")
        logger.debug(f"Skipping URL validation for '{url}' due to --force flag")
        # Remove the force flag from updates as it's not needed for config storage
        updates.pop("force", None)
        return

    try:
        repo_client = get_repository_client(url)
        normalized_url, was_corrected = repo_client.normalize_repo_url(url)

        # Validate that we can parse the normalized URL
        repo_client.parse_repo_url(normalized_url)

        # Update with normalized URL
        updates["url"] = normalized_url
    except (ValueError, AttributeError, TypeError, RepositoryError) as e:
        raise ValueError(f"Invalid repository URL: {url} - {e}") from e

    # Show correction to user if URL was corrected
    if was_corrected:
        console.print("[yellow]Detected download URL, using repository URL instead:")
        console.print(f"[dim]   Original: {url}")
        console.print(f"[dim]   Corrected: {normalized_url}")
        logger.debug(f"Corrected URL from '{url}' to '{normalized_url}'")