diff --git a/README.md b/README.md index 90c79c9..a601698 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Equally, the only thing flatnotes caches is the search index and that's incremen * Light/dark themes. * Multiple authentication options (none, read-only, username/password, 2FA). * Restful API. +* Hook on create/update/delete. See [the wiki](https://github.com/dullage/flatnotes/wiki) for more details. @@ -118,3 +119,32 @@ A special thanks to 2 fantastic open-source projects that make flatnotes possibl * [Whoosh](https://whoosh.readthedocs.io/en/latest/intro.html) - A fast, pure Python search engine library. * [TOAST UI Editor](https://ui.toast.com/tui-editor) - A GFM Markdown and WYSIWYG editor for the browser. + +## Contributing + +Requirements: + +- python3, node, npm, pipenv + +### Local run + + +1. Install deps: + + pipenv install + npm install + +3. Build UI + + npm run build + +4. Switch to virtual environment + + pipenv shell + mkdir notes # for local test, it's git-ignored + +5. Run local server + + FLATNOTES_PATH=notes FLATNOTES_AUTH_TYPE=none uvicorn --app-dir server main:app --port 8080 + +Dev instance will be available on http://127.0.0.1:8080 \ No newline at end of file diff --git a/server/notes/file_system/file_system.py b/server/notes/file_system/file_system.py index a463b41..8a9781b 100644 --- a/server/notes/file_system/file_system.py +++ b/server/notes/file_system/file_system.py @@ -3,6 +3,7 @@ import re import shutil import time +import subprocess from datetime import datetime from typing import List, Literal, Set, Tuple @@ -58,6 +59,7 @@ def create(self, data: NoteCreate) -> Note: """Create a new note.""" filepath = self._path_from_title(data.title) self._write_file(filepath, data.content) + self._exec_hook("create", filepath) return Note( title=data.title, content=data.content, @@ -93,6 +95,8 @@ def update(self, title: str, data: NoteUpdate) -> Note: content = data.new_content else: content = self._read_file(filepath) + + self._exec_hook("update", filepath) return Note( title=title, content=content, @@ -104,6 +108,7 @@ def delete(self, title: str) -> None: is_valid_filename(title) filepath = self._path_from_title(title) os.remove(filepath) + self._exec_hook("delete", filepath) def search( self, @@ -372,6 +377,22 @@ def _fieldnames_for_term(self, term: str) -> List[str]: fields.append("tags") return fields + def _exec_hook(self, hook_name: Literal["create", "update", "delete"], path: str): + """Execute shell script for provided hook. + The hook script will be from FLATNOTES_HOOK_, where name is a hook_name in upper case. + If there is no such env, hook will be ignored. Workdir is the storage path. + Changed path will be substituted from %s. + """ + hook_env_name = f"FLATNOTES_HOOK_{hook_name.strip().upper()}" + script = os.getenv(hook_env_name) + if not script: + return + subprocess.check_call( + script.replace("%s", path), + shell=True, + cwd=self.storage_path, + ) + @staticmethod def _get_matched_fields(matched_terms): """Return a set of matched fields from a set of ('field', 'term') "