effd4e3794
* feat(metadata): chmod/chown/touch via setattr with namespace overlay fallback * test(integ): guard cases.py mirage import against the redis.py sys.path shadow * fix(metadata): review fixes for metadata ops - bump snapshot format to v3 (symlinks key renamed to nodes) - disk chmod keeps owner access on the real inode, sidecar reports requested bits - touch on stat-only mounts reports Read-only file system instead of crashing - content writes clear overlay mtime/atime so touched times do not freeze - mv drops the replaced destination's node meta and keys moves into linked dirs under the real path - glob rm purges node meta via segment-wise pattern match - symbolic chmod on directories builds on 755 - touch -r resolves relative references against cwd - extract shared builtins helpers (flag/operand parsing) into shared.py * fix(metadata): typed setattr kwargs, non-null node stderr for the mypy ratchet
104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
import asyncio
|
|
import os
|
|
import shlex
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
from mirage import MountMode, Workspace
|
|
from mirage.resource.dify import DifyConfig, DifyResource
|
|
from mirage.types import PathSpec
|
|
|
|
load_dotenv(".env.development")
|
|
|
|
|
|
def require_env(name: str) -> str:
|
|
value = os.environ.get(name)
|
|
if not value:
|
|
raise RuntimeError(f"Missing required environment variable: {name}")
|
|
return value
|
|
|
|
|
|
def build_resource() -> DifyResource:
|
|
config = DifyConfig(
|
|
api_key=require_env("DIFY_API_KEY"),
|
|
base_url=os.environ.get("DIFY_BASE_URL", "https://api.dify.ai/v1"),
|
|
dataset_id=require_env("DIFY_DATASET_ID"),
|
|
slug_metadata_name=os.environ.get("DIFY_SLUG_METADATA_NAME", "slug"),
|
|
)
|
|
return DifyResource(config=config)
|
|
|
|
|
|
async def run(ws: Workspace, command: str, max_chars: int = 800) -> str:
|
|
result = await ws.execute(command)
|
|
stdout = await result.stdout_str()
|
|
stderr = (result.stderr or b"").decode(errors="replace")
|
|
print(f"$ {command}")
|
|
if stdout:
|
|
output = stdout.strip()
|
|
if len(output) > max_chars:
|
|
output = output[:max_chars] + "\n..."
|
|
print(output)
|
|
if stderr:
|
|
print(stderr.strip())
|
|
print(f"[exit={result.exit_code}]\n")
|
|
return stdout
|
|
|
|
|
|
async def first_document_path(ws: Workspace) -> str | None:
|
|
output = await run(ws, "find /knowledge/ -type f | head -n 1")
|
|
path = output.strip()
|
|
return path or None
|
|
|
|
|
|
async def main() -> None:
|
|
resource = build_resource()
|
|
ws = Workspace({"/knowledge/": resource}, mode=MountMode.READ)
|
|
|
|
print("=== Dify Knowledge ===\n")
|
|
|
|
await run(ws, "ls /knowledge/")
|
|
await run(ws, "find /knowledge/ -type f | head -n 10")
|
|
|
|
first_path = await first_document_path(ws)
|
|
if first_path is None:
|
|
print("No completed documents found in the Dify dataset.")
|
|
return
|
|
|
|
quoted_path = shlex.quote(first_path)
|
|
print(f"First document: {first_path}\n")
|
|
|
|
await run(ws, f"cat {quoted_path}", max_chars=1200)
|
|
await run(ws, f"head -n 5 {quoted_path}")
|
|
await run(ws, f"tail -n 5 {quoted_path}")
|
|
await run(ws, f"wc {quoted_path}")
|
|
|
|
# chmod/chown/touch never hit the Dify API: attrs land in the
|
|
# workspace namespace (durable, snapshot-captured) and merge into
|
|
# dispatch-level stat.
|
|
print(f"=== metadata overlay on {first_path} ===")
|
|
meta_res = await ws.execute(f"chmod 640 {quoted_path}"
|
|
f" && chown 500:dev {quoted_path}"
|
|
f" && touch -t 202601021530 {quoted_path}")
|
|
print(f" chmod/chown/touch exit={meta_res.exit_code}")
|
|
meta_st, _ = await ws.dispatch("stat", PathSpec.from_str_path(first_path))
|
|
print(f" dispatch stat: mode={oct(meta_st.mode)[2:]} uid={meta_st.uid} "
|
|
f"gid={meta_st.gid} mtime={meta_st.modified}")
|
|
|
|
query = os.environ.get("DIFY_EXAMPLE_QUERY", "getting started")
|
|
quoted_query = shlex.quote(query)
|
|
await run(ws, f"grep -i {quoted_query} {quoted_path}")
|
|
await run(ws,
|
|
f"search --method hybrid --top-k 5 {quoted_query} /knowledge/",
|
|
max_chars=1200)
|
|
|
|
records = ws.ops.records
|
|
network_bytes = ws.ops.network_bytes
|
|
cache_bytes = ws.ops.cache_bytes
|
|
print("=== Stats ===")
|
|
print(f"{len(records)} ops, {network_bytes} network bytes, "
|
|
f"{cache_bytes} cache bytes")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|