Add move operation, fix fuse server
This commit is contained in:
parent
41480a39c9
commit
6470aee802
5 changed files with 90 additions and 45 deletions
|
|
@ -14,6 +14,8 @@ from typing import NamedTuple
|
|||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from . import trove as tr
|
||||
|
||||
NOTE_ROOT_ID = uuid.UUID(int=0)
|
||||
|
||||
class ObjectInfo(NamedTuple):
|
||||
|
|
@ -288,13 +290,21 @@ class Sqlite3Trove:
|
|||
# Tree entry operations
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def link(self, parent_id: SqlObjectId, name: str, child_id: SqlObjectId) -> None:
|
||||
def link(self, parent_id: SqlObjectId, name: str, child_id: SqlObjectId, overwrite: bool = True) -> None:
|
||||
"""
|
||||
Link a child object into a tree under the given name.
|
||||
Replaces any existing entry with the same name in this tree.
|
||||
Replaces any existing entry with the same name in this tree if overwrite=True.
|
||||
Both parent_id and child_id must exist in the objects table
|
||||
(enforced by FK constraints).
|
||||
"""
|
||||
if not overwrite:
|
||||
existing = self._con.execute(
|
||||
"SELECT 1 FROM tree_entries WHERE parent_id = ? AND name = ?",
|
||||
(_sql_id(parent_id), name),
|
||||
).fetchone()
|
||||
if existing:
|
||||
raise tr.ErrorExists(f"Entry '{name}' already exists in tree {parent_id}")
|
||||
|
||||
self._con.execute(
|
||||
"INSERT OR REPLACE INTO tree_entries (parent_id, name, child_id) "
|
||||
"VALUES (?, ?, ?)",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import os
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import datetime as dt
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Optional, Dict, List, Self, Iterable, Iterator
|
||||
from pathlib import Path
|
||||
from typing import Optional, Iterable, Iterator, override
|
||||
|
||||
from .trove import Note, Trove, TreeNote, BadNoteType, TreeEntry, NoteNotFound, ObjectId
|
||||
from . import fs_util as fsu
|
||||
from . import trove as tr
|
||||
|
||||
class FSNote(Note):
|
||||
|
|
@ -78,7 +75,7 @@ class FSNote(Note):
|
|||
content = b""
|
||||
|
||||
if mime == 'inode/directory':
|
||||
if content is not None:
|
||||
if content:
|
||||
raise NotImplementedError("FSNote does not support children")
|
||||
return FSTreeNote(self._trove, self._new_child_subdir(name, False))
|
||||
|
||||
|
|
@ -104,24 +101,22 @@ class FSNote(Note):
|
|||
target_path.unlink()
|
||||
# TODO: remove meta directory!
|
||||
|
||||
|
||||
def children(self) -> Iterator[TreeEntry]:
|
||||
"""Get all children of this note."""
|
||||
if not self._fs_path.is_dir():
|
||||
def unlink_(self, name: str):
|
||||
target_path = self._fs_path / name
|
||||
if not target_path.exists():
|
||||
return
|
||||
for item in self._fs_path.iterdir():
|
||||
if item.name == ".trove":
|
||||
continue
|
||||
yield TreeEntry(name=item.name, object_id=item.stat().st_ino)
|
||||
if target_path.is_dir():
|
||||
target_path.rmdir()
|
||||
else:
|
||||
target_path.unlink()
|
||||
|
||||
class FSTreeNote(FSNote, TreeNote):
|
||||
def link(self, name: str, note: Note):
|
||||
def link_(self, name: str, note: Note):
|
||||
if not isinstance(note, FSNote):
|
||||
raise BadNoteType("Only blob notes can be linked")
|
||||
|
||||
target_path = self._fs_path / name
|
||||
if target_path.exists():
|
||||
self.unlink(name)
|
||||
self.unlink_(name)
|
||||
|
||||
note_path = note._fs_path
|
||||
|
||||
|
|
@ -141,15 +136,16 @@ class FSTreeNote(FSNote, TreeNote):
|
|||
# We move it to the new location.
|
||||
os.rename(note_path, target_path)
|
||||
|
||||
def unlink(self, name: str):
|
||||
target_path = self._fs_path / name
|
||||
if not target_path.exists():
|
||||
def children(self) -> Iterator[TreeEntry]:
|
||||
"""Get all children of this note."""
|
||||
if not self._fs_path.is_dir():
|
||||
return
|
||||
if target_path.is_dir():
|
||||
target_path.rmdir()
|
||||
else:
|
||||
target_path.unlink()
|
||||
for item in self._fs_path.iterdir():
|
||||
if item.name == ".trove":
|
||||
continue
|
||||
yield TreeEntry(name=item.name, object_id=item.stat().st_ino)
|
||||
|
||||
class FSTreeNote(FSNote, TreeNote):
|
||||
def mkdir(self, name: str) -> 'FSTreeNote':
|
||||
target_path = self._fs_path / name
|
||||
target_path.mkdir(exist_ok=True)
|
||||
|
|
@ -164,6 +160,11 @@ class FSTreeNote(FSNote, TreeNote):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
def unlink(self, name: str):
|
||||
return self.unlink_(name)
|
||||
|
||||
def link(self, name: str, note: Note):
|
||||
return self.link_(name, note)
|
||||
|
||||
|
||||
|
||||
|
|
@ -198,6 +199,25 @@ class FSTrove(Trove):
|
|||
raise NoteNotFound(note_id)
|
||||
return self.get_raw_note_by_path(p)
|
||||
|
||||
@override
|
||||
def move(self, src_parent: Note, src_name: str, dst_parent: Note, dst_name: str, overwrite: bool):
|
||||
"""Move a child note to a new location."""
|
||||
src_note = src_parent.child(src_name)
|
||||
if not isinstance(src_note, FSNote):
|
||||
raise tr.ErrorBadType("not a valid DB note")
|
||||
if not isinstance(dst_parent, FSNote):
|
||||
raise tr.ErrorBadType("not a valid DB note")
|
||||
if not dst_parent._fs_path.is_dir():
|
||||
raise NotImplementedError("FSTrove promoting during move")
|
||||
|
||||
# Remove existing target
|
||||
if overwrite:
|
||||
dst_parent.unlink_(dst_name)
|
||||
|
||||
# Link to new parent, unlink from old
|
||||
dst_parent.link_(dst_name, src_note)
|
||||
src_parent.rm_child(src_name, True)
|
||||
|
||||
def create_blob(self, data: bytes | None = None) -> Note:
|
||||
raise NotImplementedError("FSTrove does not support blobs")
|
||||
|
||||
|
|
|
|||
|
|
@ -378,12 +378,15 @@ class TroveFuseOps(pyfuse3.Operations):
|
|||
self._close_handle(handle)
|
||||
|
||||
async def unlink(self, parent_inode: InodeT, name: bytes, ctx) -> None:
|
||||
parent_note = self._get_inode_note(parent_inode)
|
||||
name_str = name.decode()
|
||||
parent_note.rm_child(name_str, False)
|
||||
try:
|
||||
parent_note = self._get_inode_note(parent_inode)
|
||||
name_str = name.decode()
|
||||
parent_note.rm_child(name_str, False)
|
||||
except tr.ErrorWithErrno as e:
|
||||
raise pyfuse3.FUSEError(e.errno) from None
|
||||
|
||||
async def rename(self, parent_inode_old: InodeT, name_old: bytes, parent_inode_new: InodeT, name_new: bytes, flags, ctx):
|
||||
# Decode / validate names
|
||||
# # Decode / validate names
|
||||
name_new_str = name_new.decode()
|
||||
name_old_str = name_old.decode()
|
||||
|
||||
|
|
@ -395,16 +398,8 @@ class TroveFuseOps(pyfuse3.Operations):
|
|||
if not isinstance(old_parent, TroveTree):
|
||||
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
||||
|
||||
# We want to maintain the inode - find the note via the internal entity
|
||||
ent, note = self._lookup_child(parent_inode_old, name_old)
|
||||
|
||||
# Remove existing target
|
||||
new_parent.unlink(name_new_str)
|
||||
|
||||
# Link to new parent, unlink from old
|
||||
new_parent.link(name_new_str, note)
|
||||
old_parent.unlink(name_old_str)
|
||||
|
||||
# Move!
|
||||
self._trove.move(old_parent, name_old_str, new_parent, name_new_str, overwrite=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Serve
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ class ErrorNotEmpty(ErrorWithErrno):
|
|||
def __init__(self, *args):
|
||||
super().__init__(errno.ENOTEMPTY, *args)
|
||||
|
||||
class ErrorBadType(TypeError):
|
||||
"""Raised when an invalid type is encountered."""
|
||||
...
|
||||
|
||||
|
||||
class BadNoteType(TypeError):
|
||||
"""Raised when an invalid note type is encountered."""
|
||||
|
|
@ -159,6 +163,10 @@ class Trove(Protocol):
|
|||
"""Retrieve a note by a object id"""
|
||||
...
|
||||
|
||||
def move(self, src_parent: Note, src_name: str, dst_parent: Note, dst_name: str, overwrite: bool):
|
||||
"""Move a child note to a new location."""
|
||||
...
|
||||
|
||||
def create_blob(self, data: bytes | None = None) -> Note:
|
||||
"""Create a new blob node at the given path with content"""
|
||||
...
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Implements BlobNote, TreeNote, and Trove protocols defined in trove.py.
|
|||
Depends on db.py (Sqlite3Trove) for storage.
|
||||
"""
|
||||
|
||||
from typing import Optional, Iterator
|
||||
from typing import Optional, Iterator, override
|
||||
from pathlib import Path
|
||||
import datetime as dt
|
||||
import uuid
|
||||
|
|
@ -78,6 +78,7 @@ class NoteImpl(Note):
|
|||
"""Create a new child note."""
|
||||
content = content if content is not None else b""
|
||||
object_id = self._db.write_blob(data=content, object_id=None, dtype=mime, executable=executable, hidden=hidden)
|
||||
self._db.link(self._object_id, name, object_id)
|
||||
# TODO fix this
|
||||
if mime == 'inode/directory':
|
||||
return TreeNoteImpl(self._parent, object_id)
|
||||
|
|
@ -139,7 +140,7 @@ class TreeNoteImpl(NoteImpl, TreeNote):
|
|||
# Trove
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TroveImpl:
|
||||
class TroveImpl(Trove):
|
||||
"""
|
||||
Concrete Trove: top-level API backed by a Sqlite3Trove database.
|
||||
|
||||
|
|
@ -179,6 +180,17 @@ class TroveImpl:
|
|||
return TreeNoteImpl(self, note_id)
|
||||
return NoteImpl(self, note_id)
|
||||
|
||||
@override
|
||||
def move(self, src_parent: Note, src_name: str, dst_parent: Note, dst_name: str, overwrite: bool):
|
||||
"""Move a child note to a new location."""
|
||||
src_note = src_parent.child(src_name)
|
||||
if not isinstance(src_note, NoteImpl):
|
||||
raise tr.ErrorBadType("not a valid DB note")
|
||||
if not isinstance(dst_parent, NoteImpl):
|
||||
raise tr.ErrorBadType("not a valid DB note")
|
||||
self._db.link(dst_parent.object_id, dst_name, src_note.object_id)
|
||||
self._db.unlink(src_parent.object_id, src_name)
|
||||
|
||||
def create_blob(self, data: bytes | None = None,
|
||||
dtype: str = "application/octet-stream") -> Note:
|
||||
"""Create a new blob object and return a BlobNote for it."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue