DEV_FILES_API

DEV_FILES_API

Plan for a file CRUD API alongside /workflows/* in galaxy-workflow-development-webapp, enabling a browser-based text editor for workflows and adjacent files. API shape mirrors the Jupyter Contents API so existing frontend components (JupyterLab filebrowser, or any editor that speaks the shape) work with minimal glue.

Status

Phase 1 implemented in commit 9736882 (2026-04-04). 27 tests passing, mypy clean.

Phase 2 implemented in commit 071ed29 (2026-04-04). 43 tests passing, mypy clean.

Phase 3 implemented in commit 071ed29 (2026-04-04). 57 tests passing, mypy clean.

Scope

Endpoints

MethodPathPurpose
GET/contents/{path:path}Read file or list directory. ?content=0 skips the content body (lightweight listing).
POST/contents/{path:path}Create new untitled file or directory in {path} (path = parent dir). Body {type, ext?}.
PUT/contents/{path:path}Save file (create-or-replace). Body is a ContentsModel.
PATCH/contents/{path:path}Rename/move. Body {path: new_relative_path}.
DELETE/contents/{path:path}Delete file or directory.

Empty {path} (/contents/ or /contents) = directory root.

ContentsModel (response + PUT body)

class ContentsModel(BaseModel):
    name: str                 # basename
    path: str                 # relative path from configured directory
    type: Literal["file", "directory"]
    writable: bool
    created: datetime
    last_modified: datetime
    size: Optional[int]       # None for directories
    mimetype: Optional[str]   # None for directories
    format: Optional[Literal["text", "base64", "json"]]  # for files; None for directories
    content: Optional[Any]    # str (text), str (base64), list[ContentsModel] (directory), None if ?content=0

Differences from Jupyter:

Query parameters

Path safety

Shared helper _resolve_safe_path(rel_path) -> str:

  1. Normalize via os.path.abspath(os.path.join(_directory, rel_path)).
  2. Reject if result is not under _directory (prefix check with os.sep).
  3. os.path.realpath() check to catch symlink escape.
  4. Reject components matching a hardcoded ignore list (.git, __pycache__, .venv).

Used by every /contents/* handler. Returns 403 on escape, 404 on missing (for reads), 400 on invalid shape.

Create semantics (POST)

Jupyter’s POST creates an untitled file/directory inside {path}. Body:

{"type": "file", "ext": ".ga"}   // or {"type": "directory"}

Server picks a unique name (untitled.ga, untitled1.ga, …) and returns the full ContentsModel.

Useful for “New File” buttons. Save-as workflows use PUT /contents/{exact_path} instead.

Rename (PATCH)

{"path": "subdir/new_name.ga"}

Atomic os.rename, with destination path also passing _resolve_safe_path. Returns the new ContentsModel.

Conflict detection

Phase 1: none. Phase 2 (implemented): opt-in via If-Unmodified-Since request header on PUT (RFC 7232 HTTP-date). If the header is present and the on-disk mtime is newer than the supplied date (1s tolerance), respond 409 and leave the file untouched. Malformed header → 400. Absent header → no check (phase 1 behavior). Chosen over body-echoed last_modified because phase 1 tests ship stale body values on freshly-created files; a header is cleanly opt-in and closer to Jupyter (which doesn’t enforce at all).

No ETag header — keeps the contract JSON-envelope-only, matching Jupyter.

Post-write refresh of /workflows

When PUT/POST/DELETE/PATCH touches a *.ga or *.gxwf.yml file, inline-call discover_workflows(_directory) to refresh _workflows. Cheap. Eliminates the need for clients to call /workflows/refresh after an edit.

Module layout

Ignore list

Hardcoded defaults: .git, __pycache__, *.pyc, .venv, .ruff_cache, .pytest_cache, .mypy_cache. Override via CLI flag --contents-ignore PATTERN (repeatable, gitignore-style). Honor .gitignore at directory root — open question below.

Tests

Phasing

Frontend compatibility

JupyterLab’s @jupyterlab/filebrowser package is the obvious reusable frontend. It expects /api/contents prefix — either mount our API at /api/contents instead of /contents, or configure the frontend’s base URL. Recommend /api/contents for drop-in compatibility.

Alternative: any CodeMirror/Monaco-based editor with a ~50-line wrapper around these endpoints.

Unresolved questions

Resolved in phase 1:

Resolved in phase 2:

Still open:

  1. Max file size cap for reads/writes?
  2. Any authentication layer, or assume trusted localhost deployment?