1
0
favro-sync/favro_sync/favro_fuse.py

260 lines
8.0 KiB
Python
Raw Normal View History

2024-09-26 19:51:53 +00:00
import dataclasses
2024-09-26 21:08:37 +00:00
import errno
import re
import stat
2024-09-26 16:49:28 +00:00
from collections.abc import Iterator
2024-09-27 09:12:14 +00:00
from logging import getLogger
2024-09-26 16:49:28 +00:00
2024-09-26 21:08:37 +00:00
import fuse
2024-09-26 21:07:46 +00:00
from .favro_client import FavroClient
2024-10-02 11:54:30 +00:00
from .favro_data_model import Card, SeqId, CustomFieldInfo, CustomFieldItemId, CustomField
from .favro_markdown import CardContents, CardFileFormatter
2024-09-26 16:49:28 +00:00
2024-09-27 09:12:14 +00:00
logger = getLogger(__name__)
2024-09-26 16:49:28 +00:00
2024-09-27 09:12:14 +00:00
fuse.fuse_python_api = (0, 2)
2024-09-26 21:08:37 +00:00
2024-09-27 14:13:03 +00:00
class FavroStat(fuse.Stat):
2024-09-26 16:49:28 +00:00
def __init__(self):
self.st_mode = 0
self.st_ino = 0
self.st_dev = 0
self.st_nlink = 0
self.st_uid = 0
self.st_gid = 0
self.st_size = 0
self.st_atime = 0
self.st_mtime = 0
self.st_ctime = 0
2024-09-26 21:08:37 +00:00
2024-10-01 09:12:17 +00:00
CARD_IDENTIFIER_FORMAT = 'PAR-{seq_id}'
CARD_FILENAME_FORMAT = CARD_IDENTIFIER_FORMAT + '.md'
2024-09-26 19:51:53 +00:00
CARD_FILENAME_REGEX = r'^\/PAR\-(\d+)\.md$'
2024-10-01 08:29:12 +00:00
OFFICIAL_URL = 'https://favro.com/organization/{org_id}?card=par-{seq_id}'
2024-09-26 21:08:37 +00:00
2024-09-26 19:51:53 +00:00
@dataclasses.dataclass(frozen=True)
2024-09-30 11:57:34 +00:00
class FileSystemItem:
2024-09-26 19:51:53 +00:00
@staticmethod
2024-09-30 11:57:34 +00:00
def from_path(path_str: str) -> 'FileSystemItem | None':
2024-09-26 19:51:53 +00:00
if path_str == '/':
2024-09-30 11:57:34 +00:00
return RootFileSystemItem()
2024-09-26 19:51:53 +00:00
if m := re.match(CARD_FILENAME_REGEX, path_str):
2024-09-30 11:57:34 +00:00
return CardFileSystemItem(SeqId(int(m.group(1))))
2024-09-26 19:51:53 +00:00
return None
2024-09-26 21:08:37 +00:00
2024-09-26 19:51:53 +00:00
@dataclasses.dataclass(frozen=True)
2024-09-30 11:57:34 +00:00
class RootFileSystemItem(FileSystemItem):
2024-09-26 19:51:53 +00:00
pass
2024-09-26 21:08:37 +00:00
2024-09-26 19:51:53 +00:00
@dataclasses.dataclass(frozen=True)
2024-09-30 11:57:34 +00:00
class CardFileSystemItem(FileSystemItem):
2024-09-26 19:51:53 +00:00
seq_id: SeqId
2024-10-02 11:54:30 +00:00
def to_custom_field_value(custom_field: CustomField, field_def: CustomFieldInfo) -> str:
value: CustomFieldItemId | list[CustomFieldItemId] = custom_field.value
if field_def.type in {'Single select','Multiple select'}:
2024-10-02 11:34:49 +00:00
items = [field_def.get_field_item(item_id) for item_id in value]
items = [i for i in items if i]
return items[0].name
2024-10-02 11:54:30 +00:00
if field_def.type in {'Color'}:
return custom_field.color
2024-10-02 11:34:49 +00:00
assert False, 'Unknown type: ' + field_def.type
def to_custom_fields(card: Card, favro_client: FavroClient) -> dict[str,str]:
custom_fields = {}
for field_assignment in card.custom_fields:
field_def = favro_client.get_custom_field(field_assignment.custom_field_id)
2024-10-02 11:54:30 +00:00
custom_fields[field_def.name] = to_custom_field_value(field_assignment, field_def)
2024-10-02 11:34:49 +00:00
del field_assignment
return custom_fields
2024-10-02 08:31:42 +00:00
def to_card_contents(card: Card, favro_client: FavroClient) -> str:
tags = [favro_client.get_tag(tag_id).name for tag_id in card.tags]
assignments = [
2024-10-02 08:32:14 +00:00
favro_client.get_user(assignment.user).name for assignment in card.assignments
2024-10-02 08:31:42 +00:00
]
dependencies = [
CARD_FILENAME_FORMAT.format(
seq_id=favro_client.get_card_by_card_id(dep.card_id).seq_id.raw_id,
)
for dep in card.dependencies
if dep.is_before
]
return CardContents(
CARD_IDENTIFIER_FORMAT.format(seq_id=card.seq_id.raw_id),
card.name,
card.detailed_description,
tags,
assignments,
dependencies,
url=OFFICIAL_URL.format(
org_id=card.organization_id.raw_id,
seq_id=card.seq_id.raw_id,
),
todo_list_completed=card.todo_list_completed,
is_archived=card.is_archived,
start_date=card.start_date,
due_date=card.due_date,
2024-10-02 11:54:30 +00:00
custom_fields=to_custom_fields(card, favro_client),
2024-10-02 08:31:42 +00:00
)
2024-09-26 16:49:28 +00:00
class FavroFuse(fuse.Fuse):
2024-09-30 11:57:34 +00:00
"""Favro FileSystem in Userspace."""
2024-09-26 16:49:28 +00:00
def __init__(
2024-09-28 12:13:51 +00:00
self,
favro_client: FavroClient,
formatter: CardFileFormatter,
**kwargs,
):
2024-09-26 16:49:28 +00:00
self.favro_client = favro_client
self.formatter = formatter
2024-09-30 11:57:34 +00:00
self.wiped_cards = set()
2024-09-26 16:49:28 +00:00
super().__init__(**kwargs)
2024-09-27 14:13:03 +00:00
def getattr(self, path: str) -> FavroStat | int:
2024-09-30 11:57:34 +00:00
thing = FileSystemItem.from_path(path)
2024-09-26 19:51:53 +00:00
2024-09-27 14:13:03 +00:00
st = FavroStat()
2024-09-30 11:57:34 +00:00
if isinstance(thing, RootFileSystemItem):
2024-09-26 16:49:28 +00:00
st.st_mode = stat.S_IFDIR | 0o755
st.st_nlink = 2
2024-09-30 11:57:34 +00:00
elif isinstance(thing, CardFileSystemItem):
2024-09-26 19:51:53 +00:00
card = self.favro_client.get_card(thing.seq_id)
st.st_mode = stat.S_IFREG | 0o666
2024-09-26 16:49:28 +00:00
st.st_nlink = 1
st.st_size = len(self._format_card_file(card))
2024-09-26 19:51:53 +00:00
st.st_ctime = int(card.creation_date.timestamp())
2024-09-26 21:08:37 +00:00
st.st_mtime = st.st_ctime # TODO
2024-09-26 16:49:28 +00:00
else:
return -errno.ENOENT
return st
def readdir(self, path: str, offset: int) -> Iterator[fuse.Direntry]:
logger.warning('readdir(path=%s, offset=%s)', path, offset)
2024-09-26 16:49:28 +00:00
yield fuse.Direntry('.')
yield fuse.Direntry('..')
for card in self.favro_client.get_todo_list_cards():
2024-09-26 19:51:53 +00:00
yield fuse.Direntry(CARD_FILENAME_FORMAT.format(seq_id=card.seq_id.raw_id))
2024-09-26 16:49:28 +00:00
def open(self, path: str, flags: int) -> int | None:
2024-09-30 11:57:34 +00:00
thing = FileSystemItem.from_path(path)
if not isinstance(thing, CardFileSystemItem):
2024-09-26 16:49:28 +00:00
return -errno.ENOENT
2024-09-26 19:51:53 +00:00
return None
2024-09-26 16:49:28 +00:00
def read(self, path: str, size: int, offset: int) -> bytes | int:
2024-09-26 19:51:53 +00:00
# Check that this is a card thing.
2024-09-30 11:57:34 +00:00
thing = FileSystemItem.from_path(path)
if not isinstance(thing, CardFileSystemItem):
2024-09-26 16:49:28 +00:00
return -errno.ENOENT
2024-09-26 19:51:53 +00:00
card = self.favro_client.get_card(thing.seq_id)
contents_str = self._format_card_file(card)
2024-09-26 19:51:53 +00:00
contents = bytes(contents_str, 'utf8')
slen = len(contents)
2024-09-26 16:49:28 +00:00
if offset < slen:
if offset + size > slen:
size = slen - offset
2024-09-26 21:08:37 +00:00
buf = contents[offset : offset + size]
2024-09-26 16:49:28 +00:00
else:
buf = b''
return buf
2024-09-26 19:51:53 +00:00
def write(self, path: str, written_buffer: bytes, offset: int) -> int:
# Check that this is a card thing.
2024-09-30 11:57:34 +00:00
thing = FileSystemItem.from_path(path)
if not isinstance(thing, CardFileSystemItem):
2024-09-26 19:51:53 +00:00
return -errno.ENOENT
card = self.favro_client.get_card(thing.seq_id)
# Splice contents
contents_str = self._format_card_file(card)
2024-09-26 19:51:53 +00:00
contents = bytes(contents_str, 'utf8')
contents = splice(contents, written_buffer, offset)
contents_str = contents.decode('utf8')
# Write to favro
card_updated = self.formatter.parse_card_contents(contents_str)
2024-09-27 14:13:03 +00:00
self.favro_client.update_card_contents(card.card_id, card_updated)
2024-09-26 19:51:53 +00:00
2024-09-30 11:57:34 +00:00
self.wiped_cards.remove(thing.seq_id)
2024-09-26 19:51:53 +00:00
# Return amount written
return len(written_buffer)
def truncate(self, path: str, new_size: int):
# Check that this is a card thing.
2024-09-30 11:57:34 +00:00
thing = FileSystemItem.from_path(path)
if not isinstance(thing, CardFileSystemItem):
2024-09-26 19:51:53 +00:00
return -errno.ENOENT
card = self.favro_client.get_card(thing.seq_id)
# Splice contents
contents_str = self._format_card_file(card)
2024-09-26 19:51:53 +00:00
contents = bytes(contents_str, 'utf8')
old_size = len(contents)
contents = contents[0:new_size] + b' ' * (old_size - new_size)
assert len(contents) == old_size
2024-09-26 19:51:53 +00:00
contents_str = contents.decode('utf8')
# Write to favro
card_updated = self.formatter.parse_card_contents(contents_str)
2024-09-27 14:13:03 +00:00
self.favro_client.update_card_contents_locally(card.card_id, card_updated)
2024-09-26 19:51:53 +00:00
2024-09-30 11:57:34 +00:00
self.wiped_cards.add(thing.seq_id)
2024-09-26 19:51:53 +00:00
# Return amount written
return 0
def _format_card_file(self, card: Card) -> str:
2024-09-30 11:57:34 +00:00
if card.seq_id in self.wiped_cards:
return ''
2024-10-02 08:31:42 +00:00
card_contents = to_card_contents(card, self.favro_client)
return self.formatter.format_card_contents(card_contents)
2024-09-26 21:08:37 +00:00
2024-09-26 19:51:53 +00:00
def splice(original_buffer: bytes, input_buffer: bytes, offset: int) -> bytes:
2024-09-26 21:08:37 +00:00
return (
original_buffer[0 : offset - 1]
+ input_buffer
+ original_buffer[offset + len(input_buffer) + 1 : len(original_buffer)]
)
2024-09-26 19:51:53 +00:00
2024-09-26 21:08:37 +00:00
HELP = (
"""
2024-09-26 16:49:28 +00:00
Userspace hello example
2024-09-26 21:08:37 +00:00
"""
+ fuse.Fuse.fusage
)
2024-09-26 16:49:28 +00:00
def start_favro_fuse(favro_client: FavroClient):
# TODO:
server = FavroFuse(
2024-09-26 21:08:37 +00:00
favro_client=favro_client,
formatter=CardFileFormatter(),
2024-09-26 21:08:37 +00:00
version='%prog ' + fuse.__version__,
usage=HELP,
dash_s_do='setsingle',
)
2024-09-26 16:49:28 +00:00
server.parse(errex=1)
server.main()