"""
rx_rust.folder_watcher - 目录系统监控与响应式分发
核心公共 API:
FolderChangeType(IntEnum): 目录变更类型枚举
FolderData: 结构化目录事件数据(支持 JSON/Pickle 往返)
FolderDispatcher: 目录系统监控与分发器
FolderSubject: 带目录监控的 Subject(自包含 Dispatcher)
FolderObserver: 按 FolderChangeType 路由回调的便捷观察者(钩子式)
from_foldersystem(...): 顶层工厂:返回 (Observable[FolderData], FolderDispatcher)
write_to_foldersystem(...):响应式操作符:把上游内容写入目录系统
设计:
- 优先使用 Rust 扩展(rx_rust._rust 中暴露的类型)
- 如果不可用,回退到纯 Python 实现(Thread + os.walk 轮询)
- 在 Windows 上使用 ReadDirectoryChangesW(可选)
"""
from __future__ import annotations
import itertools
import json
import logging
import os
import pickle
import sys
import threading
import time
from dataclasses import dataclass, field
from datetime import datetime
from enum import IntEnum
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Tuple,
TypeVar,
)
try:
from . import _rust as _rust_mod
_HAS_RUST = True
except Exception:
try:
import rx_rust as _rust_mod
_HAS_RUST = True
except Exception:
_rust_mod = None
_HAS_RUST = False
try:
from . import Observable, _PyObservable, Subscription
except Exception:
try:
from rx_rust import Observable, _PyObservable, Subscription
except Exception:
_HAS_RUST_BASE = False
T = TypeVar("T")
class Subscription:
def __init__(self, unsubscribe: Callable[[], None] | None = None) -> None:
self._unsub = unsubscribe
def dispose(self) -> None:
if self._unsub is not None:
try:
self._unsub()
except Exception:
pass
class _PyObservable:
def __init__(self, subscribe: Callable[[Any], Subscription]) -> None:
self._subscribe = subscribe
class Observable:
def __init__(self, py_observable: _PyObservable | None = None) -> None:
self._inner = py_observable
if self._inner is None:
def _nop(observer: Any) -> Subscription:
return Subscription()
self._inner = _PyObservable(_nop)
def subscribe(self, on_next: Callable[[Any], Any] | None = None,
on_error: Callable[[Any], Any] | None = None,
on_completed: Callable[[], Any] | None = None) -> Subscription:
if self._inner is None:
return Subscription()
try:
return self._inner._subscribe(on_next)
except Exception:
try:
return self._inner._subscribe(on_next)
except Exception:
return Subscription()
def pipe(self, *operators: Callable[["Observable"], "Observable"]) -> "Observable":
current = self
for op in operators:
try:
current = op(current)
except Exception:
continue
return current
log = logging.getLogger("rx_rust.folder_watcher")
T = TypeVar("T")
R = TypeVar("R")
class FolderChangeType(IntEnum):
"""目录变更类型枚举。
与 Rust 端保持一致的整数值:
CREATED = 0 - 目录被创建
DELETED = 1 - 目录被删除
RENAMED = 2 - 目录被重命名(old_path -> path)
MOVED_IN = 3 - 目录从外部移入监控范围
MOVED_OUT = 4 - 目录从监控范围移到外部
ATTRIB = 5 - 目录元数据(权限/所有者)变更
CONTENT = 6 - 目录内容(子项)发生变化
"""
CREATED = 0
DELETED = 1
RENAMED = 2
MOVED_IN = 3
MOVED_OUT = 4
ATTRIB = 5
CONTENT = 6
def __str__(self) -> str:
return self.name
_folder_seq_counter = itertools.count(1)
@dataclass(slots=True)
class FolderData:
"""结构化目录事件数据。
字段:
path: 事件发生的目录路径
old_path: 重命名/移动时的原始路径;其他情况 None
change_type: 变更类型(FolderChangeType)
file_count: 目录内文件数(某些后端可能不填充)
child_folder_count: 目录内子目录数(某些后端可能不填充)
timestamp: 事件被检测到的时间
sequence: 全局单调序号(跨事件递增)
tags: 用户自定义标签
metadata: 扩展元信息
"""
path: str
old_path: str | None
change_type: FolderChangeType
file_count: int | None
child_folder_count: int | None
timestamp: datetime
sequence: int
tags: List[str]
metadata: Dict[str, Any]
@classmethod
def now(
cls,
path: str,
old_path: str | None = None,
change_type: FolderChangeType = FolderChangeType.CONTENT,
file_count: int | None = None,
child_folder_count: int | None = None,
tags: Iterable[str] = (),
metadata: Dict[str, Any] | None = None,
) -> "FolderData":
return cls(
path=path,
old_path=old_path,
change_type=change_type,
file_count=file_count,
child_folder_count=child_folder_count,
timestamp=datetime.now(),
sequence=next(_folder_seq_counter),
tags=list(tags or ()),
metadata=dict(metadata or {}),
)
def to_dict(self) -> Dict[str, Any]:
ts = self.timestamp
ts_str = ts.isoformat() if isinstance(ts, datetime) else str(ts)
return {
"path": self.path,
"old_path": self.old_path,
"change_type": int(self.change_type),
"change_type_name": str(self.change_type),
"file_count": self.file_count,
"child_folder_count": self.child_folder_count,
"timestamp": ts_str,
"sequence": self.sequence,
"tags": list(self.tags),
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "FolderData":
d = dict(data or {})
ct_raw = d.get("change_type", FolderChangeType.CONTENT.value)
try:
ct = FolderChangeType(int(ct_raw))
except (TypeError, ValueError):
try:
ct = FolderChangeType[str(ct_raw).upper()]
except KeyError:
ct = FolderChangeType.CONTENT
ts = d.get("timestamp")
if isinstance(ts, str):
try:
ts = datetime.fromisoformat(ts)
except ValueError:
ts = datetime.now()
elif isinstance(ts, (int, float)):
try:
ts = datetime.fromtimestamp(float(ts))
except Exception:
ts = datetime.now()
elif not isinstance(ts, datetime):
ts = datetime.now()
return cls(
path=d.get("path", ""),
old_path=d.get("old_path"),
change_type=ct,
file_count=d.get("file_count"),
child_folder_count=d.get("child_folder_count"),
timestamp=ts,
sequence=int(d.get("sequence", next(_folder_seq_counter))),
tags=list(d.get("tags") or []),
metadata=dict(d.get("metadata") or {}),
)
def to_json(self, **kw: Any) -> str:
return json.dumps(self.to_dict(), ensure_ascii=False, **kw)
@classmethod
def from_json(cls, s: str, **kw: Any) -> "FolderData":
return cls.from_dict(json.loads(s, **kw))
def to_pickle(self) -> bytes:
return pickle.dumps(self)
@classmethod
def from_pickle(cls, b: bytes) -> "FolderData":
return pickle.loads(b)
def __str__(self) -> str:
return (
f"FolderData(path={self.path!r}, change_type={self.change_type.name}, "
f"seq={self.sequence})"
)
def __repr__(self) -> str:
return self.__str__()
class _FolderPollingBackend:
"""通用平台:每隔 interval 秒对所有监控路径执行 os.walk 比对快照。
仅对目录事件进行分发(忽略纯文件事件)。
"""
name = "polling"
def __init__(
self,
on_change: Callable[[str, str | None, FolderChangeType], None],
paths: Iterable[str] | None = None,
interval: float = 0.5,
) -> None:
self._on_change = on_change
self._paths: List[str] = list(paths) if paths else []
self._interval = max(0.05, float(interval))
self._thread: Optional[threading.Thread] = None
self._stop = threading.Event()
self._running = False
self._lock = threading.Lock()
self._snapshot: Dict[str, float] = {}
@property
def is_running(self) -> bool:
return self._running
def add_path(self, path: str) -> None:
p = os.path.abspath(path)
with self._lock:
if p not in self._paths:
self._paths.append(p)
try:
for root, dirs, _ in os.walk(p):
for dn in dirs:
full = os.path.join(root, dn)
try:
self._snapshot[full] = os.path.getmtime(full)
except OSError:
continue
except Exception:
pass
def remove_path(self, path: str) -> None:
p = os.path.abspath(path)
with self._lock:
if p in self._paths:
self._paths.remove(p)
for key in list(self._snapshot.keys()):
if key == p or key.startswith(p + os.sep):
self._snapshot.pop(key, None)
def start(self) -> None:
with self._lock:
if self._running:
return
self._stop.clear()
self._running = True
self._snapshot.clear()
for p in self._paths:
try:
for root, dirs, _ in os.walk(p):
for dn in dirs:
full = os.path.join(root, dn)
try:
self._snapshot[full] = os.path.getmtime(full)
except OSError:
continue
except Exception:
pass
self._thread = threading.Thread(
target=self._run,
name="rx-rust-folder-polling",
daemon=True,
)
self._thread.start()
def stop(self) -> None:
with self._lock:
if not self._running:
return
self._running = False
self._stop.set()
if self._thread and self._thread.is_alive():
try:
self._thread.join(timeout=self._interval * 3 + 0.5)
except Exception:
pass
def _run(self) -> None:
try:
while not self._stop.is_set():
time.sleep(self._interval)
current: Dict[str, float] = {}
with self._lock:
paths_snapshot = list(self._paths)
for p in paths_snapshot:
try:
for root, dirs, _ in os.walk(p):
for dn in dirs:
full = os.path.join(root, dn)
try:
current[full] = os.path.getmtime(full)
except OSError:
continue
except Exception:
pass
old_snapshot = self._snapshot
for full, mtime in current.items():
if full not in old_snapshot:
try:
self._on_change(full, None, FolderChangeType.CREATED)
except Exception as e:
log.debug("polling CREATED 异常: %s", e)
elif old_snapshot[full] != mtime:
try:
self._on_change(full, None, FolderChangeType.CONTENT)
except Exception as e:
log.debug("polling CONTENT 异常: %s", e)
for full in old_snapshot:
if full not in current:
try:
self._on_change(full, None, FolderChangeType.DELETED)
except Exception as e:
log.debug("polling DELETED 异常: %s", e)
self._snapshot = current
finally:
self._running = False
class FolderDispatcher:
"""目录系统监控与分发器。
与 file_watcher.FileDispatcher 类似,但只处理目录级事件。
典型用法:
>>> d = FolderDispatcher(paths=["./src"])
>>> d.start()
>>> d.subject.subscribe(on_next=lambda fd: print(fd.path, fd.change_type))
>>> d.stop()
或作为上下文管理器:
>>> with FolderDispatcher(paths=["./src"]) as d:
... d.subject.subscribe(on_next=print)
... ...
"""
__slots__ = (
"_backend",
"_subject",
"_lock",
"_paths",
"_change_types_allowed",
"_tags",
"_interval",
"_dispatch_count",
"_error_count",
"_backend_name",
"_running",
)
def __init__(
self,
*,
paths: Iterable[str] | None = None,
backend: str = "auto",
change_types: Iterable[FolderChangeType] | None = None,
tags: Iterable[str] = (),
interval: float = 0.5,
) -> None:
self._lock = threading.RLock()
self._paths: List[str] = list(paths) if paths else []
self._change_types_allowed: Optional[set] = (
set(change_types) if change_types else None
)
self._tags: List[str] = list(tags or ())
self._interval = max(0.05, float(interval))
self._dispatch_count = 0
self._error_count = 0
try:
from . import _rust
self._subject = _rust.PublishSubject()
except Exception:
try:
import rx_rust as _rx
self._subject = _rx.PublishSubject()
except Exception:
self._subject = _MiniPublishSubject()
backend = (backend or "auto").lower()
self._backend_name = "polling"
be: Any = _FolderPollingBackend(
self._dispatch_once,
paths=self._paths,
interval=self._interval,
)
self._backend = be
self._running = False
@property
def subject(self) -> Any:
return self._subject
@property
def backend_name(self) -> str:
return self._backend_name
@property
def dispatch_count(self) -> int:
return self._dispatch_count
@property
def error_count(self) -> int:
return self._error_count
@property
def is_running(self) -> bool:
return self._running and bool(getattr(self._backend, "is_running", False))
def start(self) -> None:
with self._lock:
if self._running and getattr(self._backend, "is_running", False):
return
try:
self._backend.start()
except Exception:
pass
self._running = True
def stop(self) -> None:
with self._lock:
if not self._running:
return
self._running = False
try:
self._backend.stop()
except Exception:
pass
def __enter__(self) -> "FolderDispatcher":
self.start()
return self
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
self.stop()
def add_path(self, path: str) -> None:
"""动态添加监控路径。"""
path = os.path.abspath(path)
with self._lock:
if path not in self._paths:
self._paths.append(path)
if hasattr(self._backend, "add_path"):
try:
self._backend.add_path(path)
except Exception:
pass
def remove_path(self, path: str) -> None:
"""动态移除监控路径。"""
path = os.path.abspath(path)
with self._lock:
if path in self._paths:
self._paths.remove(path)
if hasattr(self._backend, "remove_path"):
try:
self._backend.remove_path(path)
except Exception:
pass
def _dispatch_once(
self,
path: str,
old_path: str | None,
change_type: FolderChangeType,
) -> None:
if (
self._change_types_allowed is not None
and change_type not in self._change_types_allowed
):
return
try:
fd = FolderData.now(
path=path,
old_path=old_path,
change_type=change_type,
tags=self._tags,
)
try:
self._subject.on_next(fd)
except Exception:
try:
if callable(self._subject):
self._subject(fd)
except Exception:
pass
self._dispatch_count += 1
except Exception as e:
log.debug("FolderDispatcher.dispatch 异常: %s", e)
self._error_count += 1
class _MiniPublishSubject:
__slots__ = ("_observers", "_lock", "_disposed_ids", "_next_id")
def __init__(self) -> None:
self._observers: List[Tuple[int, Callable[[Any], Any]]] = []
self._lock = threading.RLock()
self._disposed_ids: set = set()
self._next_id = itertools.count(1)
def on_next(self, value: Any) -> None:
with self._lock:
items = list(self._observers)
for _id, cb in items:
try:
cb(value)
except Exception as e:
log.debug("Folder subject observer 异常: %s", e)
def subscribe(self, on_next: Any = None,
on_error: Any = None, on_completed: Any = None) -> Subscription:
callable_: Callable[[Any], Any]
if callable(on_next):
callable_ = on_next
else:
callable_ = lambda v: None
with self._lock:
sid = next(self._next_id)
self._observers.append((sid, callable_))
def _unsubscribe() -> None:
with self._lock:
self._observers = [(i, c) for (i, c) in self._observers if i != sid]
return Subscription(_unsubscribe)
def pipe(self, *operators: Callable[[Any], Any]) -> "Observable":
current: Any = self
def _make_subscribe(inner: Any) -> Callable[[Any], Subscription]:
def _sub(observer: Any) -> Subscription:
if callable(observer):
return inner.subscribe(observer)
return inner.subscribe(lambda v: None)
return _sub
for op in operators:
try:
current = op(current)
except Exception:
continue
try:
subscribe = _make_subscribe(current)
obs = Observable(_PyObservable(subscribe))
return obs
except Exception:
return Observable(_PyObservable(lambda ob: Subscription()))
class FolderSubject:
"""带目录监控能力的 Subject(钩子式 API)。
用法:
>>> with FolderSubject(paths=["./src"]) as fs:
... fs.pipe(
... ops.filter(lambda f: f.change_type == FolderChangeType.CREATED),
... ).subscribe(on_next=lambda f: print("新建目录:", f.path))
...
>>> # 或配合 FolderObserver 钩子式 API:
>>> obs = FolderObserver(on_created=lambda fd: print("新建:", fd.path))
>>> obs.attach(fs)
"""
__slots__ = ("_dispatcher", "_subject")
def __init__(
self,
*,
paths: Iterable[str] | None = None,
backend: str = "auto",
change_types: Iterable[FolderChangeType] | None = None,
tags: Iterable[str] = (),
interval: float = 0.5,
auto_start: bool = True,
) -> None:
self._dispatcher = FolderDispatcher(
paths=paths,
backend=backend,
change_types=change_types,
tags=tags,
interval=interval,
)
self._subject = self._dispatcher.subject
if auto_start:
self._dispatcher.start()
@property
def dispatcher(self) -> FolderDispatcher:
return self._dispatcher
@property
def backend_name(self) -> str:
return self._dispatcher.backend_name
@property
def dispatch_count(self) -> int:
return self._dispatcher.dispatch_count
@property
def error_count(self) -> int:
return self._dispatcher.error_count
@property
def is_running(self) -> bool:
return self._dispatcher.is_running
def on_next(self, value: Any) -> None:
if hasattr(self._subject, "on_next"):
self._subject.on_next(value)
def on_completed(self) -> None:
if hasattr(self._subject, "on_completed"):
self._subject.on_completed()
def subscribe(self, on_next: Any = None,
on_error: Any = None, on_completed: Any = None) -> Subscription:
if hasattr(self._subject, "subscribe"):
return self._subject.subscribe(on_next=on_next, on_error=on_error, on_completed=on_completed)
return Subscription()
def pipe(self, *operators: Callable[[Any], Any]) -> Any:
inner_sub = self._subject
def _subscribe_func(observer: Any) -> Subscription:
if callable(observer):
if hasattr(inner_sub, "subscribe"):
return inner_sub.subscribe(observer)
return Subscription()
try:
result: Any = Observable(_PyObservable(_subscribe_func))
except Exception:
result = _MiniPublishSubject()
for op in operators:
try:
result = op(result)
except Exception:
continue
return result
def start(self) -> None:
self._dispatcher.start()
def stop(self) -> None:
self._dispatcher.stop()
def __enter__(self) -> "FolderSubject":
self._dispatcher.start()
return self
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
self._dispatcher.stop()
def add_path(self, path: str) -> None:
self._dispatcher.add_path(path)
def remove_path(self, path: str) -> None:
self._dispatcher.remove_path(path)
class FolderObserver:
"""钩子式目录观察者:以 on_created / on_deleted / ... 等钩子监听目录事件。
与 vools.reactive.folder_watcher 语义对齐。
用法:
>>> obs = FolderObserver(
... on_created=lambda fd: print("新建目录:", fd.path),
... on_deleted=lambda fd: print("删除目录:", fd.path),
... on_renamed=lambda fd: print(f"{fd.old_path} -> {fd.path}"),
... on_any=lambda fd: print("事件:", fd.change_type.name),
... )
>>> sub = obs.subscribe(folder_subject)
>>> # 或:
>>> obs.attach(folder_subject)
动态钩子:
>>> obs.add_hook(FolderChangeType.CONTENT, lambda fd: print("内容变更"))
>>> obs.remove_hook(FolderChangeType.CONTENT, handler)
>>> obs.clear_hooks()
"""
__slots__ = (
"_hooks_by_type",
"_on_any",
"_on_error",
"_subscription",
"_subscribed",
"_lock",
)
def __init__(
self,
*,
on_created: Callable[[FolderData], Any] | None = None,
on_deleted: Callable[[FolderData], Any] | None = None,
on_renamed: Callable[[FolderData], Any] | None = None,
on_moved_in: Callable[[FolderData], Any] | None = None,
on_moved_out: Callable[[FolderData], Any] | None = None,
on_attrib: Callable[[FolderData], Any] | None = None,
on_content: Callable[[FolderData], Any] | None = None,
on_any: Callable[[FolderData], Any] | None = None,
on_error: Callable[[Any], Any] | None = None,
) -> None:
self._lock = threading.RLock()
self._hooks_by_type: Dict[int, List[Callable[[FolderData], Any]]] = {
FolderChangeType.CREATED.value: [],
FolderChangeType.DELETED.value: [],
FolderChangeType.RENAMED.value: [],
FolderChangeType.MOVED_IN.value: [],
FolderChangeType.MOVED_OUT.value: [],
FolderChangeType.ATTRIB.value: [],
FolderChangeType.CONTENT.value: [],
}
if on_created:
self._hooks_by_type[FolderChangeType.CREATED.value].append(on_created)
if on_deleted:
self._hooks_by_type[FolderChangeType.DELETED.value].append(on_deleted)
if on_renamed:
self._hooks_by_type[FolderChangeType.RENAMED.value].append(on_renamed)
if on_moved_in:
self._hooks_by_type[FolderChangeType.MOVED_IN.value].append(on_moved_in)
if on_moved_out:
self._hooks_by_type[FolderChangeType.MOVED_OUT.value].append(on_moved_out)
if on_attrib:
self._hooks_by_type[FolderChangeType.ATTRIB.value].append(on_attrib)
if on_content:
self._hooks_by_type[FolderChangeType.CONTENT.value].append(on_content)
self._on_any: Optional[Callable[[FolderData], Any]] = on_any
self._on_error: Optional[Callable[[Any], Any]] = on_error
self._subscription: Optional[Subscription] = None
self._subscribed = False
def __call__(self, fd: Any) -> Any:
try:
ct_int: int = int(fd.change_type)
except Exception:
ct_int = FolderChangeType.CONTENT.value
if self._on_any is not None:
try:
self._on_any(fd)
except Exception as e:
if self._on_error is not None:
try:
self._on_error(e)
except Exception:
pass
with self._lock:
hooks = list(self._hooks_by_type.get(ct_int, []))
for h in hooks:
try:
h(fd)
except Exception as e:
if self._on_error is not None:
try:
self._on_error(e)
except Exception:
pass
return None
def subscribe(self, subject_or_observable: Any) -> Subscription:
def _on_next(fd: Any) -> None:
try:
self(fd)
except Exception:
pass
try:
sub = subject_or_observable.subscribe(_on_next)
except Exception:
sub = Subscription()
with self._lock:
self._subscription = sub
self._subscribed = True
return sub
def attach(self, subject: Any) -> "FolderObserver":
self.subscribe(subject)
return self
def unsubscribe(self) -> None:
with self._lock:
if self._subscription is not None:
try:
self._subscription.dispose()
except Exception:
pass
self._subscription = None
self._subscribed = False
@property
def is_subscribed(self) -> bool:
return self._subscribed
def _normalize_ct(self, change_type_or_name: Any) -> int:
"""支持 int / FolderChangeType / str("CREATED" / "created" / "folder_created" 等)."""
if isinstance(change_type_or_name, FolderChangeType):
return int(change_type_or_name)
if isinstance(change_type_or_name, int):
return change_type_or_name
if isinstance(change_type_or_name, str):
key = change_type_or_name.upper().replace("FOLDER_", "")
try:
return int(FolderChangeType[key])
except (KeyError, ValueError):
pass
for ct in FolderChangeType:
if ct.name.upper() == key:
return int(ct)
raise ValueError(f"无法识别的 FolderChangeType: {change_type_or_name!r}")
def add_hook(self, change_type_or_name: Any, hook: Callable[[FolderData], Any]) -> None:
if not callable(hook):
raise TypeError("hook 必须是可调用对象")
ct_int = self._normalize_ct(change_type_or_name)
with self._lock:
self._hooks_by_type.setdefault(ct_int, []).append(hook)
def remove_hook(self, change_type_or_name: Any, hook: Callable[[FolderData], Any]) -> bool:
ct_int = self._normalize_ct(change_type_or_name)
with self._lock:
if ct_int not in self._hooks_by_type:
return False
lst = self._hooks_by_type[ct_int]
for i, existing in enumerate(lst):
if existing is hook or existing == hook:
lst.pop(i)
return True
return False
def clear_hooks(self) -> None:
with self._lock:
for lst in self._hooks_by_type.values():
lst.clear()
def __enter__(self) -> "FolderObserver":
return self
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
self.unsubscribe()
def from_foldersystem(
*,
paths: Iterable[str] | None = None,
backend: str = "auto",
change_types: Iterable[FolderChangeType] | None = None,
tags: Iterable[str] = (),
interval: float = 0.5,
auto_start: bool = True,
) -> Tuple[Any, FolderDispatcher]:
"""顶层工厂函数: 返回 (Observable[FolderData], FolderDispatcher) 二元组。
Subject 拥有 pipe 方法,可链式组合响应式算子:
>>> obs, d = from_foldersystem(paths=["./src"])
>>> obs.pipe(
... ops.filter(lambda f: f.change_type == FolderChangeType.CREATED),
... ).subscribe(on_next=lambda f: print("新建目录:", f.path))
"""
subject = FolderSubject(
paths=paths,
backend=backend,
change_types=change_types,
tags=tags,
interval=interval,
auto_start=auto_start,
)
obs: Any = subject.pipe()
dispatcher = subject.dispatcher
return obs, dispatcher
def write_to_foldersystem(
dispatcher: Optional[FolderDispatcher] = None,
mode: str = "create",
) -> Callable[[Any], Any]:
"""响应式操作符:把上游每一项写入目录系统(mkdir / rmdir)。
上游可接受:
FolderData → 用其 path / change_type
str → 作为目录路径(创建空目录)
dict → {"path": str, "mode": "create" | "delete", "tags": [...]}
tuple/list → (path, mode)
"""
def operator(source_observable: Any) -> Any:
def subscribe(observer: Any) -> Subscription:
def on_next(item: Any) -> None:
try:
path: str = ""
actual_mode = mode
tags: List[str] = []
meta: Dict[str, Any] = {}
if isinstance(item, FolderData):
path = item.path
tags = list(item.tags)
meta = dict(item.metadata)
elif isinstance(item, str):
path = item
elif isinstance(item, dict):
path = item.get("path", "") or ""
if item.get("mode"):
actual_mode = str(item.get("mode"))
tags = list(item.get("tags") or [])
meta = dict(item.get("metadata") or {})
elif isinstance(item, (list, tuple)):
items = list(item)
path = str(items[0]) if items else ""
if len(items) > 1:
actual_mode = str(items[1])
if not path:
return
try:
if actual_mode in ("delete", "remove", "rmdir"):
if os.path.isdir(path):
try:
os.rmdir(path)
except OSError:
import shutil
shutil.rmtree(path, ignore_errors=True)
ct = FolderChangeType.DELETED
else:
os.makedirs(path, exist_ok=True)
ct = FolderChangeType.CREATED
fd = FolderData.now(
path=path,
change_type=ct,
tags=tags,
metadata=meta,
)
if callable(observer):
observer(fd)
elif hasattr(observer, "on_next"):
try:
observer.on_next(fd)
except Exception:
pass
except Exception as e:
log.debug("write_to_foldersystem 操作异常: %s", e)
if dispatcher is not None:
try:
dispatcher._error_count += 1
except Exception:
pass
except Exception as e:
log.debug("write_to_foldersystem operator 异常: %s", e)
return source_observable.subscribe(on_next=on_next)
try:
return Observable(_PyObservable(subscribe))
except Exception:
return _FolderOpObservable(subscribe)
return operator
class _FolderOpObservable:
"""当真正的 Observable 不可用时的最小 fallback 实现。"""
def __init__(self, subscribe: Callable[[Any], Subscription]) -> None:
self._subscribe = subscribe
def subscribe(self, on_next: Any = None,
on_error: Any = None, on_completed: Any = None) -> Subscription:
if callable(on_next):
return self._subscribe(on_next)
return self._subscribe(lambda v: None)
def pipe(self, *operators: Callable[[Any], Any]) -> Any:
current: Any = self
for op in operators:
try:
current = op(current)
except Exception:
continue
return current
_RUST_OVERRIDE_OK = False
if _rust_mod is not None:
try:
if hasattr(_rust_mod, "FolderChangeType"):
pass
if hasattr(_rust_mod, "FolderData"):
_RustFolderData = _rust_mod.FolderData
if hasattr(_rust_mod, "FolderDispatcher"):
_RustFolderDispatcher = _rust_mod.FolderDispatcher
if hasattr(_rust_mod, "FolderSubject"):
_RustFolderSubject = _rust_mod.FolderSubject
if hasattr(_rust_mod, "FolderObserver"):
_RustFolderObserver = _rust_mod.FolderObserver
_RUST_OVERRIDE_OK = True
except Exception:
_RUST_OVERRIDE_OK = False
def has_rust_backend() -> bool:
"""返回当前是否有可用的 Rust 扩展实现。"""
return _RUST_OVERRIDE_OK
__all__ = [
"FolderChangeType",
"FolderData",
"FolderDispatcher",
"FolderSubject",
"FolderObserver",
"from_foldersystem",
"write_to_foldersystem",
"has_rust_backend",
"Subscription",
"Observable",
]