# retoor <retoor@molodetz.nl>
|
|
|
|
from .._shared import endpoint, field
|
|
|
|
GROUP = {
|
|
"slug": "project-files",
|
|
"title": "Project Filesystem",
|
|
"intro": """
|
|
# Project Filesystem
|
|
|
|
Each project carries a full virtual filesystem - directories and files - so a project can hold a
|
|
complete software project. Reading is public (anyone can browse a project's tree); creating,
|
|
editing, uploading, moving and deleting require the project owner. Text files are editable inline;
|
|
binary files are uploaded and served from `/static/uploads/project_files/...`.
|
|
|
|
Paths are relative POSIX paths inside the project (for example `src/main.py`). Parent directories
|
|
are created automatically on write, upload and mkdir. Paths containing `..`, null bytes or empty
|
|
segments are rejected.
|
|
|
|
Every endpoint follows the shared [Conventions & Errors](/docs/conventions.html) (auth, content
|
|
negotiation, status codes); see [Authentication](/docs/authentication.html) for the four ways to
|
|
sign requests.
|
|
""",
|
|
"endpoints": [
|
|
endpoint(
|
|
id="project-files-list",
|
|
method="GET",
|
|
path="/projects/{project_slug}/files",
|
|
title="List a project's files",
|
|
summary="Return the flat list of files and directories in a project.",
|
|
auth="public",
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
)
|
|
],
|
|
sample_response={
|
|
"project": {"uid": "PROJECT_UID", "slug": "PROJECT_SLUG"},
|
|
"files": [
|
|
{
|
|
"path": "src/main.py",
|
|
"name": "main.py",
|
|
"type": "file",
|
|
"is_binary": False,
|
|
"size": 42,
|
|
}
|
|
],
|
|
"is_owner": False,
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-raw",
|
|
method="GET",
|
|
path="/projects/{project_slug}/files/raw",
|
|
title="Read a project file",
|
|
summary="Return one file's metadata and (for text files) its content.",
|
|
auth="public",
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path",
|
|
"query",
|
|
"string",
|
|
True,
|
|
"src/main.py",
|
|
"Relative file path inside the project.",
|
|
),
|
|
],
|
|
sample_response={
|
|
"path": "src/main.py",
|
|
"name": "main.py",
|
|
"type": "file",
|
|
"is_binary": False,
|
|
"mime_type": "text/plain",
|
|
"size": 42,
|
|
"url": None,
|
|
"content": "print('hello')\n",
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-write",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/write",
|
|
title="Write a text file",
|
|
summary="Create or overwrite a text file; parent directories are created automatically.",
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path",
|
|
"form",
|
|
"string",
|
|
True,
|
|
"src/main.py",
|
|
"Relative file path.",
|
|
),
|
|
field(
|
|
"content",
|
|
"form",
|
|
"textarea",
|
|
True,
|
|
"print('hello')",
|
|
"Full file content (max 400000 chars).",
|
|
),
|
|
],
|
|
notes=["Owner only; non-owners get `403`. Invalid paths return `400`."],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/projects/PROJECT_SLUG/files",
|
|
"data": {"path": "src/main.py", "type": "file"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-lines",
|
|
method="GET",
|
|
path="/projects/{project_slug}/files/lines",
|
|
title="Read a line range",
|
|
summary="Read a 1-indexed inclusive line range of a text file. Returns lines plus total_lines for targeting edits.",
|
|
auth="public",
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path",
|
|
"query",
|
|
"string",
|
|
True,
|
|
"src/main.py",
|
|
"Relative file path.",
|
|
),
|
|
field(
|
|
"start",
|
|
"query",
|
|
"integer",
|
|
False,
|
|
"1",
|
|
"First line, 1-indexed (default 1).",
|
|
),
|
|
field(
|
|
"end",
|
|
"query",
|
|
"integer",
|
|
False,
|
|
"50",
|
|
"Last line inclusive; omit or -1 for end of file.",
|
|
),
|
|
],
|
|
notes=[
|
|
"Text files only; binary, directory, or missing paths return `404`."
|
|
],
|
|
sample_response={
|
|
"path": "src/main.py",
|
|
"start": 1,
|
|
"end": 2,
|
|
"total_lines": 2,
|
|
"lines": ["import os", "print(os.getcwd())"],
|
|
"content": "import os\nprint(os.getcwd())",
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-replace-lines",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/replace-lines",
|
|
title="Replace a line range",
|
|
summary="Replace lines start..end (inclusive) with new content; empty content deletes the range. Leaves the rest of the file untouched.",
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path",
|
|
"form",
|
|
"string",
|
|
True,
|
|
"src/main.py",
|
|
"Relative file path.",
|
|
),
|
|
field(
|
|
"start",
|
|
"form",
|
|
"integer",
|
|
True,
|
|
"10",
|
|
"First line to replace (1-indexed).",
|
|
),
|
|
field(
|
|
"end",
|
|
"form",
|
|
"integer",
|
|
True,
|
|
"12",
|
|
"Last line to replace (inclusive).",
|
|
),
|
|
field(
|
|
"content",
|
|
"form",
|
|
"textarea",
|
|
False,
|
|
"new code",
|
|
"Replacement text (empty deletes the range).",
|
|
),
|
|
],
|
|
notes=[
|
|
"Owner only. The preferred way to edit a large file; avoids rewriting the whole file."
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/projects/PROJECT_SLUG/files",
|
|
"data": {"path": "src/main.py", "type": "file"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-insert-lines",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/insert-lines",
|
|
title="Insert lines",
|
|
summary="Insert content before a 1-indexed line. Use at=1 to prepend and at=total_lines+1 to append.",
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path",
|
|
"form",
|
|
"string",
|
|
True,
|
|
"src/main.py",
|
|
"Relative file path.",
|
|
),
|
|
field(
|
|
"at",
|
|
"form",
|
|
"integer",
|
|
True,
|
|
"1",
|
|
"Insert before this 1-indexed line.",
|
|
),
|
|
field(
|
|
"content",
|
|
"form",
|
|
"textarea",
|
|
True,
|
|
"# header",
|
|
"Text to insert.",
|
|
),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/projects/PROJECT_SLUG/files",
|
|
"data": {"path": "src/main.py", "type": "file"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-delete-lines",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/delete-lines",
|
|
title="Delete a line range",
|
|
summary="Delete lines start..end (inclusive) from a text file.",
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path",
|
|
"form",
|
|
"string",
|
|
True,
|
|
"src/main.py",
|
|
"Relative file path.",
|
|
),
|
|
field(
|
|
"start",
|
|
"form",
|
|
"integer",
|
|
True,
|
|
"5",
|
|
"First line to delete (1-indexed).",
|
|
),
|
|
field(
|
|
"end",
|
|
"form",
|
|
"integer",
|
|
True,
|
|
"7",
|
|
"Last line to delete (inclusive).",
|
|
),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/projects/PROJECT_SLUG/files",
|
|
"data": {"path": "src/main.py", "type": "file"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-append",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/append",
|
|
title="Append to a file",
|
|
summary="Append content as new lines at the end of a text file; grow a large file across calls without resending it.",
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path", "form", "string", True, "log.txt", "Relative file path."
|
|
),
|
|
field(
|
|
"content",
|
|
"form",
|
|
"textarea",
|
|
True,
|
|
"next chunk",
|
|
"Text to append.",
|
|
),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/projects/PROJECT_SLUG/files",
|
|
"data": {"path": "log.txt", "type": "file"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-upload",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/upload",
|
|
title="Upload a file into a project",
|
|
summary="Upload a file into a directory (parents created); text decodes to an editable file, otherwise stored as binary.",
|
|
auth="user",
|
|
encoding="multipart",
|
|
destructive=True,
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field("file", "form", "file", True, "", "The file to upload."),
|
|
field(
|
|
"path",
|
|
"form",
|
|
"string",
|
|
False,
|
|
"assets",
|
|
"Target directory, empty for the root.",
|
|
),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/projects/PROJECT_SLUG/files",
|
|
"data": {
|
|
"path": "assets/logo.png",
|
|
"type": "file",
|
|
"is_binary": True,
|
|
},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-mkdir",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/mkdir",
|
|
title="Create a directory",
|
|
summary="Create a directory and any missing parents.",
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path",
|
|
"form",
|
|
"string",
|
|
True,
|
|
"src/components",
|
|
"Relative directory path.",
|
|
),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/projects/PROJECT_SLUG/files",
|
|
"data": {"path": "src/components", "type": "dir"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-move",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/move",
|
|
title="Move or rename",
|
|
summary="Move or rename a file or directory (and its descendants).",
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"from_path",
|
|
"form",
|
|
"string",
|
|
True,
|
|
"src/old.py",
|
|
"Existing path.",
|
|
),
|
|
field("to_path", "form", "string", True, "src/new.py", "New path."),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/projects/PROJECT_SLUG/files",
|
|
"data": {"path": "src/new.py"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-delete",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/delete",
|
|
title="Delete a file or directory",
|
|
summary="Delete a file, or a directory and everything under it. Project owner or an administrator; soft-deleted (restorable from admin trash). Blocked while the project is read-only.",
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path",
|
|
"form",
|
|
"string",
|
|
True,
|
|
"src/old.py",
|
|
"Relative path to delete.",
|
|
),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/projects/PROJECT_SLUG/files",
|
|
"data": {"path": "src/old.py"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="project-files-zip",
|
|
method="POST",
|
|
path="/projects/{project_slug}/files/zip",
|
|
title="Queue a zip of files",
|
|
summary="Archive the whole tree, or a subtree via the path query. Returns the job uid and status URL to poll with /zips/{uid}.",
|
|
auth="public",
|
|
params=[
|
|
field(
|
|
"project_slug",
|
|
"path",
|
|
"string",
|
|
True,
|
|
"PROJECT_SLUG",
|
|
"Project slug or uid.",
|
|
),
|
|
field(
|
|
"path",
|
|
"query",
|
|
"string",
|
|
False,
|
|
"src",
|
|
"Relative file or directory to archive; empty for the whole project.",
|
|
),
|
|
],
|
|
sample_response={
|
|
"uid": "ZIP_JOB_UID",
|
|
"status_url": "/zips/ZIP_JOB_UID",
|
|
},
|
|
),
|
|
],
|
|
}
|