From c09245bd1c6a8439d199bd075e5172c1f2302b14 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Mon, 30 Sep 2024 13:57:34 +0200 Subject: [PATCH] Improved robustness of FUSE --- README.md | 53 +++++++++++++++++++++++------------ favro_sync/favro_fuse.py | 45 +++++++++++++++++------------ favro_sync/favro_markdown.py | 10 +++++-- test/test_markdown_parsing.py | 1 + 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 53d3893..5d8664c 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,29 @@ your organization's payment plan. Uses [`python-fuse`](https://github.com/libfuse/python-fuse) library. +Features: + +- Local access to cards in todolist. +- Read card features: + - Title + - Description + - Tags + - Assignees + - Dependencies +- Change card features: + - Title + - Description +- [Obsidian](https://obsidian.md/) compatibility: + - Mountable within your vault. + - Link to cards by either card number or card title. + - Tags and dependencies are integrated. + Limitations: - Only cards in todolist is fetched at the moment. -- Doesn't include title anywhere. -- Tasks cannot be updated or changed. -- Slow, due to inefficient use of caches. - -A more complete implementation will probably require a Markdown parser, to -parse the saved input, and distribute it across the various Card fields (card -name, description, tasks, etc...) +- Tasks (checklists on cards) cannot be updated or changed. +- Images cannot be updated or changed. +- You cannot create new cards, nor any other files. ## Usage @@ -35,23 +48,27 @@ name, description, tasks, etc...) [`python-fuse`](https://github.com/libfuse/python-fuse) implements a whole bunch automatically.) +## Architecture -## Dependencies +- `FavroFuse` +- Markdown Parser/Renderer +- `FavroClient` + - `CardCache` -All requirements can be installed easily using: +## Work in Progress -```bash -pip install -r requirements.txt -``` +Following features are work in progress: -Full list of requirements: -- [requests](https://pypi.org/project/requests/) -- [requests-cache](https://pypi.org/project/requests-cache/) -- [fuse-python](https://pypi.org/project/fuse-python/) -- [secret_loader](https://gitfub.space/Jmaa/secret_loader) +- [ ] Frontmatter: Update Tags +- [ ] Frontmatter: Updated assigned members +- [ ] Frontmatter: Arbitrary structured data? Read-only. +- [ ] Frontmatter: Dependencies. As vault links in Obsidian mode. +- [ ] Allow users to toggle Obsidian mode, instead of being default. +- [ ] Get the correct last-modified date. +- [ ] Improve cache behaviour. User and tags can have much longer cache times. -## License +# License ``` MIT License diff --git a/favro_sync/favro_fuse.py b/favro_sync/favro_fuse.py index 6d3a6e3..77fd2fa 100644 --- a/favro_sync/favro_fuse.py +++ b/favro_sync/favro_fuse.py @@ -36,28 +36,28 @@ CARD_FILENAME_REGEX = r'^\/PAR\-(\d+)\.md$' OFFICIAL_URL='https://favro.com/organization/{org_id}?card=par-{seq_id}' @dataclasses.dataclass(frozen=True) -class Thing: +class FileSystemItem: @staticmethod - def from_path(path_str: str) -> 'Thing | None': + def from_path(path_str: str) -> 'FileSystemItem | None': if path_str == '/': - return RootThing() + return RootFileSystemItem() if m := re.match(CARD_FILENAME_REGEX, path_str): - return CardThing(SeqId(int(m.group(1)))) + return CardFileSystemItem(SeqId(int(m.group(1)))) return None @dataclasses.dataclass(frozen=True) -class RootThing(Thing): +class RootFileSystemItem(FileSystemItem): pass @dataclasses.dataclass(frozen=True) -class CardThing(Thing): +class CardFileSystemItem(FileSystemItem): seq_id: SeqId class FavroFuse(fuse.Fuse): - """Favro Filesystem in Userspace.""" + """Favro FileSystem in Userspace.""" def __init__( self, @@ -67,16 +67,17 @@ class FavroFuse(fuse.Fuse): ): self.favro_client = favro_client self.formatter = formatter + self.wiped_cards = set() super().__init__(**kwargs) def getattr(self, path: str) -> FavroStat | int: - thing = Thing.from_path(path) + thing = FileSystemItem.from_path(path) st = FavroStat() - if isinstance(thing, RootThing): + if isinstance(thing, RootFileSystemItem): st.st_mode = stat.S_IFDIR | 0o755 st.st_nlink = 2 - elif isinstance(thing, CardThing): + elif isinstance(thing, CardFileSystemItem): card = self.favro_client.get_card(thing.seq_id) st.st_mode = stat.S_IFREG | 0o666 @@ -97,19 +98,20 @@ class FavroFuse(fuse.Fuse): yield fuse.Direntry(CARD_FILENAME_FORMAT.format(seq_id=card.seq_id.raw_id)) def open(self, path: str, flags: int) -> int | None: - thing = Thing.from_path(path) - if not isinstance(thing, CardThing): + thing = FileSystemItem.from_path(path) + if not isinstance(thing, CardFileSystemItem): return -errno.ENOENT return None def read(self, path: str, size: int, offset: int) -> bytes | int: # Check that this is a card thing. - thing = Thing.from_path(path) - if not isinstance(thing, CardThing): + thing = FileSystemItem.from_path(path) + if not isinstance(thing, CardFileSystemItem): return -errno.ENOENT card = self.favro_client.get_card(thing.seq_id) + contents_str = self._format_card_file(card) contents = bytes(contents_str, 'utf8') @@ -124,8 +126,8 @@ class FavroFuse(fuse.Fuse): def write(self, path: str, written_buffer: bytes, offset: int) -> int: # Check that this is a card thing. - thing = Thing.from_path(path) - if not isinstance(thing, CardThing): + thing = FileSystemItem.from_path(path) + if not isinstance(thing, CardFileSystemItem): return -errno.ENOENT card = self.favro_client.get_card(thing.seq_id) @@ -140,13 +142,15 @@ class FavroFuse(fuse.Fuse): card_updated = self.formatter.parse_card_contents(contents_str) self.favro_client.update_card_contents(card.card_id, card_updated) + self.wiped_cards.remove(thing.seq_id) + # Return amount written return len(written_buffer) def truncate(self, path: str, new_size: int): # Check that this is a card thing. - thing = Thing.from_path(path) - if not isinstance(thing, CardThing): + thing = FileSystemItem.from_path(path) + if not isinstance(thing, CardFileSystemItem): return -errno.ENOENT card = self.favro_client.get_card(thing.seq_id) @@ -163,10 +167,15 @@ class FavroFuse(fuse.Fuse): card_updated = self.formatter.parse_card_contents(contents_str) self.favro_client.update_card_contents_locally(card.card_id, card_updated) + self.wiped_cards.add(thing.seq_id) + # Return amount written return 0 def _format_card_file(self, card: Card) -> str: + if card.seq_id in self.wiped_cards: + return '' + tags = [self.favro_client.get_tag(tag_id).name for tag_id in card.tags] assignments = [ self.favro_client.get_user(assignment.user).name diff --git a/favro_sync/favro_markdown.py b/favro_sync/favro_markdown.py index 5e9161c..df6a406 100644 --- a/favro_sync/favro_markdown.py +++ b/favro_sync/favro_markdown.py @@ -39,8 +39,10 @@ class CardFileFormatter: frontmatter_data = {} if card.name and self.obsidian_mode: frontmatter_data['aliases'] = [card.name] - frontmatter_data['tags'] = card.tags - frontmatter_data['url'] = card.url + if card.tags: + frontmatter_data['tags'] = card.tags + if card.url and self.obsidian_mode: + frontmatter_data['url'] = card.url if card.assignments: frontmatter_data['assignments'] = card.assignments if self.obsidian_mode: @@ -94,6 +96,8 @@ class CardFileFormatter: card_dependencies: list[str] = fm.metadata.get('dependencies', []) card_dependencies = [parse_obsidian_link(text) for text in card_dependencies] + url: list[str] = fm.metadata.get('url') + description = self.renderer.render_children(document).strip() return CardContents( name, @@ -101,5 +105,5 @@ class CardFileFormatter: tags=tags, assignments=assignments, card_dependencies=card_dependencies, - url='', # TODO + url=url, ) diff --git a/test/test_markdown_parsing.py b/test/test_markdown_parsing.py index 16d6fa2..84bd752 100644 --- a/test/test_markdown_parsing.py +++ b/test/test_markdown_parsing.py @@ -40,6 +40,7 @@ EXAMPLE_TEXT_3 = """ --- aliases: - 'Card: The Adventure of Card' +url: https://example.org --- # Card: The Adventure of Card