diff --git a/halig/commands/base.py b/halig/commands/base.py index d33dbf4..26db02c 100644 --- a/halig/commands/base.py +++ b/halig/commands/base.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from collections.abc import Callable from halig.settings import Settings @@ -14,5 +13,5 @@ class BaseCommand(ICommand): def __init__(self, settings: Settings, *args, **kwargs): self.settings = settings - def traverse_notebooks(self, callback_on_item: Callable): - """Traverse root_path""" + def traverse(self): + return self.settings.notebooks_root_path.glob("./**/*.age") diff --git a/halig/commands/reencrypt.py b/halig/commands/reencrypt.py new file mode 100644 index 0000000..328b522 --- /dev/null +++ b/halig/commands/reencrypt.py @@ -0,0 +1,17 @@ +from halig.commands.base import BaseCommand +from halig.encryption import Encryptor +from halig.settings import Settings + + +class ReencryptCommand(BaseCommand): + def __init__(self, settings: Settings, *args, **kwargs): + super().__init__(settings, *args, **kwargs) + self.encryptor = Encryptor(settings) + + def run(self): + for note_path in self.traverse(): + with note_path.open("rb") as fr: + orig_data = self.encryptor.decrypt(fr.read()) + new_data = self.encryptor.encrypt(orig_data) + with note_path.open("wb") as fw: + fw.write(new_data) diff --git a/halig/commands/search.py b/halig/commands/search.py index d62d397..1fff248 100644 --- a/halig/commands/search.py +++ b/halig/commands/search.py @@ -76,7 +76,7 @@ class SearchCommand(BaseCommand): ) def _index_notebooks(self): - for note_path in self.settings.notebooks_root_path.glob("./**/*.age"): + for note_path in self.traverse(): updated_at = note_path.stat().st_mtime with note_path.open("rb") as f: body = self.encryptor.decrypt(f.read()) diff --git a/halig/encryption.py b/halig/encryption.py index a5faa73..1801233 100644 --- a/halig/encryption.py +++ b/halig/encryption.py @@ -35,4 +35,6 @@ class Encryptor: return rage_encrypt(data, self.recipients) # type: ignore[no-any-return] def decrypt(self, data: bytes) -> bytes: + if not len(data): + return data return rage_decrypt(data, self.identities) # type: ignore[no-any-return] diff --git a/halig/literals.py b/halig/literals.py index 93e944a..5942d17 100644 --- a/halig/literals.py +++ b/halig/literals.py @@ -4,6 +4,12 @@ COMMANDS_EDIT_HELP = "Edit or add a note into a notebook" COMMANDS_SHOW_HELP = "Show a note's contents" COMMANDS_VERSION = "Show halig's version" COMMANDS_IMPORT_HELP = "Encrypt existing unencrypted files" +COMMANDS_SEARCH_HELP = """Perform a full-text search against all your notes, +which are indexed into a SQLite FTS5 database located at `~/.cache/halig/halig.db` +""" +COMMANDS_REENCRYPT_HELP = """Reencrypt all available notes. This operation is useful +when new public keys have been added to the config file and you want the notes +to be seen by the new pairing private keys""" # OPTIONS OPTION_CONFIG_HELP = "Configuration file. Must be YAML and schema compatible" @@ -13,6 +19,10 @@ OPTION_LEVEL_HELP = ( OPTION_UNLINK_HELP = """Setting this will remove the original markdown files; only the newly encrypted .age files will be preserved. Backup your data first """ +OPTION_INDEX_HELP = """Index the SQLite database with your notes contents. The first +time you perform a search, this flag should be set. Afterwards, you should only index +when new notes have been added or older ones have been changed, since it's a slow +operation""" # ARGUMENTS ARGUMENT_EDIT_NOTE_HELP = """A valid, settings-relative path. Be aware that valid can also mean implicit notes, that is, pointing to a diff --git a/halig/main.py b/halig/main.py index b5061a6..15a13a6 100644 --- a/halig/main.py +++ b/halig/main.py @@ -11,6 +11,7 @@ from halig.__version__ import __version__ from halig.commands.edit import EditCommand from halig.commands.import_unencrypted import ImportCommand from halig.commands.notebooks import NotebooksCommand +from halig.commands.reencrypt import ReencryptCommand from halig.commands.search import SearchCommand from halig.commands.show import ShowCommand from halig.settings import load_from_file @@ -115,10 +116,10 @@ def import_unencrypted( command.run() -@app.command() +@app.command(help=literals.COMMANDS_SEARCH_HELP) def search( term: str, - index: bool = False, + index: bool = Option(False, help=literals.OPTION_INDEX_HELP), # noqa: B008 ): settings = load_from_file() command = SearchCommand( @@ -129,6 +130,15 @@ def search( command.run() +@app.command(help=literals.COMMANDS_REENCRYPT_HELP) +def reencrypt(): + settings = load_from_file() + command = ReencryptCommand( + settings=settings, + ) + command.run() + + @app.command(help=literals.COMMANDS_VERSION) @capture def version(): diff --git a/tests/commands/test_reencrypt.py b/tests/commands/test_reencrypt.py new file mode 100644 index 0000000..44c76c4 --- /dev/null +++ b/tests/commands/test_reencrypt.py @@ -0,0 +1,16 @@ +import pytest + +from halig.commands.reencrypt import ReencryptCommand + + +@pytest.fixture() +def reencrypt_command(settings): + return ReencryptCommand(settings) + + +@pytest.mark.usefixtures('notes') +def test_reencrypt(reencrypt_command): + reencrypt_command.run() + for note_path in reencrypt_command.traverse(): + with note_path.open("rb") as f: + assert reencrypt_command.encryptor.decrypt(f.read()) == b""