1
0

Improved robustness of FUSE
Some checks failed
Test Python / Test (push) Failing after 26s

This commit is contained in:
Jon Michael Aanes 2024-09-30 13:57:34 +02:00
parent 412a20b006
commit c09245bd1c
4 changed files with 70 additions and 39 deletions

View File

@ -15,16 +15,29 @@ your organization's payment plan.
Uses [`python-fuse`](https://github.com/libfuse/python-fuse) library. 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: Limitations:
- Only cards in todolist is fetched at the moment. - Only cards in todolist is fetched at the moment.
- Doesn't include title anywhere. - Tasks (checklists on cards) cannot be updated or changed.
- Tasks cannot be updated or changed. - Images cannot be updated or changed.
- Slow, due to inefficient use of caches. - You cannot create new cards, nor any other files.
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...)
## Usage ## Usage
@ -35,23 +48,27 @@ name, description, tasks, etc...)
[`python-fuse`](https://github.com/libfuse/python-fuse) implements a whole [`python-fuse`](https://github.com/libfuse/python-fuse) implements a whole
bunch automatically.) bunch automatically.)
## Architecture
## Dependencies - `FavroFuse`
- Markdown Parser/Renderer
- `FavroClient`
- `CardCache`
All requirements can be installed easily using: ## Work in Progress
```bash Following features are work in progress:
pip install -r requirements.txt
```
Full list of requirements: - [ ] Frontmatter: Update Tags
- [requests](https://pypi.org/project/requests/) - [ ] Frontmatter: Updated assigned members
- [requests-cache](https://pypi.org/project/requests-cache/) - [ ] Frontmatter: Arbitrary structured data? Read-only.
- [fuse-python](https://pypi.org/project/fuse-python/) - [ ] Frontmatter: Dependencies. As vault links in Obsidian mode.
- [secret_loader](https://gitfub.space/Jmaa/secret_loader) - [ ] 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 MIT License

View File

@ -36,28 +36,28 @@ CARD_FILENAME_REGEX = r'^\/PAR\-(\d+)\.md$'
OFFICIAL_URL='https://favro.com/organization/{org_id}?card=par-{seq_id}' OFFICIAL_URL='https://favro.com/organization/{org_id}?card=par-{seq_id}'
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class Thing: class FileSystemItem:
@staticmethod @staticmethod
def from_path(path_str: str) -> 'Thing | None': def from_path(path_str: str) -> 'FileSystemItem | None':
if path_str == '/': if path_str == '/':
return RootThing() return RootFileSystemItem()
if m := re.match(CARD_FILENAME_REGEX, path_str): 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 return None
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class RootThing(Thing): class RootFileSystemItem(FileSystemItem):
pass pass
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class CardThing(Thing): class CardFileSystemItem(FileSystemItem):
seq_id: SeqId seq_id: SeqId
class FavroFuse(fuse.Fuse): class FavroFuse(fuse.Fuse):
"""Favro Filesystem in Userspace.""" """Favro FileSystem in Userspace."""
def __init__( def __init__(
self, self,
@ -67,16 +67,17 @@ class FavroFuse(fuse.Fuse):
): ):
self.favro_client = favro_client self.favro_client = favro_client
self.formatter = formatter self.formatter = formatter
self.wiped_cards = set()
super().__init__(**kwargs) super().__init__(**kwargs)
def getattr(self, path: str) -> FavroStat | int: def getattr(self, path: str) -> FavroStat | int:
thing = Thing.from_path(path) thing = FileSystemItem.from_path(path)
st = FavroStat() st = FavroStat()
if isinstance(thing, RootThing): if isinstance(thing, RootFileSystemItem):
st.st_mode = stat.S_IFDIR | 0o755 st.st_mode = stat.S_IFDIR | 0o755
st.st_nlink = 2 st.st_nlink = 2
elif isinstance(thing, CardThing): elif isinstance(thing, CardFileSystemItem):
card = self.favro_client.get_card(thing.seq_id) card = self.favro_client.get_card(thing.seq_id)
st.st_mode = stat.S_IFREG | 0o666 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)) yield fuse.Direntry(CARD_FILENAME_FORMAT.format(seq_id=card.seq_id.raw_id))
def open(self, path: str, flags: int) -> int | None: def open(self, path: str, flags: int) -> int | None:
thing = Thing.from_path(path) thing = FileSystemItem.from_path(path)
if not isinstance(thing, CardThing): if not isinstance(thing, CardFileSystemItem):
return -errno.ENOENT return -errno.ENOENT
return None return None
def read(self, path: str, size: int, offset: int) -> bytes | int: def read(self, path: str, size: int, offset: int) -> bytes | int:
# Check that this is a card thing. # Check that this is a card thing.
thing = Thing.from_path(path) thing = FileSystemItem.from_path(path)
if not isinstance(thing, CardThing): if not isinstance(thing, CardFileSystemItem):
return -errno.ENOENT return -errno.ENOENT
card = self.favro_client.get_card(thing.seq_id) card = self.favro_client.get_card(thing.seq_id)
contents_str = self._format_card_file(card) contents_str = self._format_card_file(card)
contents = bytes(contents_str, 'utf8') contents = bytes(contents_str, 'utf8')
@ -124,8 +126,8 @@ class FavroFuse(fuse.Fuse):
def write(self, path: str, written_buffer: bytes, offset: int) -> int: def write(self, path: str, written_buffer: bytes, offset: int) -> int:
# Check that this is a card thing. # Check that this is a card thing.
thing = Thing.from_path(path) thing = FileSystemItem.from_path(path)
if not isinstance(thing, CardThing): if not isinstance(thing, CardFileSystemItem):
return -errno.ENOENT return -errno.ENOENT
card = self.favro_client.get_card(thing.seq_id) 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) card_updated = self.formatter.parse_card_contents(contents_str)
self.favro_client.update_card_contents(card.card_id, card_updated) self.favro_client.update_card_contents(card.card_id, card_updated)
self.wiped_cards.remove(thing.seq_id)
# Return amount written # Return amount written
return len(written_buffer) return len(written_buffer)
def truncate(self, path: str, new_size: int): def truncate(self, path: str, new_size: int):
# Check that this is a card thing. # Check that this is a card thing.
thing = Thing.from_path(path) thing = FileSystemItem.from_path(path)
if not isinstance(thing, CardThing): if not isinstance(thing, CardFileSystemItem):
return -errno.ENOENT return -errno.ENOENT
card = self.favro_client.get_card(thing.seq_id) 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) card_updated = self.formatter.parse_card_contents(contents_str)
self.favro_client.update_card_contents_locally(card.card_id, card_updated) self.favro_client.update_card_contents_locally(card.card_id, card_updated)
self.wiped_cards.add(thing.seq_id)
# Return amount written # Return amount written
return 0 return 0
def _format_card_file(self, card: Card) -> str: 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] tags = [self.favro_client.get_tag(tag_id).name for tag_id in card.tags]
assignments = [ assignments = [
self.favro_client.get_user(assignment.user).name self.favro_client.get_user(assignment.user).name

View File

@ -39,8 +39,10 @@ class CardFileFormatter:
frontmatter_data = {} frontmatter_data = {}
if card.name and self.obsidian_mode: if card.name and self.obsidian_mode:
frontmatter_data['aliases'] = [card.name] frontmatter_data['aliases'] = [card.name]
frontmatter_data['tags'] = card.tags if card.tags:
frontmatter_data['url'] = card.url frontmatter_data['tags'] = card.tags
if card.url and self.obsidian_mode:
frontmatter_data['url'] = card.url
if card.assignments: if card.assignments:
frontmatter_data['assignments'] = card.assignments frontmatter_data['assignments'] = card.assignments
if self.obsidian_mode: if self.obsidian_mode:
@ -94,6 +96,8 @@ class CardFileFormatter:
card_dependencies: list[str] = fm.metadata.get('dependencies', []) card_dependencies: list[str] = fm.metadata.get('dependencies', [])
card_dependencies = [parse_obsidian_link(text) for text in card_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() description = self.renderer.render_children(document).strip()
return CardContents( return CardContents(
name, name,
@ -101,5 +105,5 @@ class CardFileFormatter:
tags=tags, tags=tags,
assignments=assignments, assignments=assignments,
card_dependencies=card_dependencies, card_dependencies=card_dependencies,
url='', # TODO url=url,
) )

View File

@ -40,6 +40,7 @@ EXAMPLE_TEXT_3 = """
--- ---
aliases: aliases:
- 'Card: The Adventure of Card' - 'Card: The Adventure of Card'
url: https://example.org
--- ---
# Card: The Adventure of Card # Card: The Adventure of Card