bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
413 lines
15 KiB
Python
413 lines
15 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
import posixpath
|
|
from collections import Counter
|
|
from datetime import datetime, timezone
|
|
|
|
from aiohttp import web
|
|
|
|
import mirage.core.onedrive._client as onedrive_client
|
|
|
|
MODIFIED = datetime(2026, 3, 31,
|
|
tzinfo=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
def _norm(path: str) -> str:
|
|
return path.strip("/")
|
|
|
|
|
|
def _parse_range(header: str | None, size: int) -> tuple[int, int]:
|
|
if not header or not header.startswith("bytes="):
|
|
return 0, size
|
|
spec = header[len("bytes="):]
|
|
start_s, _, end_s = spec.partition("-")
|
|
start = int(start_s) if start_s else 0
|
|
end = int(end_s) + 1 if end_s else size
|
|
return start, min(end, size)
|
|
|
|
|
|
class FakeGraph:
|
|
|
|
def __init__(self) -> None:
|
|
self.files: dict[str, dict] = {}
|
|
self.dirs: set[str] = {""}
|
|
self.base = ""
|
|
self._seq = 0
|
|
|
|
def _tag(self) -> str:
|
|
self._seq += 1
|
|
return f"tag{self._seq}"
|
|
|
|
def _ensure_parents(self, path: str) -> None:
|
|
parent = posixpath.dirname(path)
|
|
while parent:
|
|
self.dirs.add(parent)
|
|
parent = posixpath.dirname(parent)
|
|
|
|
def _write_file(self, path: str, content: bytes) -> dict:
|
|
path = _norm(path)
|
|
self._ensure_parents(path)
|
|
prior = self.files.get(path)
|
|
versions = prior["versions"] if prior else []
|
|
ctag = self._tag()
|
|
versions = versions + [{
|
|
"id": ctag,
|
|
"lastModifiedDateTime": MODIFIED,
|
|
"content": content,
|
|
}]
|
|
entry = {
|
|
"content": content,
|
|
"ctag": ctag,
|
|
"etag": ctag,
|
|
"modified": MODIFIED,
|
|
"versions": versions,
|
|
}
|
|
self.files[path] = entry
|
|
return self._file_item(path)
|
|
|
|
def _children(self, dirpath: str) -> list[str]:
|
|
dirpath = _norm(dirpath)
|
|
names: set[str] = set()
|
|
for f in self.files:
|
|
if posixpath.dirname(f) == dirpath:
|
|
names.add(posixpath.basename(f))
|
|
for d in self.dirs:
|
|
if d and posixpath.dirname(d) == dirpath:
|
|
names.add(posixpath.basename(d))
|
|
return sorted(names)
|
|
|
|
def _file_item(self, path: str) -> dict:
|
|
entry = self.files[path]
|
|
return {
|
|
"id":
|
|
entry["ctag"],
|
|
"name":
|
|
posixpath.basename(path),
|
|
"size":
|
|
len(entry["content"]),
|
|
"lastModifiedDateTime":
|
|
entry["modified"],
|
|
"cTag":
|
|
entry["ctag"],
|
|
"eTag":
|
|
entry["etag"],
|
|
"file": {
|
|
"mimeType": "application/octet-stream"
|
|
},
|
|
"@microsoft.graph.downloadUrl":
|
|
f"{self.base}/download/{path}",
|
|
"versions": [{
|
|
"id": v["id"],
|
|
"lastModifiedDateTime": v["lastModifiedDateTime"],
|
|
} for v in entry["versions"]],
|
|
}
|
|
|
|
def _folder_size(self, path: str) -> int:
|
|
path = _norm(path)
|
|
prefix = path + "/" if path else ""
|
|
return sum(
|
|
len(entry["content"]) for f, entry in self.files.items()
|
|
if not path or f == path or f.startswith(prefix))
|
|
|
|
def _folder_item(self, path: str) -> dict:
|
|
return {
|
|
"id": f"folder:{path}" if path else "root",
|
|
"name": posixpath.basename(path) if path else "root",
|
|
"size": self._folder_size(path),
|
|
"lastModifiedDateTime": MODIFIED,
|
|
"folder": {
|
|
"childCount": len(self._children(path))
|
|
},
|
|
}
|
|
|
|
def _item(self, path: str) -> dict | None:
|
|
path = _norm(path)
|
|
if path in self.files:
|
|
return self._file_item(path)
|
|
if path in self.dirs:
|
|
return self._folder_item(path)
|
|
return None
|
|
|
|
def _delete(self, path: str) -> bool:
|
|
path = _norm(path)
|
|
if path in self.files:
|
|
del self.files[path]
|
|
return True
|
|
if path in self.dirs:
|
|
self.dirs.discard(path)
|
|
for f in list(self.files):
|
|
if f == path or f.startswith(path + "/"):
|
|
del self.files[f]
|
|
for d in list(self.dirs):
|
|
if d == path or d.startswith(path + "/"):
|
|
self.dirs.discard(d)
|
|
return True
|
|
return False
|
|
|
|
|
|
def _not_found() -> web.Response:
|
|
return web.json_response(
|
|
{"error": {
|
|
"code": "itemNotFound",
|
|
"message": "Item not found"
|
|
}},
|
|
status=404)
|
|
|
|
|
|
def _parse_item_path(path: str) -> tuple[str, str]:
|
|
idx = path.find("/root")
|
|
if idx < 0:
|
|
return "", ""
|
|
rest = path[idx + len("/root"):]
|
|
if rest in ("", "/"):
|
|
return "", ""
|
|
if rest == "/children":
|
|
return "", "children"
|
|
rest = rest[1:] if rest.startswith(":") else rest
|
|
item_part, sep, action = rest.partition(":/")
|
|
return _norm(item_part), (action if sep else "")
|
|
|
|
|
|
def _ref_parent(ref_path: str) -> str:
|
|
after = ref_path.split("root", 1)[-1]
|
|
after = after[1:] if after.startswith(":") else after
|
|
return _norm(after)
|
|
|
|
|
|
class GraphServer:
|
|
|
|
def __init__(self, state: FakeGraph) -> None:
|
|
self.state = state
|
|
self.uploads: dict[str, dict] = {}
|
|
self.calls: Counter = Counter()
|
|
self._upload_seq = 0
|
|
|
|
async def handle(self, request: web.Request) -> web.StreamResponse:
|
|
path = request.path
|
|
method = request.method
|
|
if path.startswith("/download/"):
|
|
self.calls["download"] += 1
|
|
return self._serve_bytes(request, path[len("/download/"):])
|
|
if path.startswith("/upload/"):
|
|
return await self._upload(request, path[len("/upload/"):])
|
|
if path.startswith("/monitor/"):
|
|
return web.json_response({"status": "completed"})
|
|
item_path, action = _parse_item_path(path)
|
|
if method == "GET":
|
|
kind = action if action in ("children", "content") else "item"
|
|
self.calls[kind] += 1
|
|
return await self._drive(request, method, item_path, action)
|
|
|
|
async def _drive(self, request: web.Request, method: str, item_path: str,
|
|
action: str) -> web.StreamResponse:
|
|
state = self.state
|
|
if action == "children":
|
|
if method == "POST":
|
|
return await self._mkdir(request, item_path)
|
|
return self._children_response(item_path)
|
|
if action == "content":
|
|
if method == "PUT":
|
|
data = await request.read()
|
|
return web.json_response(state._write_file(item_path, data))
|
|
return self._content_response(request, item_path)
|
|
if action == "createUploadSession":
|
|
return self._create_upload(item_path)
|
|
if action == "copy":
|
|
return await self._copy(request, item_path)
|
|
if action.startswith("versions/") and action.endswith("/content"):
|
|
vid = action[len("versions/"):-len("/content")]
|
|
return self._version_content(request, item_path, vid)
|
|
if action.endswith("/restoreVersion"):
|
|
return web.Response(status=204)
|
|
if action == "versions":
|
|
return self._versions_response(item_path)
|
|
if method == "DELETE":
|
|
return web.Response(
|
|
status=204) if state._delete(item_path) else _not_found()
|
|
if method == "PATCH":
|
|
return await self._patch(request, item_path)
|
|
item = state._item(item_path)
|
|
return web.json_response(item) if item is not None else _not_found()
|
|
|
|
def _children_response(self, item_path: str) -> web.Response:
|
|
state = self.state
|
|
if item_path and item_path not in state.dirs:
|
|
return _not_found()
|
|
value = [
|
|
state._item(posixpath.join(item_path, name))
|
|
for name in state._children(item_path)
|
|
]
|
|
return web.json_response({"value": value})
|
|
|
|
def _content_response(self, request: web.Request,
|
|
item_path: str) -> web.Response:
|
|
return self._serve_bytes(request, item_path)
|
|
|
|
def _serve_bytes(self, request: web.Request, path: str) -> web.Response:
|
|
entry = self.state.files.get(_norm(path))
|
|
if entry is None:
|
|
return _not_found()
|
|
return self._range_body(request, entry["content"])
|
|
|
|
def _range_body(self, request: web.Request,
|
|
content: bytes) -> web.Response:
|
|
header = request.headers.get("Range")
|
|
start, end = _parse_range(header, len(content))
|
|
body = content[start:end]
|
|
if header and header.startswith("bytes=") and start < end:
|
|
return web.Response(status=206,
|
|
body=body,
|
|
content_type="application/octet-stream",
|
|
headers={
|
|
"Content-Range":
|
|
f"bytes {start}-{end - 1}/{len(content)}"
|
|
})
|
|
return web.Response(body=body, content_type="application/octet-stream")
|
|
|
|
def _versions_response(self, item_path: str) -> web.Response:
|
|
entry = self.state.files.get(_norm(item_path))
|
|
if entry is None:
|
|
return _not_found()
|
|
value = [{
|
|
"id": v["id"],
|
|
"lastModifiedDateTime": v["lastModifiedDateTime"],
|
|
} for v in entry["versions"]]
|
|
return web.json_response({"value": value})
|
|
|
|
def _version_content(self, request: web.Request, item_path: str,
|
|
vid: str) -> web.Response:
|
|
entry = self.state.files.get(_norm(item_path))
|
|
if entry is None:
|
|
return _not_found()
|
|
for v in entry["versions"]:
|
|
if v["id"] == vid:
|
|
return self._range_body(request, v["content"])
|
|
return _not_found()
|
|
|
|
async def _mkdir(self, request: web.Request, parent: str) -> web.Response:
|
|
body = await request.json()
|
|
name = body.get("name", "")
|
|
target = _norm(posixpath.join(parent, name))
|
|
self.state._ensure_parents(target)
|
|
self.state.dirs.add(target)
|
|
return web.json_response(self.state._folder_item(target))
|
|
|
|
async def _patch(self, request: web.Request,
|
|
item_path: str) -> web.Response:
|
|
state = self.state
|
|
item_path = _norm(item_path)
|
|
body = await request.json()
|
|
name = body.get("name") or posixpath.basename(item_path)
|
|
ref = body.get("parentReference", {})
|
|
if "path" in ref:
|
|
parent = _ref_parent(ref["path"])
|
|
else:
|
|
parent = posixpath.dirname(item_path)
|
|
dest = _norm(posixpath.join(parent, name))
|
|
if item_path in state.files:
|
|
entry = state.files.pop(item_path)
|
|
state._ensure_parents(dest)
|
|
state.files[dest] = entry
|
|
return web.json_response(state._file_item(dest))
|
|
if item_path in state.dirs:
|
|
self._move_dir(item_path, dest)
|
|
return web.json_response(state._folder_item(dest))
|
|
return _not_found()
|
|
|
|
def _move_dir(self, src: str, dest: str) -> None:
|
|
state = self.state
|
|
state.dirs.discard(src)
|
|
state.dirs.add(dest)
|
|
state._ensure_parents(dest)
|
|
for f in list(state.files):
|
|
if f == src or f.startswith(src + "/"):
|
|
entry = state.files.pop(f)
|
|
state.files[dest + f[len(src):]] = entry
|
|
for d in list(state.dirs):
|
|
if d != src and d.startswith(src + "/"):
|
|
state.dirs.discard(d)
|
|
state.dirs.add(dest + d[len(src):])
|
|
|
|
async def _copy(self, request: web.Request,
|
|
item_path: str) -> web.Response:
|
|
state = self.state
|
|
item_path = _norm(item_path)
|
|
body = await request.json()
|
|
name = body.get("name") or posixpath.basename(item_path)
|
|
parent = _ref_parent(body.get("parentReference", {}).get("path", ""))
|
|
dest = _norm(posixpath.join(parent, name))
|
|
if item_path in state.files:
|
|
state._write_file(dest, state.files[item_path]["content"])
|
|
elif item_path in state.dirs:
|
|
self._copy_dir(item_path, dest)
|
|
else:
|
|
return _not_found()
|
|
resp = web.Response(status=202)
|
|
resp.headers["Location"] = f"{state.base}/monitor/{dest}"
|
|
return resp
|
|
|
|
def _copy_dir(self, src: str, dest: str) -> None:
|
|
state = self.state
|
|
state.dirs.add(dest)
|
|
state._ensure_parents(dest)
|
|
for f in list(state.files):
|
|
if f == src or f.startswith(src + "/"):
|
|
state._write_file(dest + f[len(src):],
|
|
state.files[f]["content"])
|
|
for d in list(state.dirs):
|
|
if d != src and d.startswith(src + "/"):
|
|
state.dirs.add(dest + d[len(src):])
|
|
|
|
def _create_upload(self, item_path: str) -> web.Response:
|
|
self._upload_seq += 1
|
|
token = f"{_norm(item_path)}#{self._upload_seq}"
|
|
self.uploads[token] = {"path": _norm(item_path), "buffer": bytearray()}
|
|
return web.json_response({
|
|
"uploadUrl": f"{self.state.base}/upload/{token}",
|
|
"expirationDateTime": MODIFIED,
|
|
})
|
|
|
|
async def _upload(self, request: web.Request, token: str) -> web.Response:
|
|
session = self.uploads.get(token)
|
|
if session is None:
|
|
return _not_found()
|
|
chunk = await request.read()
|
|
session["buffer"].extend(chunk)
|
|
content_range = request.headers.get("Content-Range", "")
|
|
total = int(content_range.rsplit("/", 1)[-1]) if "/" in content_range \
|
|
else len(session["buffer"])
|
|
if len(session["buffer"]) >= total:
|
|
item = self.state._write_file(session["path"],
|
|
bytes(session["buffer"]))
|
|
del self.uploads[token]
|
|
return web.json_response(item, status=201)
|
|
return web.json_response(
|
|
{"nextExpectedRanges": [f"{len(session['buffer'])}-"]}, status=202)
|
|
|
|
|
|
async def start_fake_graph() -> tuple[FakeGraph, "GraphServer", web.AppRunner]:
|
|
state = FakeGraph()
|
|
server = GraphServer(state)
|
|
app = web.Application()
|
|
app.router.add_route("*", "/{tail:.*}", server.handle)
|
|
runner = web.AppRunner(app)
|
|
await runner.setup()
|
|
site = web.TCPSite(runner, "127.0.0.1", 0)
|
|
await site.start()
|
|
port = site._server.sockets[0].getsockname()[1]
|
|
state.base = f"http://127.0.0.1:{port}"
|
|
onedrive_client.GRAPH_API = state.base
|
|
return state, server, runner
|