# retoor <retoor@molodetz.nl>
from .._shared import endpoint, field
GROUP = {
"slug": "uploads",
"title": "Uploads",
"intro": """
# Uploads
Attachment storage. Upload a file - or hand the server a public URL to fetch - to receive an
attachment record, then reference its `uid` in an `attachment_uids` field when creating a post,
comment, project, gist, message, or issue - see
[Posts, Comments, Projects, Gists & News](/docs/content.html). Images and videos embed and
play inline once posted; other types render as download links. The record's `is_image` and
`is_video` flags indicate how the file is displayed.
You manage your own attachments over the full lifecycle: **list** every file you uploaded, **get**
one by uid, **rename** its display filename, and **delete** it. The list is the same set of
attachments that appear on your posts and other content - listing, renaming, or deleting one is
reflected everywhere it is used.
Every endpoint follows the shared [Conventions & Errors](/docs/conventions.html) (auth, content
negotiation, pagination, status codes); see [Authentication](/docs/authentication.html) for the
four ways to sign requests.
""",
"endpoints": [
endpoint(
id="uploads-upload",
method="POST",
path="/uploads/upload",
title="Upload a file",
summary="Store a file and return its attachment record.",
auth="user",
encoding="multipart",
params=[field("file", "form", "file", True, "", "The file to upload.")],
notes=[
"Allowed file types and the size limit are configured by administrators. Images and common video formats (mp4, webm, ogv, mov, m4v) are accepted by default.",
"Returns `201` on success, `413` if too large, `415` if the type is not allowed.",
],
sample_response={
"uid": "ATTACHMENT_UID",
"filename": "clip.mp4",
"url": "/static/uploads/attachments/ab/cd/ATTACHMENT_UID.mp4",
"size": 20480,
"is_image": False,
"is_video": True,
"mime_type": "video/mp4",
},
),
endpoint(
id="uploads-upload-url",
method="POST",
path="/uploads/upload-url",
title="Attach a file from a URL",
summary="Download a public URL on the server and store it as an attachment.",
auth="user",
params=[
field(
"url",
"form",
"string",
True,
"https://example.com/photo.png",
"Public http(s) URL of the file to download and attach.",
),
field(
"filename",
"form",
"string",
False,
"photo.png",
"Optional filename with an allowed extension, used when the URL has no clear name.",
),
],
notes=[
"The server fetches the URL (SSRF-guarded, size-capped) and stores the bytes through the same pipeline as a direct upload; the response is identical to Upload a file.",
"The file type is taken from the URL path or the response Content-Type. Returns `201` on success, `413` if too large, `415` if the type cannot be resolved to an allowed type, `400` for an unreachable or private address.",
],
sample_response={
"uid": "ATTACHMENT_UID",
"filename": "photo.png",
"url": "/static/uploads/attachments/ab/cd/ATTACHMENT_UID.png",
"size": 20480,
"is_image": True,
"is_video": False,
"mime_type": "image/png",
},
),
endpoint(
id="uploads-list",
method="GET",
path="/uploads",
title="List your attachments",
summary="List every attachment you uploaded, newest first, paginated (24 per page).",
auth="user",
params=[
field(
"page",
"query",
"integer",
False,
"1",
"1-based page number.",
),
field(
"linked",
"query",
"string",
False,
"",
"Filter: `true` returns only attachments already used on a post/comment/project/gist/issue, `false` returns only orphaned uploads. Omit for all.",
),
],
notes=[
"Each item carries `uid`, `original_filename`, `mime_type`, `url`, `file_size`, its `target_type`/`target_uid`/`target_url` when linked, and a `linked` flag.",
],
sample_response={
"attachments": [
{
"uid": "ATTACHMENT_UID",
"original_filename": "photo.png",
"file_size": 20480,
"mime_type": "image/png",
"url": "/static/uploads/attachments/ab/cd/ATTACHMENT_UID.png",
"is_image": True,
"is_video": False,
"is_audio": False,
"linked": True,
"target_type": "post",
"target_uid": "POST_UID",
"target_url": "/posts/POST_SLUG",
"created_at": "2026-01-01T12:00:00+00:00",
}
],
"pagination": {"page": 1, "per_page": 24, "total": 1, "total_pages": 1},
"total": 1,
},
),
endpoint(
id="uploads-get",
method="GET",
path="/uploads/{attachment_uid}",
title="Get one attachment",
summary="Fetch the metadata of a single attachment you own; administrators may fetch any user's attachment.",
auth="user",
params=[
field(
"attachment_uid",
"path",
"string",
True,
"ATTACHMENT_UID",
"UID of the attachment.",
)
],
notes=["Returns `404` if the attachment does not exist, `403` if it is not yours."],
sample_response={
"uid": "ATTACHMENT_UID",
"original_filename": "photo.png",
"file_size": 20480,
"mime_type": "image/png",
"url": "/static/uploads/attachments/ab/cd/ATTACHMENT_UID.png",
"is_image": True,
"is_video": False,
"is_audio": False,
"linked": True,
"target_type": "post",
"target_uid": "POST_UID",
"target_url": "/posts/POST_SLUG",
"created_at": "2026-01-01T12:00:00+00:00",
},
),
endpoint(
id="uploads-rename",
method="PATCH",
path="/uploads/{attachment_uid}",
title="Rename an attachment",
summary="Change the display filename of an attachment you own; administrators may rename any user's attachment.",
auth="user",
params=[
field(
"attachment_uid",
"path",
"string",
True,
"ATTACHMENT_UID",
"UID of the attachment.",
),
field(
"filename",
"form",
"string",
True,
"renamed.png",
"New display filename.",
),
],
notes=[
"Only the display filename changes; the stored file and its extension are untouched. The original extension is always preserved, so the file type cannot be altered.",
"Returns the updated attachment record. `404` if it does not exist, `403` if it is not yours, `400` for an empty filename.",
],
sample_response={
"uid": "ATTACHMENT_UID",
"original_filename": "renamed.png",
"file_size": 20480,
"mime_type": "image/png",
"url": "/static/uploads/attachments/ab/cd/ATTACHMENT_UID.png",
"is_image": True,
"linked": True,
"target_type": "post",
"target_uid": "POST_UID",
"target_url": "/posts/POST_SLUG",
"created_at": "2026-01-01T12:00:00+00:00",
},
),
endpoint(
id="uploads-delete",
method="DELETE",
path="/uploads/delete/{attachment_uid}",
title="Delete an attachment",
summary="Remove an attachment you previously uploaded; administrators may remove any user's attachment.",
auth="user",
destructive=True,
params=[
field(
"attachment_uid",
"path",
"string",
True,
"ATTACHMENT_UID",
"UID of the attachment (the `uid` returned by Upload a file, Attach a file from a URL, or List your attachments).",
)
],
notes=[
"Only the owner may delete their own attachment; an administrator may delete any user's. Deleting one you do not own returns `403`.",
"The attachment is removed everywhere at once: it leaves your attachment list (List your attachments) and disappears from every post, comment, project, gist, message, or issue it was attached to, and its file stops being served under `/static/uploads/`.",
"Idempotent from the caller's view: an already-removed or unknown uid returns `404`. A successful delete returns `200` with `{\"status\": \"deleted\"}`.",
"To detach a file from a single post/comment without removing the upload itself, edit that object's attachment list instead - deleting here removes the attachment from every place it is used.",
],
sample_response={"status": "deleted"},
),
],
}