Coverage for seedboxsync/core/utils.py: 100%
56 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 17:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 17:46 +0000
1#
2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr>
3#
4# For the full copyright and license information, please view the LICENSE
5# file that was distributed with this source code.
6#
7"""A collection of utility functions for SeedboxSync."""
9import os
10from os import PathLike
11from pathlib import Path
12from typing import Any
13from urllib.parse import urlparse
14from bcoding import bdecode
15from flask import current_app as app
18def byte_to_gi(bytes_value: float, suffix: str = "B") -> str:
19 """
20 Convert in human readable units.
22 Args:
23 bytes_value (integer): Value not human readable.
24 suffix (str): Suffix for value given to (default: B).
26 Returns:
27 str: human readable value in Gi.
28 """
29 gib = bytes_value / (1024**3)
30 return f"{gib:.1f}Gi{suffix}"
33def ensure_dir_exists(path: str | PathLike[str]) -> None:
34 """
35 Ensure the directory ``path`` exists, and if not create it.
37 Args:
38 path (str): The filesystem path of a directory.
40 Raises:
41 AssertionError: If the directory ``path`` exists, but is not a directory.
43 """
44 path = Path(path).expanduser().resolve()
46 if path.exists() and not path.is_dir():
47 raise AssertionError(f"Path `{path}` exists but is not a directory!")
48 if not path.exists():
49 path.mkdir()
52def get_torrent_infos(torrent_path: str | PathLike[str]) -> Any | None:
53 """
54 Extracts information from a torrent file.
56 Args:
57 torrent_path (str | PathLike[str]): Path to the torrent file.
59 Returns:
60 str: Decoded torrent information.
62 Raises:
63 Exception: If the file is not a valid torrent.
64 """
65 with Path(torrent_path).open("rb") as torrent:
66 torrent_info = None
68 try:
69 torrent_info = bdecode(torrent.read())
70 except Exception:
71 app.logger.exception("Not valid torrent")
72 finally:
73 torrent.close()
75 return torrent_info
78def is_running_in_docker() -> bool:
79 """
80 Return whether the current process appears to run inside Docker.
82 Returns:
83 bool: True if in docker envoronment.
85 """
86 # Test mountinfo
87 if Path("/proc/self/mountinfo").exists():
88 with Path("/proc/self/mountinfo").open() as f:
89 if "docker" in f.read() or "overlay" in f.read():
90 return True
92 # Test du cgroup
93 if Path("/proc/1/cgroup").exists():
94 with Path("/proc/1/cgroup").open() as f:
95 lines = f.read()
96 if "docker" in lines or "kubepods" in lines:
97 return True
99 # Test /.dockerenv but not on podman
100 return Path("/.dockerenv").exists()
103def get_web_healthcheck_url() -> str:
104 """
105 Return the URL used to check the local Flask application.
107 Returns:
108 str: The healthcheck URL.
109 """
110 explicit_url = os.getenv("HEALTHCHECK_URL")
111 if explicit_url:
112 return explicit_url.rstrip("/") + "/healthcheck"
114 bind = os.getenv("BIND")
115 if bind:
116 return _healthcheck_url_from_bind(bind)
118 port = 8000 if is_running_in_docker() else 5000
119 return f"http://127.0.0.1:{port}/healthcheck"
122def _healthcheck_url_from_bind(bind: str) -> str:
123 """
124 Build a local healthcheck URL from a Gunicorn bind value.
126 Returns:
127 str: The healthcheck URL from BIND.
128 """
129 bind = bind.strip()
131 if bind.startswith("unix:"):
132 raise ValueError("A Unix socket bind cannot be checked with a standard HTTP URL.")
134 # urlparse requires a scheme to correctly parse host and port.
135 parsed = urlparse(f"//{bind}")
137 if parsed.port is None:
138 raise ValueError(f"Invalid BIND value: {bind}")
140 host = parsed.hostname or "127.0.0.1"
142 if host in {"0.0.0.0", "::", "[::]"}:
143 host = "127.0.0.1"
145 return f"http://{host}:{parsed.port}/healthcheck"