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

convert_app_to_dict(app)

Convert application object to dictionary for JSON serialization.

Source code in src/appimage_updater/config/operations.py
def convert_app_to_dict(app: Any) -> dict[str, Any]:
    """Convert application object to dictionary for JSON serialization."""
    # Build core application dictionary
    app_dict = _build_core_app_dict(app)

    # Add optional fields
    _add_optional_fields(app_dict, app)

    return app_dict

determine_save_target(config_file, config_dir)

Determine where to save the configuration (file or directory).

Source code in src/appimage_updater/config/operations.py
def determine_save_target(config_file: Path | None, config_dir: Path | None) -> tuple[Path | None, Path | None]:
    """Determine where to save the configuration (file or directory)."""
    if config_file:
        return config_file, None
    elif config_dir:
        return None, config_dir
    else:
        # Use defaults
        default_dir = GlobalConfigManager.get_default_config_dir()
        default_file = GlobalConfigManager.get_default_config_path()

        if default_dir.exists():
            return None, default_dir
        elif default_file.exists():
            return default_file, None
        else:
            return None, default_dir  # Default to directory-based

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)

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

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

    return config, prerelease_auto_enabled

handle_add_directory_creation(download_dir, create_dir, yes=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) -> 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)
    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

save_updated_configuration(app, config, config_file, config_dir)

Save the updated configuration back to file or directory.

Source code in src/appimage_updater/config/operations.py
def save_updated_configuration(app: Any, config: Any, config_file: Path | None, config_dir: Path | None) -> None:
    """Save the updated configuration back to file or directory."""
    app_dict = convert_app_to_dict(app)
    target_file, target_dir = determine_save_target(config_file, config_dir)

    if target_file:
        update_app_in_config_file(app_dict, target_file)
    elif target_dir:
        update_app_in_config_directory(app_dict, target_dir)
    else:
        raise ValueError("Could not determine where to save configuration")

update_app_in_config_directory(app_dict, config_dir)

Update application in a directory-based config structure.

Source code in src/appimage_updater/config/operations.py
def update_app_in_config_directory(app_dict: dict[str, Any], config_dir: Path) -> None:
    """Update application in a directory-based config structure."""
    # Convert dict to ApplicationConfig object
    app_config = ApplicationConfig(**app_dict)

    # Use manager method for config file operations
    manager = Manager()
    manager.update_application_in_config_directory(app_config, config_dir)

update_app_in_config_file(app_dict, config_file)

Update application in a single JSON config file.

Source code in src/appimage_updater/config/operations.py
def update_app_in_config_file(app_dict: dict[str, Any], config_file: Path) -> None:
    """Update application in a single JSON config file."""
    # Convert dict to ApplicationConfig object
    app_config = ApplicationConfig(**app_dict)

    # Use manager method for config file operations
    manager = Manager()
    manager.update_application_in_config_file(app_config, config_file)

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
    """
    # For direct URLs, just validate basic URL format and return as-is
    if direct:
        try:
            parsed = urlparse(url)
            if not parsed.scheme or not parsed.netloc:
                console.print(f"[red]Error: Invalid URL format: {url}")
                return None
            return url
        except Exception as e:
            console.print(f"[red]Error: Invalid URL format: {url}")
            console.print(f"[yellow]Error details: {e}")
            return None

    # For repository URLs, use the existing validation logic
    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)

    except Exception as e:
        console.print(f"[red]Error: Invalid repository URL: {url}")
        console.print(f"[yellow]Error details: {e}")
        return None

    # Inform user if we corrected the URL
    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 download URL to repository URL: {url}{normalized_url}")

    return normalized_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 Exception 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}'")