dev.atfs.repo.listFiles

atfs.dev

{
  "id": "dev.atfs.repo.listFiles",
  "defs": {
    "file": {
      "type": "object",
      "required": [
        "cid",
        "ipfsRoot",
        "size",
        "mimeType"
      ],
      "properties": {
        "cid": {
          "type": "cid-link",
          "description": "The blessed CID of the file's bytes (CIDv1, raw multicodec, sha-256 multihash) — the canonical identity. This is what dev.atfs.repo.uploadFile (and its com.atproto.repo.uploadBlob alias) returns, what equality/dedup use, what atfs's own HTTP surfaces serve at any size, and the only DASL-compatible form. Never a DAG root."
        },
        "size": {
          "type": "integer",
          "description": "Size of the file, in bytes."
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 128,
            "minLength": 1,
            "description": "A single free-form tag: 1-128 bytes, no control characters. maxGraphemes is the same 128 rather than a smaller, byte-aware figure, because the bound atfs actually enforces is bytes, not graphemes — a tighter maxGraphemes here would advertise a stricter limit than the server applies.",
            "maxGraphemes": 128
          },
          "maxLength": 16,
          "minLength": 1,
          "description": "Free-form labels attached to this content, deduplicated and sorted. Advisory and instance-local, in the same register as `providers`: never part of the file's identity (two identical uploads to two different instances, or under two different accounts, can carry entirely different tags for the same cid), never required for correctness, and not something a `dev.atfs.file` reference's recipient should trust as a global fact about the content — only as a note from whoever produced this reference. In dev.atfs.repo.listFiles output this is a union: every tag borne by ANY of that file's claims here — account-class and mirrored-server claims alike — with no indication of which claimant applied which; listFiles never discloses who pinned or tagged anything (pass listFiles' `did` parameter to scope this to one claimant's own tags instead). dev.atfs.repo.pinFile reads this field on input and adopts it verbatim under the calling instance's own claim (see dev.atfs.repo.pinFile)."
        },
        "ipfsRoot": {
          "type": "cid-link",
          "description": "The CID to fetch this file over the IPFS network: the UnixFS root for chunked (large) files, or exactly equal to `cid` when the file fits a single block. Always present, even when equal to `cid`, so consumers never need conditional logic — fetch `ipfsRoot`, then verify the bytes against `cid`. Chunker-dependent: this is *a* valid way to fetch the bytes, not a second identity — re-adding the same bytes elsewhere with different chunk settings would yield a different, equally valid root."
        },
        "mimeType": {
          "type": "string",
          "description": "The IANA media type of the file's bytes, exactly as recorded at upload time (see dev.atfs.repo.uploadFile). Defaults to application/octet-stream when the uploader supplied no Content-Type."
        },
        "firstSeen": {
          "type": "string",
          "format": "datetime",
          "description": "When this instance first held these bytes. It is this instance's own observation, never the content's age and never adopted from anywhere: a reference handed to dev.atfs.repo.pinFile carries no such field, and an instance mirroring a followed origin records when IT first held the bytes, not when the origin did. Nothing can set or change it. It survives claims arriving and being released — re-uploading identical bytes, or another account pinning them, leaves it exactly where it was, since content addressing means those are the same bytes either way. The one thing that resets it is the content actually leaving: once the last claim is released and the bytes are deleted, a later upload or pin of the same cid starts it again. Absent when this instance has no trustworthy reading to report — an appliance accepts uploads before its clock is set, and reporting nothing is deliberate, since a 1970 timestamp would be indistinguishable from a real one. Those files are stamped once the clock is set, so an absent firstSeen is not necessarily permanent."
        }
      },
      "description": "One listed file. Structurally dev.atfs.file plus firstSeen, so an entry can be handed straight back to dev.atfs.repo.pinFile — which ignores the extra field — but deliberately a def of its own rather than a ref to dev.atfs.file, because firstSeen is this instance's observation about its own copy and has no meaning inside a portable reference. providers is absent for the reason it always was: listFiles has never emitted it, and the ref this def replaces advertised a field that never appeared."
    },
    "main": {
      "type": "query",
      "output": {
        "schema": {
          "type": "object",
          "required": [
            "files"
          ],
          "properties": {
            "files": {
              "type": "array",
              "items": {
                "ref": "#file",
                "type": "ref"
              },
              "description": "Each entry's tags field (see #file) is a flat union: every tag borne by ANY of that file's claims, account-class and mirrored-server alike, with no indication of which claimant applied which — listFiles never discloses who pinned or tagged anything. Pass the `did` parameter above to scope both this listing and each entry's tags to one claimant's own claims instead."
            },
            "cursor": {
              "type": "string",
              "description": "Present only when this page filled up to the requested limit, meaning more files may follow. Its absence marks the final page."
            }
          }
        },
        "encoding": "application/json"
      },
      "parameters": {
        "type": "params",
        "properties": {
          "did": {
            "type": "string",
            "format": "did",
            "minLength": 1,
            "description": "Restrict the listing to files where this DID holds at least one claim (account-class or mirrored-server), and scope both the reported `tags` and the `tag` filter below to just that DID's own claims, instead of the union across every claimant — this is how to ask the question the output otherwise never answers, since listFiles never says on its own which claimant applied a tag. `?did=X&tag=index` therefore means: files X claims, where X itself applied the tag index. Must look like a DID; anything else is `InvalidRequest`. Listing eligibility is unchanged — a file still needs an account-class claim from ANY claimant to appear at all, so naming a followed server's DID here returns only that server's files that ALSO carry a local account-class claim; content held only as a mirror stays unlisted regardless of `did`. Omit for the unscoped, all-claimants view. The cursor does NOT encode this parameter, so a paginated walk must resend the same `did` on every page, exactly as it must keep `limit` and `tag` stable."
          },
          "tag": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 128,
              "minLength": 1,
              "maxGraphemes": 128
            },
            "maxLength": 16,
            "description": "Restrict the listing to files carrying every named tag — `?tag=index&tag=v0.1.0` lists only files tagged with both, ANDed rather than ORed. Plain strings. Without `did` above, a file matches when each named tag appears on at least one of its claims — any claimant, any class — so filtering deliberately doesn't ask who applied a tag, only whether it was applied; this is deliberate, not an oversight. With `did`, matching narrows to that one DID's own claims, so the two parameters together ask a single-claimant question. Omit `tag` for no filtering. The cursor does NOT encode which tags (or which did) produced it — it's a plain lexical cid comparison applied before any filtering, the same as it's applied before the account-claim check — so changing either between calls silently changes what the rest of the walk returns rather than erroring, and a caller paginating a filtered listing must resend the identical tag set (and did) on every page, exactly as it must keep limit stable. This is the same discipline an unfiltered walk already needs; filtering doesn't relax it. Bounded at 16 like a claim's own tag list, since no file can carry more than that and a longer filter could only ever match nothing."
          },
          "limit": {
            "type": "integer",
            "default": 500,
            "maximum": 1000,
            "minimum": 1,
            "description": "Maximum number of files to return in this page."
          },
          "cursor": {
            "type": "string",
            "description": "Opaque resume token from a previous call's response. Omit to start from the beginning."
          }
        }
      },
      "description": "Enumerate every file this instance directly claims — the whole instance, not scoped to any one account, since atfs has no per-repo notion to scope by. This exists so another atfs instance can replicate pins: a follower walks every page, diffs the resulting cid set against its own mirror of this instance, dev.atfs.repo.pinFile whatever's new, and releases whatever's vanished. It's a poll rather than a subscription deliberately — the store keeps no event log, so there's nothing for a websocket-style firehose to replay, and a plain set-diff over current state already yields both pins and unpins with no history needed. Public and unauthenticated, unlike uploadFile/pinFile/deleteFile: every cid this instance pins is already announced to the IPFS DHT as a provider record and served at /ipfs/<cid>, so nothing here is secret, and an instance that has disabled uploads (no serviceDid configured) must still be enumerable. Only DIRECTLY claimed content is listed: a file must have been uploaded here or pinned here by one of this instance's accounts. Content this instance merely mirrors from an instance it follows is deliberately absent, so a mirror never re-exports what it mirrors — which is what makes an A-follows-B-follows-A pair converge instead of echoing, lets an origin's deletions propagate outward, and keeps mirroring non-transitive (follow each origin you actually want). The instance still serves mirrored content at /ipfs/<cid> and announces it to the DHT; it just doesn't advertise it here. A file mid-GC (its pin list has emptied but the bytes haven't been swept yet) or not yet indexed (a large blob whose UnixFS DAG hasn't finished building) is omitted from every page too — both are transient states that can appear or disappear between one poll and the next, so a follower should expect the set it sees to shift slightly poll to poll even with no new uploads, and should never treat a single absence as a deletion."
    }
  },
  "$type": "com.atproto.lexicon.schema",
  "lexicon": 1
}

Validate Record

Validate a record against dev.atfs.repo.listFiles

Validation Options
Treat any remaining unresolved references as valid

Metadata

DID
did:web:atfs.dev
CID
bafyreigutw7uta32l7uuone7fj255dbfpcuslagqkr334j2cpj37tt3fta
Indexed At
2026-08-23 16:19 UTC
AT-URI
at://did:web:atfs.dev/com.atproto.lexicon.schema/dev.atfs.repo.listFiles

Version History (3 versions)

Lexicon Garden

@