diff --git a/favro_sync/__init__.py b/favro_sync/__init__.py index d3f4071..66ca511 100644 --- a/favro_sync/__init__.py +++ b/favro_sync/__init__.py @@ -6,4 +6,6 @@ Synchronize your local notes and your Favro. Uses the [Favro API](https://favro.com/developer/). Rate limiting depends upon your organization's payment plan. + +Uses [`python-fuse`](https://github.com/libfuse/python-fuse) library. """ diff --git a/favro_sync/__main__.py b/favro_sync/__main__.py index 0aa000e..7eda2a6 100644 --- a/favro_sync/__main__.py +++ b/favro_sync/__main__.py @@ -12,7 +12,9 @@ def main(): favro_username = secrets.load_or_fail('FAVRO_USERNAME') favro_password = secrets.load_or_fail('FAVRO_PASSWORD') - with tempfile.TemporaryDirectory(prefix='favro_sync-') as tmpdirname: + #with tempfile.TemporaryDirectory(prefix='favro_sync_') as tmpdirname: + tmpdirname = './output' # TODO + if True: session = requests_cache.CachedSession(tmpdirname + '/http-cache.sqlite', expire_after=360) client = FavroClient(favro_org_id=OrganizationId(favro_org_id), diff --git a/favro_sync/favro_client.py b/favro_sync/favro_client.py index 29dacb7..a1536d9 100644 --- a/favro_sync/favro_client.py +++ b/favro_sync/favro_client.py @@ -46,11 +46,6 @@ class Card: detailed_description: str | None - # Derived - - seq_id_with_prefix: str - - ''' TODO, fieds: 'position': -399 'listPosition': -399 @@ -82,8 +77,6 @@ class Card: tags = json['tags'], creator_user_id = UserId(json['createdByUserId']), creation_date = datetime.datetime.fromisoformat(json['createdAt']), - - seq_id_with_prefix = PREFIX + str(json['sequentialId']), ) # Endpoints @@ -115,11 +108,11 @@ class FavroClient: yield from self.get_cards(todo_list=True) - def get_cards(self, *, seqid: SeqId | None = None, todo_list=True) -> Iterator[Card]: + def get_cards(self, *, seq_id: SeqId | None = None, todo_list=True) -> Iterator[Card]: # Determine params for get_cards params = {'descriptionFormat': 'markdown'} - if seqid: - params['cardSequentialId']= str(seqid.raw_id) + if seq_id: + params['cardSequentialId']= str(seq_id.raw_id) if todo_list: params['todoList'] = 'true' @@ -133,11 +126,11 @@ class FavroClient: yield Card.from_json(entity_json) del entity_json - def get_card(self, seqid: SeqId) -> Card: - return next(self.get_cards(seqid=seqid)) + def get_card(self, seq_id: SeqId) -> Card: + return next(self.get_cards(seq_id=seq_id)) - def get_card_id(self, seqid: SeqId) -> CardId: - first_card = next(self.get_cards(seqid = seqid)) + def get_card_id(self, seq_id: SeqId) -> CardId: + first_card = next(self.get_cards(seq_id = seq_id)) return first_card.card_id def update_card_description(self, card_id: CardId, description: str) -> Card: diff --git a/favro_sync/favro_fuse.py b/favro_sync/favro_fuse.py index b66d54f..dd43a97 100644 --- a/favro_sync/favro_fuse.py +++ b/favro_sync/favro_fuse.py @@ -1,8 +1,10 @@ import os, stat, errno, fuse from pathlib import Path +import re +import dataclasses from collections.abc import Iterator -from .favro_client import FavroClient +from .favro_client import FavroClient, CardId, SeqId, Card fuse.fuse_python_api = (0, 2) @@ -22,21 +24,59 @@ class MyStat(fuse.Stat): self.st_mtime = 0 self.st_ctime = 0 +CARD_FILENAME_FORMAT = 'PAR-{seq_id}.md' +CARD_FILENAME_REGEX = r'^\/PAR\-(\d+)\.md$' + +@dataclasses.dataclass(frozen=True) +class Thing: + + @staticmethod + def from_path(path_str: str) -> 'Thing | None': + if path_str == '/': + return RootThing() + if m := re.match(CARD_FILENAME_REGEX, path_str): + return CardThing(SeqId(int(m.group(1)))) + return None + +@dataclasses.dataclass(frozen=True) +class RootThing(Thing): + pass + +@dataclasses.dataclass(frozen=True) +class CardThing(Thing): + seq_id: SeqId + +def card_to_contents(card: Card) -> str: + ls = [] + #ls.append('# ') + #ls.append(card.name) + #ls.append('\n\n') + ls.append(card.detailed_description or '') + return ''.join(ls) + class FavroFuse(fuse.Fuse): + '''Favro Filesystem in Userspace. + ''' def __init__(self, favro_client: FavroClient, **kwargs): self.favro_client = favro_client super().__init__(**kwargs) def getattr(self, path: str) -> MyStat | int: + thing = Thing.from_path(path) + st = MyStat() - if path == '/': + if isinstance(thing, RootThing): st.st_mode = stat.S_IFDIR | 0o755 st.st_nlink = 2 - elif path == hello_path: - st.st_mode = stat.S_IFREG | 0o444 + elif isinstance(thing, CardThing): + card = self.favro_client.get_card(thing.seq_id) + + st.st_mode = stat.S_IFREG | 0o666 st.st_nlink = 1 - st.st_size = len(hello_str) + st.st_size = len(card_to_contents(card)) + st.st_ctime = int(card.creation_date.timestamp()) + st.st_mtime = st.st_ctime # TODO else: return -errno.ENOENT return st @@ -46,27 +86,81 @@ class FavroFuse(fuse.Fuse): yield fuse.Direntry('..') for card in self.favro_client.get_todo_list_cards(): - yield fuse.Direntry(card.seq_id_with_prefix) + yield fuse.Direntry(CARD_FILENAME_FORMAT.format(seq_id=card.seq_id.raw_id)) + return # TODO - def open(self, path: str, flags) -> int: - if path != hello_path: + def open(self, path: str, flags) -> int | None: + thing = Thing.from_path(path) + if not isinstance(thing, CardThing): return -errno.ENOENT - accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR - if (flags & accmode) != os.O_RDONLY: - return -errno.EACCES + #accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR + #if (flags & accmode) != os.O_RDONLY: return -errno.EACCES + return None def read(self, path: str, size: int, offset: int) -> bytes | int: - if path != hello_path: + # Check that this is a card thing. + thing = Thing.from_path(path) + if not isinstance(thing, CardThing): return -errno.ENOENT - slen = len(hello_str) + + card = self.favro_client.get_card(thing.seq_id) + + contents_str = card_to_contents(card) + contents = bytes(contents_str, 'utf8') + + slen = len(contents) if offset < slen: if offset + size > slen: size = slen - offset - buf = hello_str[offset:offset+size] + buf = contents[offset:offset+size] else: buf = b'' return buf + 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): + return -errno.ENOENT + + card = self.favro_client.get_card(thing.seq_id) + + # Splice contents + contents_str = card_to_contents(card) + contents = bytes(contents_str, 'utf8') + contents = splice(contents, written_buffer, offset) + contents_str = contents.decode('utf8') + + # Write to favro + self.favro_client.update_card_description(card.card_id, contents_str) + + # Return amount written + return len(written_buffer) + + def truncate(self, path: str, new_size: int): + print('Trunca', path, new_size) + # Check that this is a card thing. + thing = Thing.from_path(path) + if not isinstance(thing, CardThing): + return -errno.ENOENT + + card = self.favro_client.get_card(thing.seq_id) + + # Splice contents + contents_str = card_to_contents(card) + contents = bytes(contents_str, 'utf8') + contents = contents[0:new_size] + contents_str = contents.decode('utf8') + + # Write to favro + self.favro_client.update_card_description(card.card_id, contents_str) + + # Return amount written + return 0 + +def splice(original_buffer: bytes, input_buffer: bytes, offset: int) -> bytes: + return original_buffer[0:offset-1] + input_buffer + original_buffer[offset+len(input_buffer)+1:len(original_buffer)] + HELP=""" Userspace hello example