mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-07 08:26:06 +00:00
* refactor(transfer): drop orphaned ingest step, make service discovery cross-platform - Remove ingest step: superseded by auto_resource (drop files under resource/ → watcher interprets them); its meta.json/<date>.md outputs had no consumers and tripped the auto_resource watcher. - Replace lsof/pgrep shell-outs in service_utils with psutil (per-process enumeration, no root needed on macOS) for Windows/macOS/Linux support. - Add cross-platform test coverage for _pid_on_port / _scan_reme_procs. - Deps: +psutil, -filelock (only used by the removed ingest lock). * chore(release): bump version to 0.4.0.5
98 lines
4 KiB
Python
98 lines
4 KiB
Python
"""``file_upload`` — copy a file from the local filesystem into the workspace.
|
|
|
|
Symmetric counterpart to ``file_download``: source is on the local
|
|
filesystem, target is under the workspace.
|
|
|
|
``src_path`` (filesystem source) is an absolute path on the host
|
|
filesystem (the file to copy in). Returns ``error="not found"``
|
|
when the file isn't on disk.
|
|
|
|
``dst_path`` is a path relative to the workspace, **required**, and must
|
|
include a directory component so the caller is always explicit about
|
|
where in the workspace the file lands. ``overwrite`` defaults to False —
|
|
callers must opt in to clobber an existing destination.
|
|
|
|
To capture an external asset into the resource bucket, upload it under
|
|
``resource/<YYYY-MM-DD>/`` (or loosely under ``resource/``); the resource
|
|
file watcher then interprets it into a source-linked daily note.
|
|
"""
|
|
|
|
import mimetypes
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from ..base_step import BaseStep
|
|
|
|
from ...components import R
|
|
|
|
|
|
@R.register("upload_step")
|
|
class UploadStep(BaseStep):
|
|
"""Copy ``src_path`` (on local fs) to ``dst_path`` (under the workspace)."""
|
|
|
|
async def execute(self):
|
|
assert self.context is not None
|
|
src_path: str = self.context.get("src_path", "") or ""
|
|
dst_path: str = self.context.get("dst_path", "") or ""
|
|
overwrite: bool = bool(self.context.get("overwrite", False))
|
|
payload = await self._upload(src_path, dst_path, overwrite)
|
|
if "error" in payload:
|
|
self.context.response.success = False
|
|
self.context.response.answer = f"Error: {payload['error']}"
|
|
self.logger.info(
|
|
f"[{self.name}] upload failed src={src_path} dst={dst_path} error={payload['error']!r}",
|
|
)
|
|
else:
|
|
self.context.response.success = True
|
|
self.context.response.answer = f"Uploaded {src_path} → {dst_path} ({payload['size']} bytes)"
|
|
self.logger.info(
|
|
f"[{self.name}] src={src_path} dst={dst_path} size={payload['size']} mime={payload['mime']}",
|
|
)
|
|
self.context.response.metadata.update(payload)
|
|
|
|
async def _upload(
|
|
self,
|
|
src_path: str,
|
|
dst_path: str,
|
|
overwrite: bool,
|
|
) -> dict: # pylint: disable=too-many-return-statements
|
|
# pylint: disable=too-many-return-statements
|
|
if not src_path:
|
|
return {"src_path": src_path, "error": "src_path is required"}
|
|
if not dst_path:
|
|
return {"dst_path": dst_path, "error": "dst_path is required"}
|
|
|
|
src_abs = Path(src_path).resolve()
|
|
if not src_abs.is_file():
|
|
return {"src_path": src_path, "error": "not found"}
|
|
if "/" not in dst_path:
|
|
return {
|
|
"dst_path": dst_path,
|
|
"error": "dst_path must be relative to the workspace with a directory component",
|
|
}
|
|
if Path(dst_path).is_absolute():
|
|
return {"dst_path": dst_path, "error": "dst_path must be relative to the workspace"}
|
|
workspace_dir = Path(self.file_store.workspace_path or ".").resolve()
|
|
dst_abs = (workspace_dir / dst_path).resolve()
|
|
try:
|
|
dst_abs.relative_to(workspace_dir)
|
|
except ValueError:
|
|
return {"dst_path": dst_path, "error": "dst_path must stay inside the workspace"}
|
|
if dst_abs == src_abs:
|
|
return {"src_path": src_path, "dst_path": dst_path, "error": "src_path and dst_path are the same"}
|
|
if dst_abs.is_dir():
|
|
return {"dst_path": dst_path, "error": "destination is a directory"}
|
|
if dst_abs.exists() and not overwrite:
|
|
return {
|
|
"src_path": src_path,
|
|
"dst_path": dst_path,
|
|
"error": "destination exists; pass overwrite=True",
|
|
}
|
|
dst_abs.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(src_abs, dst_abs)
|
|
return {
|
|
"src_path": src_path,
|
|
"dst_path": dst_path,
|
|
"size": dst_abs.stat().st_size,
|
|
"mime": mimetypes.guess_type(dst_abs.name)[0] or "application/octet-stream",
|
|
}
|