knowflow 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- knowflow/__init__.py +4 -0
- knowflow/__main__.py +3 -0
- knowflow/adapters/knowledge/__init__.py +42 -0
- knowflow/adapters/knowledge/local.py +43 -0
- knowflow/adapters/knowledge/obsidian.py +64 -0
- knowflow/adapters/storage/__init__.py +62 -0
- knowflow/adapters/storage/git.py +155 -0
- knowflow/adapters/storage/local.py +110 -0
- knowflow/adapters/storage/quark.py +225 -0
- knowflow/adapters/storage/s3.py +118 -0
- knowflow/adapters/storage/webdav.py +196 -0
- knowflow/cli/__init__.py +265 -0
- knowflow/cli/template_manager.py +100 -0
- knowflow/core/__init__.py +10 -0
- knowflow/core/knowledge.py +85 -0
- knowflow/core/pipeline.py +119 -0
- knowflow/core/storage.py +167 -0
- knowflow/dashboard/__init__.py +355 -0
- knowflow/templates/daily-journal/plugin.yaml +21 -0
- knowflow/templates/football-prediction/plugin.yaml +22 -0
- knowflow/templates/project-wiki/plugin.yaml +20 -0
- knowflow/templates/research-notes/plugin.yaml +26 -0
- knowflow/templates/stock-analysis/plugin.yaml +22 -0
- knowflow/templates/web-monitor/plugin.yaml +20 -0
- knowflow/templates/worldcup-tracker/plugin.yaml +18 -0
- knowflow/transforms/__init__.py +6 -0
- knowflow/transforms/archive.py +87 -0
- knowflow/transforms/markdown_export.py +110 -0
- knowflow/transforms/profile_index.py +89 -0
- knowflow-0.1.0.dist-info/METADATA +264 -0
- knowflow-0.1.0.dist-info/RECORD +35 -0
- knowflow-0.1.0.dist-info/WHEEL +5 -0
- knowflow-0.1.0.dist-info/entry_points.txt +2 -0
- knowflow-0.1.0.dist-info/licenses/LICENSE +21 -0
- knowflow-0.1.0.dist-info/top_level.txt +1 -0
knowflow/__init__.py
ADDED
knowflow/__main__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""knowflow/adapters/knowledge/__init__.py — Knowledge adapter registry"""
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from knowflow.core.knowledge import KnowledgeAdapter, KnowledgePage
|
|
5
|
+
|
|
6
|
+
_REGISTRY = {}
|
|
7
|
+
|
|
8
|
+
def register(name: str, cls):
|
|
9
|
+
_REGISTRY[name.lower()] = cls
|
|
10
|
+
|
|
11
|
+
def create_knowledge(name: str, config: Optional[dict] = None, **kwargs) -> KnowledgeAdapter:
|
|
12
|
+
"""Factory: create a knowledge-base adapter by name.
|
|
13
|
+
|
|
14
|
+
两种调用方式都支持:
|
|
15
|
+
create_knowledge("obsidian", path="~/my-vault") # 扁平参数
|
|
16
|
+
create_knowledge("obsidian", config={"path": "..."}) # 显式 config 字典
|
|
17
|
+
"""
|
|
18
|
+
name = name.lower()
|
|
19
|
+
if name not in _REGISTRY:
|
|
20
|
+
available = list(_REGISTRY.keys())
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"Unknown knowledge base: '{name}'. Available: {available}"
|
|
23
|
+
)
|
|
24
|
+
if config is None:
|
|
25
|
+
config = dict(kwargs) if kwargs else None
|
|
26
|
+
elif kwargs:
|
|
27
|
+
config = {**config, **kwargs}
|
|
28
|
+
return _REGISTRY[name](config)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# 别名:core/knowledge.py 的文档里写的是 create_kb,保持一致(曾导致 ImportError)
|
|
32
|
+
create_kb = create_knowledge
|
|
33
|
+
|
|
34
|
+
def list_backends() -> list[str]:
|
|
35
|
+
return sorted(_REGISTRY.keys())
|
|
36
|
+
|
|
37
|
+
# Built-in adapters
|
|
38
|
+
from . import local as _local
|
|
39
|
+
register("local", _local.LocalKB)
|
|
40
|
+
|
|
41
|
+
from . import obsidian as _obsidian
|
|
42
|
+
register("obsidian", _obsidian.ObsidianKB)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
knowflow/adapters/knowledge/local.py — Local directory KB
|
|
3
|
+
Simple markdown files in a directory.
|
|
4
|
+
"""
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from knowflow.core.knowledge import KnowledgeAdapter, KnowledgePage, KnowledgeError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LocalKB(KnowledgeAdapter):
|
|
10
|
+
"""Local markdown directory knowledge base"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: dict = None):
|
|
13
|
+
super().__init__(config)
|
|
14
|
+
path = config.get("path", "~/knowflow-output") if config else "~/knowflow-output"
|
|
15
|
+
self.root = Path(path).expanduser().resolve()
|
|
16
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return f"Local ({self.root})"
|
|
20
|
+
|
|
21
|
+
def sync(self, pages: list[KnowledgePage]) -> dict:
|
|
22
|
+
synced = 0
|
|
23
|
+
errors = []
|
|
24
|
+
for page in pages:
|
|
25
|
+
try:
|
|
26
|
+
target = self.root / page.path
|
|
27
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
target.write_text(page.content, encoding="utf-8")
|
|
29
|
+
synced += 1
|
|
30
|
+
except Exception as e:
|
|
31
|
+
errors.append(f"{page.path}: {e}")
|
|
32
|
+
return {"synced": synced, "skipped": 0, "errors": errors}
|
|
33
|
+
|
|
34
|
+
def search(self, query: str, limit: int = 10) -> list[KnowledgePage]:
|
|
35
|
+
results = []
|
|
36
|
+
for md_file in sorted(self.root.rglob("*.md"))[:limit]:
|
|
37
|
+
if query.lower() in md_file.read_text(encoding="utf-8").lower():
|
|
38
|
+
results.append(KnowledgePage(
|
|
39
|
+
title=md_file.stem,
|
|
40
|
+
content=md_file.read_text(encoding="utf-8"),
|
|
41
|
+
path=str(md_file.relative_to(self.root)),
|
|
42
|
+
))
|
|
43
|
+
return results
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
knowflow/adapters/knowledge/obsidian.py — Obsidian vault KB
|
|
3
|
+
Syncs markdown pages to an Obsidian-compatible vault directory.
|
|
4
|
+
"""
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from knowflow.core.knowledge import KnowledgeAdapter, KnowledgePage, KnowledgeError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ObsidianKB(KnowledgeAdapter):
|
|
10
|
+
"""Obsidian vault knowledge base"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: dict = None):
|
|
13
|
+
super().__init__(config)
|
|
14
|
+
path = config.get("path", "~/obsidian-vault") if config else "~/obsidian-vault"
|
|
15
|
+
self.root = Path(path).expanduser().resolve()
|
|
16
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
self._ensure_vault()
|
|
18
|
+
|
|
19
|
+
def _ensure_vault(self):
|
|
20
|
+
"""Ensure .obsidian/ config exists for vault detection"""
|
|
21
|
+
obsidian_dir = self.root / ".obsidian"
|
|
22
|
+
obsidian_dir.mkdir(exist_ok=True)
|
|
23
|
+
app_file = obsidian_dir / "app.json"
|
|
24
|
+
if not app_file.exists():
|
|
25
|
+
import json
|
|
26
|
+
app_file.write_text(json.dumps({"promptDelete": False}, indent=2))
|
|
27
|
+
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return f"Obsidian ({self.root})"
|
|
30
|
+
|
|
31
|
+
def sync(self, pages: list[KnowledgePage]) -> dict:
|
|
32
|
+
synced = 0
|
|
33
|
+
errors = []
|
|
34
|
+
for page in pages:
|
|
35
|
+
try:
|
|
36
|
+
target = self.root / page.path
|
|
37
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
# Add Obsidian frontmatter tags if provided
|
|
39
|
+
content = page.content
|
|
40
|
+
if page.tags:
|
|
41
|
+
tags_line = "\n".join(f" - {t}" for t in page.tags)
|
|
42
|
+
frontmatter = f"---\ntags:\n{tags_line}\n---\n\n"
|
|
43
|
+
if not content.startswith("---"):
|
|
44
|
+
content = frontmatter + content
|
|
45
|
+
target.write_text(content, encoding="utf-8")
|
|
46
|
+
synced += 1
|
|
47
|
+
except Exception as e:
|
|
48
|
+
errors.append(f"{page.path}: {e}")
|
|
49
|
+
return {"synced": synced, "skipped": 0, "errors": errors}
|
|
50
|
+
|
|
51
|
+
def search(self, query: str, limit: int = 10) -> list[KnowledgePage]:
|
|
52
|
+
results = []
|
|
53
|
+
for md_file in sorted(self.root.rglob("*.md"))[:limit]:
|
|
54
|
+
try:
|
|
55
|
+
content = md_file.read_text(encoding="utf-8")
|
|
56
|
+
if query.lower() in content.lower():
|
|
57
|
+
results.append(KnowledgePage(
|
|
58
|
+
title=md_file.stem,
|
|
59
|
+
content=content,
|
|
60
|
+
path=str(md_file.relative_to(self.root)),
|
|
61
|
+
))
|
|
62
|
+
except Exception:
|
|
63
|
+
continue
|
|
64
|
+
return results
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""knowflow/adapters/storage/__init__.py — Storage adapter registry"""
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from knowflow.core.storage import StorageAdapter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Registry: name → class
|
|
8
|
+
_REGISTRY = {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(name: str, cls):
|
|
12
|
+
"""Register a storage adapter"""
|
|
13
|
+
_REGISTRY[name.lower()] = cls
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_storage(name: str, config: Optional[dict] = None, **kwargs) -> StorageAdapter:
|
|
17
|
+
"""Factory: create a storage adapter by name.
|
|
18
|
+
|
|
19
|
+
两种调用方式都支持:
|
|
20
|
+
create_storage("git", path="~/my-data") # 扁平参数(文档里的写法)
|
|
21
|
+
create_storage("git", config={"path": "..."}) # 显式 config 字典
|
|
22
|
+
"""
|
|
23
|
+
name = name.lower()
|
|
24
|
+
if name not in _REGISTRY:
|
|
25
|
+
available = list(_REGISTRY.keys())
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Unknown storage backend: '{name}'. "
|
|
28
|
+
f"Available: {available}"
|
|
29
|
+
)
|
|
30
|
+
if config is None:
|
|
31
|
+
config = dict(kwargs) if kwargs else None
|
|
32
|
+
elif kwargs:
|
|
33
|
+
config = {**config, **kwargs}
|
|
34
|
+
return _REGISTRY[name](config)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def list_backends() -> list[str]:
|
|
38
|
+
"""List all registered storage backends"""
|
|
39
|
+
return sorted(_REGISTRY.keys())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ─── Import and register built-in adapters ─────────────────
|
|
43
|
+
from . import local as _local_mod
|
|
44
|
+
register("local", _local_mod.LocalStorage)
|
|
45
|
+
|
|
46
|
+
from . import git as _git_mod
|
|
47
|
+
register("git", _git_mod.GitStorage)
|
|
48
|
+
|
|
49
|
+
# Optional adapters — import fails silently, available if deps installed
|
|
50
|
+
try:
|
|
51
|
+
from . import s3 as _s3_mod
|
|
52
|
+
register("s3", _s3_mod.S3Storage)
|
|
53
|
+
except ImportError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
# Quark 夸克网盘 — 依赖 kuake_cli,无额外Python包
|
|
57
|
+
from . import quark as _quark_mod # noqa
|
|
58
|
+
register("quark", _quark_mod.QuarkStorage)
|
|
59
|
+
|
|
60
|
+
# WebDAV — 依赖 requests,已预装
|
|
61
|
+
from . import webdav as _webdav_mod # noqa
|
|
62
|
+
register("webdav", _webdav_mod.WebDAVStorage)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
knowflow/adapters/storage/git.py — Git-backed storage
|
|
5
|
+
======================================================
|
|
6
|
+
Recommended backend: local files + full version history.
|
|
7
|
+
|
|
8
|
+
Config:
|
|
9
|
+
path: str (default: ~/knowflow-data)
|
|
10
|
+
"""
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import subprocess
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
import json
|
|
16
|
+
from knowflow.core.storage import (
|
|
17
|
+
StorageAdapter, StorageItem, StorageError, StorageNotFoundError
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GitStorage(StorageAdapter):
|
|
22
|
+
"""Git-backed storage with version history"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: dict = None):
|
|
25
|
+
super().__init__(config)
|
|
26
|
+
root = config.get("path", "~/knowflow-data") if config else "~/knowflow-data"
|
|
27
|
+
self.root = Path(root).expanduser().resolve()
|
|
28
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
self._git("init", check=False)
|
|
30
|
+
self._ensure_user()
|
|
31
|
+
|
|
32
|
+
def _resolve(self, path: str) -> Path:
|
|
33
|
+
clean = Path(path).as_posix().lstrip("/")
|
|
34
|
+
resolved = (self.root / clean).resolve()
|
|
35
|
+
if not str(resolved).startswith(str(self.root)):
|
|
36
|
+
raise StorageError(f"Path traversal blocked: {path}")
|
|
37
|
+
return resolved
|
|
38
|
+
|
|
39
|
+
def _git(self, *args, check: bool = True) -> str:
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["git", "-C", str(self.root), *args],
|
|
43
|
+
capture_output=True, text=True, timeout=30
|
|
44
|
+
)
|
|
45
|
+
if check and result.returncode != 0:
|
|
46
|
+
raise StorageError(f"Git: {result.stderr.strip() or 'unknown error'}")
|
|
47
|
+
return result.stdout.strip()
|
|
48
|
+
except subprocess.TimeoutExpired:
|
|
49
|
+
raise StorageError("Git command timed out")
|
|
50
|
+
|
|
51
|
+
def _ensure_user(self):
|
|
52
|
+
if not self._git("config", "user.name", check=False):
|
|
53
|
+
self._git("config", "user.name", "KnowFlow")
|
|
54
|
+
self._git("config", "user.email", "knowflow@local")
|
|
55
|
+
|
|
56
|
+
def read(self, path: str) -> str:
|
|
57
|
+
p = self._resolve(path)
|
|
58
|
+
if not p.exists():
|
|
59
|
+
raise StorageNotFoundError(f"Not found: {path}")
|
|
60
|
+
return p.read_text(encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
def read_binary(self, path: str) -> bytes:
|
|
63
|
+
p = self._resolve(path)
|
|
64
|
+
if not p.exists():
|
|
65
|
+
raise StorageNotFoundError(f"Not found: {path}")
|
|
66
|
+
return p.read_bytes()
|
|
67
|
+
|
|
68
|
+
def write(self, path: str, content: str) -> str:
|
|
69
|
+
p = self._resolve(path)
|
|
70
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
p.write_text(content, encoding="utf-8")
|
|
72
|
+
return self._git("add", str(p.relative_to(self.root)))
|
|
73
|
+
|
|
74
|
+
def write_binary(self, path: str, content: bytes) -> str:
|
|
75
|
+
p = self._resolve(path)
|
|
76
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
p.write_bytes(content)
|
|
78
|
+
return self._git("add", str(p.relative_to(self.root)))
|
|
79
|
+
|
|
80
|
+
def delete(self, path: str) -> bool:
|
|
81
|
+
p = self._resolve(path)
|
|
82
|
+
if not p.exists():
|
|
83
|
+
return False
|
|
84
|
+
if p.is_file():
|
|
85
|
+
p.unlink()
|
|
86
|
+
else:
|
|
87
|
+
# 与 LocalStorage 行为一致:目录递归删除
|
|
88
|
+
import shutil
|
|
89
|
+
shutil.rmtree(p)
|
|
90
|
+
self._git("add", "-A")
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
def exists(self, path: str) -> bool:
|
|
94
|
+
try:
|
|
95
|
+
return self._resolve(path).exists()
|
|
96
|
+
except StorageError:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def list(self, prefix: str = "") -> list[StorageItem]:
|
|
100
|
+
p = self._resolve(prefix) if prefix else self.root
|
|
101
|
+
if not p.exists():
|
|
102
|
+
return []
|
|
103
|
+
items = []
|
|
104
|
+
for child in sorted(p.iterdir()):
|
|
105
|
+
try:
|
|
106
|
+
stat = child.stat()
|
|
107
|
+
rel = child.relative_to(self.root).as_posix()
|
|
108
|
+
items.append(StorageItem(
|
|
109
|
+
path=rel, size=stat.st_size if child.is_file() else 0,
|
|
110
|
+
modified_at=stat.st_mtime, is_dir=child.is_dir(),
|
|
111
|
+
))
|
|
112
|
+
except OSError:
|
|
113
|
+
continue
|
|
114
|
+
return items
|
|
115
|
+
|
|
116
|
+
def walk(self, prefix: str = "") -> list[StorageItem]:
|
|
117
|
+
p = self._resolve(prefix) if prefix else self.root
|
|
118
|
+
if not p.exists():
|
|
119
|
+
return []
|
|
120
|
+
items = []
|
|
121
|
+
for root_dir, dirs, files in os.walk(str(p)):
|
|
122
|
+
root_path = Path(root_dir)
|
|
123
|
+
for name in files:
|
|
124
|
+
child = root_path / name
|
|
125
|
+
try:
|
|
126
|
+
stat = child.stat()
|
|
127
|
+
rel = child.relative_to(self.root).as_posix()
|
|
128
|
+
items.append(StorageItem(path=rel, size=stat.st_size, modified_at=stat.st_mtime))
|
|
129
|
+
except OSError:
|
|
130
|
+
continue
|
|
131
|
+
return sorted(items, key=lambda x: x.path)
|
|
132
|
+
|
|
133
|
+
def commit(self, message: str = "") -> str:
|
|
134
|
+
self._git("add", "-A")
|
|
135
|
+
status = self._git("status", "--porcelain")
|
|
136
|
+
if not status:
|
|
137
|
+
return ""
|
|
138
|
+
if not message:
|
|
139
|
+
message = f"Auto-commit {time.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
140
|
+
self._ensure_user()
|
|
141
|
+
self._git("commit", "-m", message)
|
|
142
|
+
return self._git("rev-parse", "HEAD")
|
|
143
|
+
|
|
144
|
+
def log(self, max_count: int = 10) -> list[dict]:
|
|
145
|
+
fmt = '{"id":"%H","message":"%s","timestamp":"%aI","author":"%an"}'
|
|
146
|
+
output = self._git("log", f"--max-count={max_count}", f"--format={fmt}", check=False)
|
|
147
|
+
if not output:
|
|
148
|
+
return []
|
|
149
|
+
return [json.loads(line) for line in output.split("\n") if line.strip()]
|
|
150
|
+
|
|
151
|
+
def diff(self, rev_a: str, rev_b: str, path: str = "") -> str:
|
|
152
|
+
args = ["diff", f"{rev_a}..{rev_b}"]
|
|
153
|
+
if path:
|
|
154
|
+
args.append("--", str(self._resolve(path)))
|
|
155
|
+
return self._git(*args, check=False)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
knowflow/adapters/storage/local.py — Local filesystem storage
|
|
5
|
+
===============================================================
|
|
6
|
+
Simplest backend: plain directory on disk, no versioning.
|
|
7
|
+
|
|
8
|
+
Config:
|
|
9
|
+
path: str (default: ~/knowflow-data)
|
|
10
|
+
"""
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import os
|
|
13
|
+
import time
|
|
14
|
+
from knowflow.core.storage import (
|
|
15
|
+
StorageAdapter, StorageItem, StorageError, StorageNotFoundError
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LocalStorage(StorageAdapter):
|
|
20
|
+
"""Local filesystem storage backend"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: dict = None):
|
|
23
|
+
super().__init__(config)
|
|
24
|
+
root = config.get("path", "~/knowflow-data") if config else "~/knowflow-data"
|
|
25
|
+
self.root = Path(root).expanduser().resolve()
|
|
26
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
def _resolve(self, path: str) -> Path:
|
|
29
|
+
clean = Path(path).as_posix().lstrip("/")
|
|
30
|
+
resolved = (self.root / clean).resolve()
|
|
31
|
+
if not str(resolved).startswith(str(self.root)):
|
|
32
|
+
raise StorageError(f"Path traversal blocked: {path}")
|
|
33
|
+
return resolved
|
|
34
|
+
|
|
35
|
+
def read(self, path: str) -> str:
|
|
36
|
+
p = self._resolve(path)
|
|
37
|
+
if not p.exists():
|
|
38
|
+
raise StorageNotFoundError(f"Not found: {path}")
|
|
39
|
+
return p.read_text(encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
def read_binary(self, path: str) -> bytes:
|
|
42
|
+
p = self._resolve(path)
|
|
43
|
+
if not p.exists():
|
|
44
|
+
raise StorageNotFoundError(f"Not found: {path}")
|
|
45
|
+
return p.read_bytes()
|
|
46
|
+
|
|
47
|
+
def write(self, path: str, content: str) -> str:
|
|
48
|
+
p = self._resolve(path)
|
|
49
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
p.write_text(content, encoding="utf-8")
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
def write_binary(self, path: str, content: bytes) -> str:
|
|
54
|
+
p = self._resolve(path)
|
|
55
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
p.write_bytes(content)
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
def delete(self, path: str) -> bool:
|
|
60
|
+
p = self._resolve(path)
|
|
61
|
+
if not p.exists():
|
|
62
|
+
return False
|
|
63
|
+
if p.is_file():
|
|
64
|
+
p.unlink()
|
|
65
|
+
else:
|
|
66
|
+
import shutil
|
|
67
|
+
shutil.rmtree(p)
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def exists(self, path: str) -> bool:
|
|
71
|
+
try:
|
|
72
|
+
return self._resolve(path).exists()
|
|
73
|
+
except StorageError:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def list(self, prefix: str = "") -> list[StorageItem]:
|
|
77
|
+
p = self._resolve(prefix) if prefix else self.root
|
|
78
|
+
if not p.exists():
|
|
79
|
+
return []
|
|
80
|
+
items = []
|
|
81
|
+
for child in sorted(p.iterdir()):
|
|
82
|
+
try:
|
|
83
|
+
stat = child.stat()
|
|
84
|
+
rel = child.relative_to(self.root).as_posix()
|
|
85
|
+
items.append(StorageItem(
|
|
86
|
+
path=rel,
|
|
87
|
+
size=stat.st_size if child.is_file() else 0,
|
|
88
|
+
modified_at=stat.st_mtime,
|
|
89
|
+
is_dir=child.is_dir(),
|
|
90
|
+
))
|
|
91
|
+
except OSError:
|
|
92
|
+
continue
|
|
93
|
+
return items
|
|
94
|
+
|
|
95
|
+
def walk(self, prefix: str = "") -> list[StorageItem]:
|
|
96
|
+
p = self._resolve(prefix) if prefix else self.root
|
|
97
|
+
if not p.exists():
|
|
98
|
+
return []
|
|
99
|
+
items = []
|
|
100
|
+
for root_dir, dirs, files in os.walk(str(p)):
|
|
101
|
+
root_path = Path(root_dir)
|
|
102
|
+
for name in files:
|
|
103
|
+
child = root_path / name
|
|
104
|
+
try:
|
|
105
|
+
stat = child.stat()
|
|
106
|
+
rel = child.relative_to(self.root).as_posix()
|
|
107
|
+
items.append(StorageItem(path=rel, size=stat.st_size, modified_at=stat.st_mtime))
|
|
108
|
+
except OSError:
|
|
109
|
+
continue
|
|
110
|
+
return sorted(items, key=lambda x: x.path)
|