obsideo-cli 0.2.5__tar.gz → 0.2.6__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.
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/PKG-INFO +1 -1
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo/cli.py +97 -12
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo/sync.py +17 -10
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_cli.egg-info/PKG-INFO +1 -1
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/pyproject.toml +1 -1
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/tests/test_cli.py +36 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/README.md +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo/__init__.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo/__main__.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo/manifest.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_cli.egg-info/SOURCES.txt +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_cli.egg-info/dependency_links.txt +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_cli.egg-info/entry_points.txt +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_cli.egg-info/requires.txt +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_cli.egg-info/top_level.txt +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_core/__init__.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_core/config.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_core/crypto.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_core/identity.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_core/login.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_core/names.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/obsideo_core/storage.py +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/setup.cfg +0 -0
- {obsideo_cli-0.2.5 → obsideo_cli-0.2.6}/tests/test_core.py +0 -0
|
@@ -310,6 +310,13 @@ def run_login(url: str | None = None) -> bool:
|
|
|
310
310
|
print("Note: storage activation is finishing rollout; if an upload fails, retry shortly.")
|
|
311
311
|
print("Your files are encrypted with a local key. Back it up:")
|
|
312
312
|
print(f" {crypto.DATA_KEY_FILE}")
|
|
313
|
+
# Create the sync folder now so it's ready (never make the user mkdir it).
|
|
314
|
+
try:
|
|
315
|
+
from obsideo import sync as _sync
|
|
316
|
+
sd = _sync.ensure_sync_dir()
|
|
317
|
+
print(f"Your sync folder is ready (drop files here, then `sync push`):\n {sd}")
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
313
320
|
print("Type 'obsideo' to open the shell, or 'obsideo put <file>' to store something.")
|
|
314
321
|
return True
|
|
315
322
|
|
|
@@ -541,27 +548,105 @@ class ObsideoShell(cmd.Cmd):
|
|
|
541
548
|
|
|
542
549
|
# ── account ───────────────────────────────────────────────────────────────
|
|
543
550
|
def do_account(self, arg):
|
|
544
|
-
"""Show your
|
|
551
|
+
"""Show your account: plan, storage used, and where your files/keys live."""
|
|
545
552
|
if not self._require_login():
|
|
546
553
|
return
|
|
547
|
-
|
|
554
|
+
from obsideo import sync as sync_mod
|
|
548
555
|
print()
|
|
549
556
|
print(" -- Obsideo account --------------------------")
|
|
550
557
|
print(" Plan: Free")
|
|
551
|
-
if
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
print(
|
|
555
|
-
bar_len = 30
|
|
556
|
-
filled = int(bar_len * min(pct, 1.0))
|
|
557
|
-
print(f" [{'#'*filled}{'-'*(bar_len-filled)}]")
|
|
558
|
-
if pct >= 0.8:
|
|
559
|
-
print(" You're near your limit - reply to any Obsideo email to upgrade.")
|
|
558
|
+
if not config.account_token():
|
|
559
|
+
# No shim token (e.g. creds set via env / pre-login account): we can't
|
|
560
|
+
# query usage. Don't claim the service is down — tell them how to link it.
|
|
561
|
+
print(" Usage: sign in with `obsideo login` to see usage details")
|
|
560
562
|
else:
|
|
561
|
-
|
|
563
|
+
usage = _fetch_usage()
|
|
564
|
+
if usage:
|
|
565
|
+
used, quota = usage["used_bytes"], usage["quota_bytes"]
|
|
566
|
+
pct = usage.get("percent_used", (used / quota if quota else 0))
|
|
567
|
+
print(f" Used: {_human(used)} / {_human(quota)} ({pct*100:.1f}%)")
|
|
568
|
+
bar_len = 30
|
|
569
|
+
filled = int(bar_len * min(pct, 1.0))
|
|
570
|
+
print(f" [{'#'*filled}{'-'*(bar_len-filled)}]")
|
|
571
|
+
if pct >= 0.8:
|
|
572
|
+
print(" You're near your limit - reply to any Obsideo email to upgrade.")
|
|
573
|
+
else:
|
|
574
|
+
print(" Usage: couldn't reach the account service - try again shortly")
|
|
575
|
+
print(f" Files: bucket '{storage.bucket()}' · sync folder {sync_mod.ensure_sync_dir()}")
|
|
576
|
+
print(f" Keys: {config.CONFIG_DIR} (back up data.key)")
|
|
562
577
|
print(" ---------------------------------------------")
|
|
563
578
|
print()
|
|
564
579
|
|
|
580
|
+
# ── about / faq / messages ────────────────────────────────────────────────
|
|
581
|
+
def do_about(self, arg):
|
|
582
|
+
"""What Obsideo is."""
|
|
583
|
+
print("""
|
|
584
|
+
OBSIDEO DRIVE - encrypted storage we can't read.
|
|
585
|
+
|
|
586
|
+
Your files are encrypted on your device (AES-256-GCM) before they ever leave
|
|
587
|
+
it, then stored across three independent providers (RF=3). Obsideo's servers
|
|
588
|
+
only ever see ciphertext - never your filenames, never your data.
|
|
589
|
+
|
|
590
|
+
- Free: 3 GB, no card, no expiry.
|
|
591
|
+
- Your keys live only on your machine (~/.obsideo). Back up data.key - lose it
|
|
592
|
+
and the data is unrecoverable by design. That's the point: not even we can read it.
|
|
593
|
+
- Install / update: pip install -U obsideo-cli More: https://obsideo.io
|
|
594
|
+
""")
|
|
595
|
+
|
|
596
|
+
def do_faq(self, arg):
|
|
597
|
+
"""Frequently asked questions."""
|
|
598
|
+
print("""
|
|
599
|
+
-- Obsideo FAQ --
|
|
600
|
+
|
|
601
|
+
Q: Can Obsideo read my files?
|
|
602
|
+
A: No. They're encrypted on your device before upload; we only store ciphertext.
|
|
603
|
+
|
|
604
|
+
Q: What's free?
|
|
605
|
+
A: 3 GB, no credit card, no expiry.
|
|
606
|
+
|
|
607
|
+
Q: What if I lose my key?
|
|
608
|
+
A: Your key is ~/.obsideo/data.key - back it up. Without it the data can't be
|
|
609
|
+
decrypted by anyone, including us.
|
|
610
|
+
|
|
611
|
+
Q: Two folders - what's the difference?
|
|
612
|
+
A: ~/.obsideo = your keys + settings (don't touch).
|
|
613
|
+
~/obsideo-sync = your SYNC folder - files you put here sync with `sync push`.
|
|
614
|
+
|
|
615
|
+
Q: What does `sync` do?
|
|
616
|
+
A: Mirrors your sync folder to/from the cloud. `sync push` uploads new/changed
|
|
617
|
+
files, `sync pull` downloads, `sync status` shows what's pending. It's manual
|
|
618
|
+
(you run it) and covers files in the top of the folder.
|
|
619
|
+
|
|
620
|
+
Q: How do I change settings (sync folder, encryption)?
|
|
621
|
+
A: `config` shows them; `config set sync_dir <path>` / `config set encrypt_names false`.
|
|
622
|
+
|
|
623
|
+
Q: More space?
|
|
624
|
+
A: Reply to any Obsideo email to upgrade. (A referral program is coming.)
|
|
625
|
+
|
|
626
|
+
Q: Updating?
|
|
627
|
+
A: pip install -U obsideo-cli - the CLI also nudges you when an update is out.
|
|
628
|
+
""")
|
|
629
|
+
|
|
630
|
+
def do_messages(self, arg):
|
|
631
|
+
"""Messages from the Obsideo team."""
|
|
632
|
+
try:
|
|
633
|
+
req = urllib.request.Request(
|
|
634
|
+
f"{config.signup_url()}/v1/notices",
|
|
635
|
+
headers={"User-Agent": config.USER_AGENT},
|
|
636
|
+
)
|
|
637
|
+
with urllib.request.urlopen(req, timeout=10, context=config.ssl_context()) as resp:
|
|
638
|
+
notices = json.loads(resp.read().decode()).get("notices", [])
|
|
639
|
+
except Exception:
|
|
640
|
+
print("\n Couldn't reach the message service - try again shortly.\n")
|
|
641
|
+
return
|
|
642
|
+
if not notices:
|
|
643
|
+
print("\n No messages from the Obsideo team right now.\n")
|
|
644
|
+
return
|
|
645
|
+
print("\n -- Messages from the Obsideo team --")
|
|
646
|
+
for n in notices:
|
|
647
|
+
print(f" - {n.get('body', '').strip()}")
|
|
648
|
+
print()
|
|
649
|
+
|
|
565
650
|
# ── sync ──────────────────────────────────────────────────────────────────
|
|
566
651
|
def do_sync(self, arg):
|
|
567
652
|
"""Sync your local folder with Obsideo. Usage: sync push|pull|status"""
|
|
@@ -18,18 +18,24 @@ def _sync_dir() -> Path:
|
|
|
18
18
|
return Path(config.load_config().get("sync_dir", str(Path.home() / "obsideo-sync")))
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def ensure_sync_dir() -> Path:
|
|
22
|
+
"""Return the sync folder, creating it if needed. Called on login and by every
|
|
23
|
+
sync command so a user never has to make the folder by hand — it just exists."""
|
|
24
|
+
sd = _sync_dir()
|
|
25
|
+
sd.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
return sd
|
|
27
|
+
|
|
28
|
+
|
|
21
29
|
def _remote_key(name: str) -> str:
|
|
22
30
|
return f"{REMOTE_PREFIX}{name}"
|
|
23
31
|
|
|
24
32
|
|
|
25
33
|
def sync_status() -> dict:
|
|
26
|
-
sync_dir =
|
|
34
|
+
sync_dir = ensure_sync_dir()
|
|
27
35
|
entries = manifest.get_all()
|
|
28
36
|
status = {"to_push": [], "to_pull": [], "synced": []}
|
|
29
37
|
|
|
30
|
-
local_files = {}
|
|
31
|
-
if sync_dir.exists():
|
|
32
|
-
local_files = {f.name: f for f in sync_dir.iterdir() if f.is_file()}
|
|
38
|
+
local_files = {f.name: f for f in sync_dir.iterdir() if f.is_file()}
|
|
33
39
|
|
|
34
40
|
for name, f in local_files.items():
|
|
35
41
|
local_hash = manifest.file_sha256(f)
|
|
@@ -53,17 +59,19 @@ def sync_status() -> dict:
|
|
|
53
59
|
|
|
54
60
|
|
|
55
61
|
def push(verbose: bool = True) -> int:
|
|
56
|
-
sync_dir =
|
|
57
|
-
|
|
62
|
+
sync_dir = ensure_sync_dir()
|
|
63
|
+
files = [p for p in sync_dir.iterdir() if p.is_file()]
|
|
64
|
+
if not files:
|
|
58
65
|
if verbose:
|
|
59
|
-
print(f"
|
|
66
|
+
print(f" Your sync folder is empty:\n {sync_dir}\n"
|
|
67
|
+
f" Drop files in there, then run `sync push` again.")
|
|
60
68
|
return 0
|
|
61
69
|
|
|
62
70
|
do_encrypt = config.load_config().get("encrypt", True)
|
|
63
71
|
entries = manifest.get_all()
|
|
64
72
|
pushed = 0
|
|
65
73
|
|
|
66
|
-
for f in
|
|
74
|
+
for f in files:
|
|
67
75
|
local_hash = manifest.file_sha256(f)
|
|
68
76
|
entry = entries.get(f.name)
|
|
69
77
|
if entry and entry.get("local_hash") == local_hash:
|
|
@@ -87,8 +95,7 @@ def push(verbose: bool = True) -> int:
|
|
|
87
95
|
|
|
88
96
|
|
|
89
97
|
def pull(verbose: bool = True) -> int:
|
|
90
|
-
sync_dir =
|
|
91
|
-
sync_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
sync_dir = ensure_sync_dir()
|
|
92
99
|
|
|
93
100
|
try:
|
|
94
101
|
remote = storage.list_prefix(REMOTE_PREFIX)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "obsideo-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
8
8
|
description = "Obsideo Cloud - encrypted storage we can't read. Save, browse, and sync whatever you want, from your terminal."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -246,3 +246,39 @@ def test_update_check_windows_prints_command_not_pip(monkeypatch, capsys):
|
|
|
246
246
|
err = capsys.readouterr().err
|
|
247
247
|
assert ran == [] # never attempts the locked self-replace
|
|
248
248
|
assert "pip install -U --no-cache-dir obsideo-cli" in err
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ── sync auto-setup + info commands (0.2.6) ───────────────────────────────────
|
|
252
|
+
|
|
253
|
+
def test_ensure_sync_dir_creates(tmp_path, monkeypatch):
|
|
254
|
+
from obsideo import sync
|
|
255
|
+
target = tmp_path / "mysync"
|
|
256
|
+
monkeypatch.setattr(sync, "_sync_dir", lambda: target)
|
|
257
|
+
assert not target.exists()
|
|
258
|
+
sd = sync.ensure_sync_dir()
|
|
259
|
+
assert sd == target and target.is_dir() # auto-created, no manual mkdir
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_push_empty_folder_is_friendly_noop(tmp_path, monkeypatch, capsys):
|
|
263
|
+
from obsideo import sync
|
|
264
|
+
monkeypatch.setattr(sync, "_sync_dir", lambda: tmp_path / "s")
|
|
265
|
+
n = sync.push(verbose=True)
|
|
266
|
+
assert n == 0
|
|
267
|
+
out = capsys.readouterr().out.lower()
|
|
268
|
+
assert "empty" in out and "sync push" in out # guides instead of erroring
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_about_and_faq_print(capsys):
|
|
272
|
+
sh = cli.ObsideoShell()
|
|
273
|
+
sh.do_about("")
|
|
274
|
+
sh.do_faq("")
|
|
275
|
+
out = capsys.readouterr().out
|
|
276
|
+
assert "OBSIDEO DRIVE" in out and "FAQ" in out and "obsideo-sync" in out
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_messages_handles_unreachable(monkeypatch, capsys):
|
|
280
|
+
def boom(*a, **k):
|
|
281
|
+
raise OSError("network down")
|
|
282
|
+
monkeypatch.setattr(cli.urllib.request, "urlopen", boom)
|
|
283
|
+
cli.ObsideoShell().do_messages("")
|
|
284
|
+
assert "couldn't reach" in capsys.readouterr().out.lower()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|