remarkablesync 2.0.1__tar.gz → 2.0.3__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.
- {remarkablesync-2.0.1/remarkablesync.egg-info → remarkablesync-2.0.3}/PKG-INFO +1 -1
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/RemarkableSync.py +7 -2
- {remarkablesync-2.0.1 → remarkablesync-2.0.3/remarkablesync.egg-info}/PKG-INFO +1 -1
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/remarkablesync.egg-info/SOURCES.txt +3 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/__version__.py +1 -1
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/backup/backup_manager.py +37 -78
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/backup/connection.py +55 -15
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/commands/backup_command.py +2 -1
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/commands/config_command.py +92 -42
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/commands/pipeline.py +41 -26
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/commands/sync_command.py +16 -5
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/commands/watch_command.py +1 -1
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/config.py +13 -1
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/hybrid_converter.py +47 -27
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/pdf_md_converter.py +19 -89
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/rm_pdf_converter.py +25 -29
- remarkablesync-2.0.3/src/utils/__init__.py +62 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/utils/console.py +5 -5
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/utils/logging.py +6 -1
- remarkablesync-2.0.3/src/utils/name_registry.py +117 -0
- remarkablesync-2.0.3/tests/test_agent_issues.py +362 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_config.py +4 -2
- remarkablesync-2.0.3/tests/test_config_command.py +114 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_hybrid_converter.py +2 -2
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_md_exporter.py +16 -14
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_pipeline.py +6 -6
- remarkablesync-2.0.1/src/utils/__init__.py +0 -1
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/LICENSE +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/MANIFEST.in +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/README.md +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/pyproject.toml +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/remarkablesync.egg-info/dependency_links.txt +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/remarkablesync.egg-info/entry_points.txt +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/remarkablesync.egg-info/requires.txt +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/remarkablesync.egg-info/top_level.txt +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/requirements-dev.txt +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/requirements.txt +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/setup.cfg +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/setup.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/__init__.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/ai/__init__.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/ai/base_provider.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/ai/claude_provider.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/ai/github_models_provider.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/auth/__init__.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/auth/github_device_flow.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/backup/__init__.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/backup/metadata.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/commands/__init__.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/commands/convert_command.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/converters/__init__.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/converters/base_converter.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/converters/v4_converter.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/converters/v5_converter.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/converters/v6_converter.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/keyring_store.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/md_export/__init__.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/ocr/__init__.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/ocr/ocr_engine.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/src/template_renderer.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/__init__.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/conftest.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/generate_test_assets.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/mock_ai_provider.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/mock_connection.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_ai_providers.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_auth_device_flow.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_basic.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_console.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_keyring_store.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_logging.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_metadata.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_ocr_pipeline.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_template_renderer.py +0 -0
- {remarkablesync-2.0.1 → remarkablesync-2.0.3}/tests/test_wifi_connection.py +0 -0
|
@@ -590,7 +590,13 @@ def main():
|
|
|
590
590
|
|
|
591
591
|
if not has_command and '--version' not in sys.argv and '--help' not in sys.argv:
|
|
592
592
|
# Load config to decide which pipeline to run
|
|
593
|
-
from src.config import load_config
|
|
593
|
+
from src.config import get_config_path, load_config
|
|
594
|
+
|
|
595
|
+
if not get_config_path().exists():
|
|
596
|
+
script_name = Path(sys.argv[0]).name or "RemarkableSync.py"
|
|
597
|
+
click.echo("[ERROR] No configuration found.", err=True)
|
|
598
|
+
click.echo(f"Run: python {script_name} config", err=True)
|
|
599
|
+
sys.exit(1)
|
|
594
600
|
|
|
595
601
|
cfg = load_config()
|
|
596
602
|
actions = cfg.get("sync_actions", [])
|
|
@@ -638,4 +644,3 @@ def main():
|
|
|
638
644
|
|
|
639
645
|
if __name__ == "__main__":
|
|
640
646
|
main()
|
|
641
|
-
|
|
@@ -48,15 +48,18 @@ src/ocr/ocr_engine.py
|
|
|
48
48
|
src/utils/__init__.py
|
|
49
49
|
src/utils/console.py
|
|
50
50
|
src/utils/logging.py
|
|
51
|
+
src/utils/name_registry.py
|
|
51
52
|
tests/__init__.py
|
|
52
53
|
tests/conftest.py
|
|
53
54
|
tests/generate_test_assets.py
|
|
54
55
|
tests/mock_ai_provider.py
|
|
55
56
|
tests/mock_connection.py
|
|
57
|
+
tests/test_agent_issues.py
|
|
56
58
|
tests/test_ai_providers.py
|
|
57
59
|
tests/test_auth_device_flow.py
|
|
58
60
|
tests/test_basic.py
|
|
59
61
|
tests/test_config.py
|
|
62
|
+
tests/test_config_command.py
|
|
60
63
|
tests/test_console.py
|
|
61
64
|
tests/test_hybrid_converter.py
|
|
62
65
|
tests/test_keyring_store.py
|
|
@@ -48,6 +48,8 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
48
48
|
host: str = "10.11.99.1",
|
|
49
49
|
use_wifi: bool = False,
|
|
50
50
|
wifi_host: str = "",
|
|
51
|
+
pre_sync_command: str = "",
|
|
52
|
+
post_sync_command: str = "",
|
|
51
53
|
):
|
|
52
54
|
"""Initialize backup orchestrator.
|
|
53
55
|
|
|
@@ -57,22 +59,24 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
57
59
|
host: Tablet IP/hostname (USB default: 10.11.99.1)
|
|
58
60
|
use_wifi: Connect via Wi-Fi instead of USB
|
|
59
61
|
wifi_host: Wi-Fi IP/hostname (auto-discovered if empty)
|
|
62
|
+
pre_sync_command: Shell command to run before SSH connects.
|
|
63
|
+
post_sync_command: Shell command to run after SSH disconnects.
|
|
60
64
|
"""
|
|
61
65
|
self.backup_dir = backup_dir
|
|
62
|
-
self.files_dir = backup_dir / "Notebooks"
|
|
63
|
-
self.templates_dir = backup_dir / "Templates"
|
|
66
|
+
self.files_dir = backup_dir / "Notebooks"
|
|
67
|
+
self.templates_dir = backup_dir / "Templates"
|
|
64
68
|
self.metadata_file = backup_dir / "sync_metadata.json"
|
|
65
69
|
|
|
66
|
-
# Create directories
|
|
67
70
|
self.files_dir.mkdir(parents=True, exist_ok=True)
|
|
68
71
|
self.templates_dir.mkdir(parents=True, exist_ok=True)
|
|
69
72
|
|
|
70
|
-
# Initialize components
|
|
71
73
|
self.connection = ReMarkableConnection(
|
|
72
74
|
password=password,
|
|
73
75
|
host=host,
|
|
74
76
|
use_wifi=use_wifi,
|
|
75
77
|
wifi_host=wifi_host,
|
|
78
|
+
pre_sync_command=pre_sync_command,
|
|
79
|
+
post_sync_command=post_sync_command,
|
|
76
80
|
)
|
|
77
81
|
self.metadata = FileMetadata(self.metadata_file)
|
|
78
82
|
|
|
@@ -173,23 +177,13 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
173
177
|
print(f" Found {len(allowed)} notebooks in selected folders")
|
|
174
178
|
return allowed
|
|
175
179
|
|
|
176
|
-
def
|
|
180
|
+
def _do_backup_files(
|
|
177
181
|
self,
|
|
178
182
|
) -> Tuple[bool, Set[str], Dict[str, Set[str]]]: # pylint: disable=too-many-branches
|
|
179
|
-
"""Backup files from ReMarkable tablet.
|
|
180
|
-
|
|
181
|
-
Returns:
|
|
182
|
-
Tuple of (success, set of notebook UUIDs that were updated,
|
|
183
|
-
dict mapping notebook UUID to set of changed page IDs)
|
|
184
|
-
"""
|
|
183
|
+
"""Backup files from ReMarkable tablet. Assumes connection is already open."""
|
|
185
184
|
logging.info("Starting file backup...")
|
|
186
|
-
print(" Connecting to tablet...")
|
|
187
|
-
|
|
188
|
-
if not self.connection.connect():
|
|
189
|
-
return False, set(), {}
|
|
190
185
|
|
|
191
186
|
try:
|
|
192
|
-
# Resolve folder filter before listing files
|
|
193
187
|
allowed_uuids = self._resolve_allowed_uuids()
|
|
194
188
|
|
|
195
189
|
# Get list of remote files
|
|
@@ -298,7 +292,7 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
298
292
|
progress.update(task, advance=1, description=local_path.name[:40])
|
|
299
293
|
|
|
300
294
|
except (OSError, SCPException) as e:
|
|
301
|
-
print_error(f"
|
|
295
|
+
print_error(f" ERR - Failed to download {remote_file['path']}: {e}")
|
|
302
296
|
progress.update(task, advance=1)
|
|
303
297
|
|
|
304
298
|
# Save metadata
|
|
@@ -316,23 +310,10 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
316
310
|
logging.error("Backup failed: %s", e)
|
|
317
311
|
return False, set(), {}
|
|
318
312
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def backup_templates(self) -> bool:
|
|
323
|
-
"""Backup template files from ReMarkable tablet.
|
|
324
|
-
|
|
325
|
-
Templates are stored in /usr/share/remarkable/templates/ and include
|
|
326
|
-
PNG/SVG template images and a templates.json configuration file.
|
|
327
|
-
|
|
328
|
-
Returns:
|
|
329
|
-
bool: True if successful, False otherwise
|
|
330
|
-
"""
|
|
313
|
+
def _do_backup_templates(self) -> bool:
|
|
314
|
+
"""Backup template files from ReMarkable tablet. Assumes connection is already open."""
|
|
331
315
|
logging.info("Starting template backup...")
|
|
332
316
|
|
|
333
|
-
if not self.connection.connect():
|
|
334
|
-
return False
|
|
335
|
-
|
|
336
317
|
try:
|
|
337
318
|
# Get list of template files
|
|
338
319
|
remote_files = self.connection.list_files(self.remote_templates_dir)
|
|
@@ -373,7 +354,7 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
373
354
|
self.metadata.update_file_metadata(remote_file, local_path)
|
|
374
355
|
|
|
375
356
|
except (OSError, SCPException) as e:
|
|
376
|
-
print_error(f"
|
|
357
|
+
print_error(f" ERR - Failed to download {remote_file['path']}: {e}")
|
|
377
358
|
|
|
378
359
|
progress.update(task, advance=1, description=local_path.name[:40])
|
|
379
360
|
|
|
@@ -387,9 +368,6 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
387
368
|
logging.error("Template backup failed: %s", e)
|
|
388
369
|
return False
|
|
389
370
|
|
|
390
|
-
finally:
|
|
391
|
-
self.connection.disconnect()
|
|
392
|
-
|
|
393
371
|
def find_notebooks(self) -> List[Dict]:
|
|
394
372
|
"""Find and parse notebook metadata.
|
|
395
373
|
|
|
@@ -465,7 +443,7 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
465
443
|
force_convert_all: bool = False,
|
|
466
444
|
convert_to_pdf: bool = False,
|
|
467
445
|
backup_templates: bool = True,
|
|
468
|
-
) -> bool:
|
|
446
|
+
) -> Tuple[bool, Set[str], Dict[str, Set[str]]]:
|
|
469
447
|
"""Run complete backup process with optional PDF conversion.
|
|
470
448
|
|
|
471
449
|
Args:
|
|
@@ -474,27 +452,34 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
474
452
|
backup_templates: If True, backup template files from the tablet (default: True)
|
|
475
453
|
|
|
476
454
|
Returns:
|
|
477
|
-
|
|
455
|
+
Tuple of (success, updated_notebook_uuids, updated_pages)
|
|
478
456
|
"""
|
|
479
457
|
logging.info("Starting ReMarkable backup process")
|
|
480
458
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if not success:
|
|
484
|
-
return False
|
|
459
|
+
if not self.connection.connect():
|
|
460
|
+
return False, set(), {}
|
|
485
461
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
462
|
+
updated_notebook_uuids: Set[str] = set()
|
|
463
|
+
updated_pages: Dict[str, Set[str]] = {}
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
success, updated_notebook_uuids, updated_pages = self._do_backup_files()
|
|
467
|
+
if not success:
|
|
468
|
+
return False, set(), {}
|
|
469
|
+
|
|
470
|
+
if backup_templates:
|
|
471
|
+
templates_success = self._do_backup_templates()
|
|
472
|
+
if not templates_success:
|
|
473
|
+
logging.warning("Template backup failed, but continuing with main backup")
|
|
474
|
+
finally:
|
|
475
|
+
self.connection.disconnect()
|
|
491
476
|
|
|
492
|
-
# Automatic PDF conversion using hybrid converter
|
|
493
477
|
if convert_to_pdf:
|
|
494
|
-
|
|
478
|
+
ok = self.run_pdf_conversion(updated_notebook_uuids, force_convert_all, updated_pages)
|
|
479
|
+
return ok, updated_notebook_uuids, updated_pages
|
|
495
480
|
|
|
496
481
|
logging.info("Backup process completed successfully")
|
|
497
|
-
return True
|
|
482
|
+
return True, updated_notebook_uuids, updated_pages
|
|
498
483
|
|
|
499
484
|
def run_pdf_conversion(
|
|
500
485
|
self,
|
|
@@ -527,34 +512,15 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
527
512
|
output_dir = self.backup_dir / "PDF"
|
|
528
513
|
logging.warning("No pdf_dir configured, falling back to %s", output_dir)
|
|
529
514
|
|
|
530
|
-
|
|
531
|
-
updated_only_file = None
|
|
532
|
-
if force_convert_all:
|
|
533
|
-
logging.info("Force conversion enabled - converting all notebooks to PDF")
|
|
534
|
-
elif updated_notebook_uuids:
|
|
535
|
-
# Create a temporary file list of updated notebooks for selective conversion
|
|
536
|
-
updated_list_file = self.backup_dir / "updated_notebooks.txt"
|
|
537
|
-
try:
|
|
538
|
-
with open(updated_list_file, "w", encoding="utf-8") as f:
|
|
539
|
-
for uuid in sorted(updated_notebook_uuids):
|
|
540
|
-
f.write(f"{uuid}\n")
|
|
541
|
-
|
|
542
|
-
updated_only_file = updated_list_file
|
|
543
|
-
logging.info("Converting %d updated notebooks to PDF", len(updated_notebook_uuids))
|
|
544
|
-
except OSError as e:
|
|
545
|
-
logging.error("Failed to create updated notebooks list: %s", e)
|
|
546
|
-
return False
|
|
547
|
-
else:
|
|
515
|
+
if not updated_notebook_uuids and not force_convert_all:
|
|
548
516
|
logging.info("No notebooks were updated - skipping PDF conversion")
|
|
549
517
|
return True
|
|
550
518
|
|
|
551
|
-
# Load folder filter from config
|
|
552
519
|
from ..config import load_config
|
|
553
520
|
|
|
554
521
|
config = load_config()
|
|
555
522
|
folder_filter = config.get("folders", []) or None
|
|
556
523
|
|
|
557
|
-
# Run conversion
|
|
558
524
|
try:
|
|
559
525
|
success, _converted = run_conversion(
|
|
560
526
|
backup_dir=self.backup_dir,
|
|
@@ -562,18 +528,11 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
562
528
|
verbose="INF",
|
|
563
529
|
sample=None,
|
|
564
530
|
notebook_filter=None,
|
|
565
|
-
|
|
531
|
+
updated_uuids=updated_notebook_uuids if not force_convert_all else None,
|
|
566
532
|
updated_pages=updated_pages,
|
|
567
533
|
folder_filter=folder_filter,
|
|
568
534
|
)
|
|
569
535
|
|
|
570
|
-
# Clean up temporary file if created
|
|
571
|
-
if updated_only_file and updated_only_file.exists():
|
|
572
|
-
try:
|
|
573
|
-
updated_only_file.unlink()
|
|
574
|
-
except OSError:
|
|
575
|
-
pass # Ignore cleanup errors
|
|
576
|
-
|
|
577
536
|
if success:
|
|
578
537
|
logging.info("PDF conversion completed successfully")
|
|
579
538
|
else:
|
|
@@ -19,6 +19,14 @@ import click
|
|
|
19
19
|
import paramiko
|
|
20
20
|
from scp import SCPClient
|
|
21
21
|
|
|
22
|
+
from ..utils.console import print_error, print_success, print_warn
|
|
23
|
+
|
|
24
|
+
# Suppress paramiko noise regardless of when setup_logging is called
|
|
25
|
+
for _n in ("paramiko", "paramiko.transport", "paramiko.auth", "paramiko.channel"):
|
|
26
|
+
_l = logging.getLogger(_n)
|
|
27
|
+
_l.setLevel(logging.CRITICAL)
|
|
28
|
+
_l.propagate = False
|
|
29
|
+
|
|
22
30
|
try:
|
|
23
31
|
import keyring # type: ignore
|
|
24
32
|
|
|
@@ -76,6 +84,8 @@ class ReMarkableConnection:
|
|
|
76
84
|
password: str | None = None,
|
|
77
85
|
use_wifi: bool = False,
|
|
78
86
|
wifi_host: str = "",
|
|
87
|
+
pre_sync_command: str = "",
|
|
88
|
+
post_sync_command: str = "",
|
|
79
89
|
):
|
|
80
90
|
"""Initialize connection parameters.
|
|
81
91
|
|
|
@@ -89,6 +99,8 @@ class ReMarkableConnection:
|
|
|
89
99
|
Falls back to USB if *wifi_host* is empty.
|
|
90
100
|
wifi_host: IP address or hostname of the tablet on the local
|
|
91
101
|
network. Ignored when *use_wifi* is False.
|
|
102
|
+
pre_sync_command: Shell command to run before SSH connects.
|
|
103
|
+
post_sync_command: Shell command to run after SSH disconnects.
|
|
92
104
|
"""
|
|
93
105
|
# Resolve effective host
|
|
94
106
|
if use_wifi:
|
|
@@ -107,6 +119,8 @@ class ReMarkableConnection:
|
|
|
107
119
|
self.scp_client = None
|
|
108
120
|
self.password = password
|
|
109
121
|
self.password_saved = False
|
|
122
|
+
self.pre_sync_command = pre_sync_command.strip()
|
|
123
|
+
self.post_sync_command = post_sync_command.strip()
|
|
110
124
|
|
|
111
125
|
def get_saved_password(self) -> str | None:
|
|
112
126
|
"""Get saved password from system keyring.
|
|
@@ -188,13 +202,22 @@ class ReMarkableConnection:
|
|
|
188
202
|
def connect(self) -> bool:
|
|
189
203
|
"""Establish SSH connection to ReMarkable tablet.
|
|
190
204
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
Handles password retry logic if saved password fails.
|
|
205
|
+
Runs the pre-sync command (if configured) before opening the SSH
|
|
206
|
+
connection, and the post-sync command (if configured) in disconnect().
|
|
194
207
|
|
|
195
208
|
Returns:
|
|
196
209
|
bool: True if connection successful, False otherwise
|
|
197
210
|
"""
|
|
211
|
+
if self.pre_sync_command:
|
|
212
|
+
from ..utils import run_shell_command
|
|
213
|
+
|
|
214
|
+
print(f" Running pre-sync: {self.pre_sync_command}")
|
|
215
|
+
rc = run_shell_command(self.pre_sync_command)
|
|
216
|
+
if rc != 0:
|
|
217
|
+
print_error(f" ERR - Pre-sync command failed (exit {rc})")
|
|
218
|
+
return False
|
|
219
|
+
print_success(" OK - Pre-sync done")
|
|
220
|
+
|
|
198
221
|
max_password_retries = 3
|
|
199
222
|
password_attempt = 0
|
|
200
223
|
used_saved_password = False
|
|
@@ -246,7 +269,7 @@ class ReMarkableConnection:
|
|
|
246
269
|
logging.warning("Authentication failed on attempt %d: %s", i + 1, e)
|
|
247
270
|
# Authentication failed - might be wrong password
|
|
248
271
|
if used_saved_password:
|
|
249
|
-
|
|
272
|
+
print_warn(" WRN - Saved password appears to be incorrect.")
|
|
250
273
|
if click.confirm(
|
|
251
274
|
"Would you like to enter a new password?", default=True
|
|
252
275
|
):
|
|
@@ -263,13 +286,14 @@ class ReMarkableConnection:
|
|
|
263
286
|
else:
|
|
264
287
|
return False
|
|
265
288
|
else:
|
|
266
|
-
|
|
267
|
-
|
|
289
|
+
print_error(
|
|
290
|
+
" ERR - Authentication failed. Please check your password."
|
|
291
|
+
)
|
|
268
292
|
self.password = None
|
|
269
293
|
password_attempt += 1
|
|
270
294
|
break
|
|
271
295
|
except (paramiko.SSHException, OSError) as e:
|
|
272
|
-
logging.
|
|
296
|
+
logging.debug("Connection attempt %d failed: %s", i + 1, e)
|
|
273
297
|
if self.ssh_client:
|
|
274
298
|
try:
|
|
275
299
|
self.ssh_client.close()
|
|
@@ -278,28 +302,44 @@ class ReMarkableConnection:
|
|
|
278
302
|
self.ssh_client = paramiko.SSHClient()
|
|
279
303
|
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
280
304
|
|
|
281
|
-
logging.
|
|
305
|
+
logging.debug("All connection attempts failed")
|
|
306
|
+
|
|
307
|
+
print_error(
|
|
308
|
+
f" ERR - Connection to {self.host} failed. "
|
|
309
|
+
"Check that the tablet is connected and try again."
|
|
310
|
+
)
|
|
282
311
|
return False
|
|
283
312
|
|
|
284
313
|
except (paramiko.SSHException, OSError) as e:
|
|
285
|
-
logging.
|
|
314
|
+
logging.debug("Failed to connect to ReMarkable: %s", e)
|
|
315
|
+
print_error(
|
|
316
|
+
f" ERR - Connection to {self.host} failed. "
|
|
317
|
+
"Check that the tablet is connected and try again."
|
|
318
|
+
)
|
|
286
319
|
return False
|
|
287
320
|
|
|
288
|
-
|
|
321
|
+
print_error(" ERR - Maximum password retry attempts reached.")
|
|
289
322
|
return False
|
|
290
323
|
|
|
291
324
|
def disconnect(self):
|
|
292
|
-
"""Close SSH and SCP connections to ReMarkable tablet.
|
|
293
|
-
|
|
294
|
-
Safely closes both SCP and SSH client connections,
|
|
295
|
-
ensuring clean disconnection from the tablet.
|
|
296
|
-
"""
|
|
325
|
+
"""Close SSH and SCP connections to ReMarkable tablet."""
|
|
326
|
+
print(" Disconnecting...")
|
|
297
327
|
if self.scp_client:
|
|
298
328
|
self.scp_client.close()
|
|
299
329
|
if self.ssh_client:
|
|
300
330
|
self.ssh_client.close()
|
|
301
331
|
logging.info("Disconnected from ReMarkable tablet")
|
|
302
332
|
|
|
333
|
+
if self.post_sync_command:
|
|
334
|
+
from ..utils import run_shell_command
|
|
335
|
+
|
|
336
|
+
print(f" Running post-sync: {self.post_sync_command}")
|
|
337
|
+
rc = run_shell_command(self.post_sync_command)
|
|
338
|
+
if rc != 0:
|
|
339
|
+
print_error(f" ERR - Post-sync command failed (exit {rc})")
|
|
340
|
+
else:
|
|
341
|
+
print_success(" OK - Post-sync done")
|
|
342
|
+
|
|
303
343
|
def execute_command(self, command: str) -> Tuple[str, str, int]:
|
|
304
344
|
"""Execute command on ReMarkable tablet via SSH.
|
|
305
345
|
|
|
@@ -34,7 +34,8 @@ def run_backup_command(
|
|
|
34
34
|
Returns:
|
|
35
35
|
Exit code (0 for success, 1 for failure)
|
|
36
36
|
"""
|
|
37
|
-
|
|
37
|
+
log_dir = backup_dir.parent
|
|
38
|
+
setup_logging(log_level, log_dir=log_dir)
|
|
38
39
|
|
|
39
40
|
print("ReMarkable Tablet Backup")
|
|
40
41
|
print("=" * 70)
|