termux-sync 1.2.0__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {termux_sync-1.2.0 → termux_sync-1.2.2}/CHANGELOG.md +21 -0
- {termux_sync-1.2.0 → termux_sync-1.2.2}/PKG-INFO +1 -1
- {termux_sync-1.2.0 → termux_sync-1.2.2}/pyproject.toml +1 -1
- {termux_sync-1.2.0 → termux_sync-1.2.2}/src/termux_sync/__init__.py +98 -59
- {termux_sync-1.2.0 → termux_sync-1.2.2}/LICENSE +0 -0
- {termux_sync-1.2.0 → termux_sync-1.2.2}/README.md +0 -0
|
@@ -18,6 +18,27 @@ Version numbers follow [Semantic Versioning](https://semver.org/).
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
+
## [1.2.2] - 2026-07-11
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **`restore` — stuck retrying and failing with `HTTP Error 404: Not Found` when the GitHub backend points to a private repo** — `GitHubStorage.download()` and the `manifest.json` lookup in `list_backups()` downloaded release assets via `asset["browser_download_url"]` using the standard `Accept: application/vnd.github.v3+json` header. That URL only resolves without further authentication for public repos; for private repos GitHub returns 404 even with a valid token, since the download must go through the `/repos/{repo}/releases/assets/{id}` API endpoint with `Accept: application/octet-stream`. Added `GitHubStorage._asset_headers()` and `_asset_url()` and switched both call sites to use them, so restoring from a private-repo backend now downloads correctly instead of exhausting all 8 retries.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## [1.2.1] - 2026-05-22
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`setup` — required fields accepted empty input silently** — pressing Enter on the `GitHub Personal Access Token` and `GitHub repo (owner/repo)` prompts passed through an empty string, which would later cause a crash or silent failure when actually running a backup or restore. Both fields are now validated in a loop that keeps prompting until a non-empty value is provided, with a descriptive error message on each failed attempt.
|
|
34
|
+
- `github_token`: re-prompts with `✗ Token cannot be empty. Paste your Personal Access Token.`
|
|
35
|
+
- `github_repo`: re-prompts with `✗ Invalid format. Use: username/repo-name` if empty or if the value does not contain exactly one `/` in a valid position
|
|
36
|
+
- `local_path`: re-prompts with `✗ Path cannot be empty.` if the user clears the default and submits nothing
|
|
37
|
+
- **`setup` — existing token not acknowledged** — when a GitHub token was already saved in config, the password prompt showed an empty field with no indication that a value already existed. A hint line is now printed before the prompt: `A token is already saved. Press Enter to keep it, or paste a new one.`
|
|
38
|
+
- **`KeyboardInterrupt` traceback instead of clean exit** — pressing Ctrl+C during any interactive prompt (e.g. `setup`, `schedule`, `restore`, `delete`) printed a full Python traceback instead of the intended cancellation panel. The root cause was that the `try/except KeyboardInterrupt` block only wrapped `main()` inside `if __name__ == "__main__"`, which is never executed when the script is invoked via a pip entry point or the tsctl launcher. The handler has been moved inside `main()` itself so it fires regardless of invocation method. The panel logic was extracted into a dedicated `_abort()` helper and `if __name__ == "__main__"` simplified to a bare `main()` call.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
21
42
|
## [1.2.0] - 2026-05-22
|
|
22
43
|
|
|
23
44
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termux-sync
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Backup and restore your entire Termux environment across devices
|
|
5
5
|
Project-URL: Homepage, https://github.com/djunekz/termux-sync
|
|
6
6
|
Project-URL: Repository, https://github.com/djunekz/termux-sync
|
|
@@ -43,7 +43,7 @@ try:
|
|
|
43
43
|
except ImportError:
|
|
44
44
|
RICH_AVAILABLE = False
|
|
45
45
|
|
|
46
|
-
VERSION = "1.2.
|
|
46
|
+
VERSION = "1.2.1"
|
|
47
47
|
APP_NAME = "termux-sync"
|
|
48
48
|
CONFIG_DIR = Path.home() / ".config" / APP_NAME
|
|
49
49
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
@@ -469,6 +469,15 @@ class GitHubStorage:
|
|
|
469
469
|
"Accept": "application/vnd.github.v3+json",
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
def _asset_headers(self) -> dict:
|
|
473
|
+
return {
|
|
474
|
+
"Authorization": f"token {self.token}",
|
|
475
|
+
"Accept": "application/octet-stream",
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
def _asset_url(self, asset_id: int) -> str:
|
|
479
|
+
return f"{self.API}/repos/{self.repo}/releases/assets/{asset_id}"
|
|
480
|
+
|
|
472
481
|
def _api(self, method: str, endpoint: str, **kwargs):
|
|
473
482
|
import urllib.request, urllib.error
|
|
474
483
|
url = f"{self.API}{endpoint}"
|
|
@@ -633,8 +642,8 @@ class GitHubStorage:
|
|
|
633
642
|
try:
|
|
634
643
|
import urllib.request
|
|
635
644
|
req = urllib.request.Request(
|
|
636
|
-
asset["
|
|
637
|
-
headers=self.
|
|
645
|
+
self._asset_url(asset["id"]),
|
|
646
|
+
headers=self._asset_headers()
|
|
638
647
|
)
|
|
639
648
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
640
649
|
manifest = json.loads(r.read())
|
|
@@ -659,14 +668,14 @@ class GitHubStorage:
|
|
|
659
668
|
dl_task = progress.add_task("[cyan]Downloading from GitHub...", total=len(assets))
|
|
660
669
|
|
|
661
670
|
for asset in assets:
|
|
662
|
-
url = asset["
|
|
671
|
+
url = self._asset_url(asset["id"])
|
|
663
672
|
out = dest / asset["name"]
|
|
664
673
|
size = asset.get("size", 0)
|
|
665
674
|
f_task = progress.add_task(f"[cyan]{asset['name']}...", total=max(size, 1))
|
|
666
675
|
last_err = None
|
|
667
676
|
for attempt in range(8):
|
|
668
677
|
try:
|
|
669
|
-
req = urllib.request.Request(url, headers=self.
|
|
678
|
+
req = urllib.request.Request(url, headers=self._asset_headers())
|
|
670
679
|
with urllib.request.urlopen(req, timeout=120) as r:
|
|
671
680
|
data = r.read()
|
|
672
681
|
out.write_bytes(data)
|
|
@@ -1138,9 +1147,14 @@ def cmd_setup():
|
|
|
1138
1147
|
cfg["storage"] = storage_map[choice]
|
|
1139
1148
|
|
|
1140
1149
|
if cfg["storage"] == "local":
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1150
|
+
while True:
|
|
1151
|
+
val = Prompt.ask(
|
|
1152
|
+
" Local backup path", default=cfg.get("local_path", str(DEFAULT_DEST))
|
|
1153
|
+
).strip()
|
|
1154
|
+
if val:
|
|
1155
|
+
cfg["local_path"] = val
|
|
1156
|
+
break
|
|
1157
|
+
console.print(" [red]✗ Path cannot be empty.[/red]")
|
|
1144
1158
|
|
|
1145
1159
|
elif cfg["storage"] == "gdrive":
|
|
1146
1160
|
console.print()
|
|
@@ -1159,12 +1173,34 @@ def cmd_setup():
|
|
|
1159
1173
|
)
|
|
1160
1174
|
console.print(" [dim]Token URL:[/dim] [white]https://github.com/settings/tokens[/white]")
|
|
1161
1175
|
console.print()
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1176
|
+
|
|
1177
|
+
existing_token = cfg.get("github_token", "")
|
|
1178
|
+
if existing_token:
|
|
1179
|
+
console.print(" [dim]A token is already saved. Press Enter to keep it, or paste a new one.[/dim]")
|
|
1180
|
+
while True:
|
|
1181
|
+
val = Prompt.ask(
|
|
1182
|
+
" GitHub Personal Access Token",
|
|
1183
|
+
default=existing_token,
|
|
1184
|
+
password=True,
|
|
1185
|
+
).strip()
|
|
1186
|
+
if val:
|
|
1187
|
+
cfg["github_token"] = val
|
|
1188
|
+
break
|
|
1189
|
+
console.print(" [red]✗ Token cannot be empty. Paste your Personal Access Token.[/red]")
|
|
1190
|
+
|
|
1191
|
+
while True:
|
|
1192
|
+
val = Prompt.ask(
|
|
1193
|
+
" GitHub repo (owner/repo)", default=cfg.get("github_repo", "")
|
|
1194
|
+
).strip()
|
|
1195
|
+
if not val:
|
|
1196
|
+
console.print(" [red]✗ Repo cannot be empty. Enter in format: username/repo-name[/red]")
|
|
1197
|
+
continue
|
|
1198
|
+
if "/" not in val or val.startswith("/") or val.endswith("/"):
|
|
1199
|
+
console.print(" [red]✗ Invalid format. Use: username/repo-name (e.g. djunekz/my-termux-backup)[/red]")
|
|
1200
|
+
continue
|
|
1201
|
+
cfg["github_repo"] = val
|
|
1202
|
+
break
|
|
1203
|
+
|
|
1168
1204
|
cfg["github_branch"] = Prompt.ask(
|
|
1169
1205
|
" Branch", default=cfg.get("github_branch", "main")
|
|
1170
1206
|
)
|
|
@@ -1757,51 +1793,54 @@ Examples:
|
|
|
1757
1793
|
|
|
1758
1794
|
cfg = load_config()
|
|
1759
1795
|
|
|
1760
|
-
if args.command == "setup":
|
|
1761
|
-
cmd_setup()
|
|
1762
|
-
elif args.command == "backup":
|
|
1763
|
-
cmd_backup(cfg, label=args.label)
|
|
1764
|
-
elif args.command == "restore":
|
|
1765
|
-
cmd_restore(cfg, backup_name=args.name)
|
|
1766
|
-
elif args.command == "list":
|
|
1767
|
-
cmd_list(cfg)
|
|
1768
|
-
elif args.command == "delete":
|
|
1769
|
-
cmd_delete(cfg, backup_name=args.name)
|
|
1770
|
-
elif args.command == "schedule":
|
|
1771
|
-
cmd_schedule()
|
|
1772
|
-
elif args.command == "daemon":
|
|
1773
|
-
cmd_daemon(cfg)
|
|
1774
|
-
elif args.command == "status":
|
|
1775
|
-
cmd_status(cfg)
|
|
1776
|
-
elif args.command == "logs":
|
|
1777
|
-
cmd_logs(args.lines)
|
|
1778
|
-
elif args.command == "clear-logs":
|
|
1779
|
-
cmd_clear_logs()
|
|
1780
|
-
elif args.command == "check":
|
|
1781
|
-
cmd_check(args.target or "")
|
|
1782
|
-
elif args.command == "clear-cache":
|
|
1783
|
-
cmd_clear_cache()
|
|
1784
|
-
elif args.command == "export-config":
|
|
1785
|
-
cmd_export_config(args.target or "")
|
|
1786
|
-
elif args.command == "import-config":
|
|
1787
|
-
cmd_import_config(args.target or "")
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
if __name__ == "__main__":
|
|
1791
1796
|
try:
|
|
1792
|
-
|
|
1797
|
+
if args.command == "setup":
|
|
1798
|
+
cmd_setup()
|
|
1799
|
+
elif args.command == "backup":
|
|
1800
|
+
cmd_backup(cfg, label=args.label)
|
|
1801
|
+
elif args.command == "restore":
|
|
1802
|
+
cmd_restore(cfg, backup_name=args.name)
|
|
1803
|
+
elif args.command == "list":
|
|
1804
|
+
cmd_list(cfg)
|
|
1805
|
+
elif args.command == "delete":
|
|
1806
|
+
cmd_delete(cfg, backup_name=args.name)
|
|
1807
|
+
elif args.command == "schedule":
|
|
1808
|
+
cmd_schedule()
|
|
1809
|
+
elif args.command == "daemon":
|
|
1810
|
+
cmd_daemon(cfg)
|
|
1811
|
+
elif args.command == "status":
|
|
1812
|
+
cmd_status(cfg)
|
|
1813
|
+
elif args.command == "logs":
|
|
1814
|
+
cmd_logs(args.lines)
|
|
1815
|
+
elif args.command == "clear-logs":
|
|
1816
|
+
cmd_clear_logs()
|
|
1817
|
+
elif args.command == "check":
|
|
1818
|
+
cmd_check(args.target or "")
|
|
1819
|
+
elif args.command == "clear-cache":
|
|
1820
|
+
cmd_clear_cache()
|
|
1821
|
+
elif args.command == "export-config":
|
|
1822
|
+
cmd_export_config(args.target or "")
|
|
1823
|
+
elif args.command == "import-config":
|
|
1824
|
+
cmd_import_config(args.target or "")
|
|
1793
1825
|
except KeyboardInterrupt:
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1826
|
+
_abort()
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
def _abort():
|
|
1830
|
+
console.print()
|
|
1831
|
+
console.print()
|
|
1832
|
+
console.print(
|
|
1833
|
+
Panel(
|
|
1834
|
+
"[bold yellow]Operation cancelled[/bold yellow]\n\n"
|
|
1835
|
+
" You pressed [bold]Ctrl+C[/bold] — termux-sync was stopped.\n"
|
|
1836
|
+
" [dim]Any partial temporary files have been cleaned up.[/dim]\n\n"
|
|
1837
|
+
" Run the command again whenever you're ready.",
|
|
1838
|
+
border_style="yellow",
|
|
1839
|
+
padding=(1, 2),
|
|
1806
1840
|
)
|
|
1807
|
-
|
|
1841
|
+
)
|
|
1842
|
+
sys.exit(0)
|
|
1843
|
+
|
|
1844
|
+
|
|
1845
|
+
if __name__ == "__main__":
|
|
1846
|
+
main()
|
|
File without changes
|
|
File without changes
|