{"result":{"depiction_url":null,"edges":{"in_module":{"objects":[{"created":"2026-11-05T18:43:03Z","object_id":{"id":335,"is_a":["text","documentation","reference","module"],"name":"doc_core","title":"Zotonic Core","uri":"https:\/\/zotonic.com\/id\/335"},"seq":1}],"predicate":{"id":333,"is_a":["meta","predicate"],"name":"in_module","title":{"_type":"trans","tr":{"en":"In module"}},"uri":"https:\/\/zotonic.com\/id\/in_module"}},"subject":{"objects":[{"created":"2026-11-05T18:43:03Z","object_id":{"id":2555,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_reference","title":"Reference","uri":"https:\/\/zotonic.com\/id\/2555"},"seq":1},{"created":"2026-11-05T18:43:03Z","object_id":{"id":2564,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_backend_developer","title":"Backend developer","uri":"https:\/\/zotonic.com\/id\/2564"},"seq":2},{"created":"2026-11-05T18:43:03Z","object_id":{"id":2576,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_media_management","title":"Media management","uri":"https:\/\/zotonic.com\/id\/2576"},"seq":3},{"created":"2026-11-05T18:43:03Z","object_id":{"id":2577,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_image_management","title":"Image management","uri":"https:\/\/zotonic.com\/id\/2577"},"seq":4},{"created":"2026-11-05T18:43:03Z","object_id":{"id":2581,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_file_storage","title":"File storage","uri":"https:\/\/zotonic.com\/id\/2581"},"seq":5},{"created":"2026-11-05T18:43:03Z","object_id":{"id":2625,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_model","title":"Model","uri":"https:\/\/zotonic.com\/id\/2625"},"seq":6}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":3141,"is_a":["text","documentation","reference","model"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/3141"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_model_model_cafs","page_url":{"en":"https:\/\/zotonic.com\/docs\/3141\/cafs","x-default":"https:\/\/zotonic.com\/docs\/3141\/cafs"},"preview_url":null,"resource":{"version":15,"pivot_location_lat":null,"title":"cafs","is_authoritative":true,"body":"<p>Site-local content-addressable file storage (CAFS) for originals, explicit previews\nsuch as video posters, and generated image variants. Identical bytes share one\nSHA-256 object, while resources keep their own logical filenames and permissions.<\/p>\n<h2>Interface and permissions<\/h2>\n<p>This is an internal Erlang API used by <code>m_media<\/code>, file resolution, filestore, and\nbackground processing. It has no template, HTTP model, or MQTT model paths.\nFunctions do not enforce resource ACLs: callers must authorize the resource and\noperation before resolving or changing storage. Knowing a hash never grants access.\nUse <code>m_media<\/code> for ordinary uploads, replacement, duplication, and deletion.<\/p>\n<p>For example, after authorizing access to a medium&#39;s logical filename:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">case m_cafs:lookup(Filename, Context) of\n    {ok, Hash} -&gt;\n        m_cafs:with_local_file(Hash, fun file:read_file\/1, Context);\n    {error, enoent} -&gt;\n        {error, legacy_or_missing}\nend.\n<\/code><\/pre>\n<p><code>lookup\/2<\/code> returns <code>{ok, Hash}<\/code> or <code>{error, enoent}<\/code>. An absent mapping does not\nprove the file is missing: it may still use legacy storage. <code>with_local_file\/3<\/code>\nresolves local or remote storage, pins the object, and passes a temporary readable\nfilename to the callback. It returns the callback result or a storage error.\nThe callback runs in a monitored worker; its filename is valid only during the\ncallback and must not be modified or retained for later use.<\/p>\n<h2>Storage and attachment<\/h2>\n<p>Objects live at <code>archive\/cafs\/sha256\/ab\/cd\/&lt;hash&gt;<\/code>, relative to the site&#39;s files\ndirectory. Hashes are 64 lowercase hexadecimal bytes; the directory components\nare their first four characters. The path has no filename extension. <code>path\/1<\/code>\nconstructs this relative path; <code>path_hash\/1<\/code> validates and extracts its hash.\nFilestore uses the same physical key and can move the bytes to remote storage.<\/p>\n<p><code>filename_cafs<\/code> maps logical filenames to objects in <code>cafs<\/code>. These mappings are\nindependent of physical storage: cloning a mapped filename requires no file read\nor remote download. Existing files are promoted only when their bytes are already\navailable for an operation; installation never downloads or hashes all old files.<\/p>\n<p>Database-free sites (<code>dbdatabase = none<\/code>) do not use CAFS. <code>is_enabled\/1<\/code> checks\nconfiguration, not pool availability, so a database outage never enables a legacy\nstorage fallback. On these sites, <code>lookup\/2<\/code> returns <code>{error, enoent}<\/code>,\n<code>stage_file\/2<\/code> returns <code>{error, nodb}<\/code>, and installation\/maintenance are no-ops.\nLibrary previews use the local preview directory without provenance tracking or GC.<\/p>\n<p><code>stage_file\/2<\/code> hashes and atomically publishes a local file, returning\n<code>{ok, #{hash =&gt; Hash, token =&gt; Token}}<\/code>. Do this outside the resource transaction.\nThe token holds a one-day staging pin. <code>attach\/3<\/code> attaches a unique logical filename\nand consumes that pin; use the resource transaction&#39;s context to commit the medium\nand mapping together. <code>stage_medium\/2<\/code> and <code>attach_medium\/2<\/code> perform this handoff\nfor owned originals and posters. <code>stage_medium\/2<\/code> returns <code>{ok, Staged}<\/code> or a\ncontrolled error and releases partial staging pins on failure. <code>release\/2<\/code> releases remaining staging pins from\na list of <code>{Filename, Stage}<\/code> pairs, including after failure.<\/p>\n<p><code>stage_medium\/3<\/code> accepts the logical filename of a fresh private archive copy\ncreated by the upload routine. Only that original uses a single hash pass over\nits staging copy; the medium digest reuses this hash. The default staging APIs,\nlegacy migration, extracted previews and observer-supplied replacement filenames\nhash both source and copy to detect changes while copying. Publication always\nuses the hash of the staged bytes, with an atomic rename on the CAFS filesystem.<\/p>\n<p>Attachment can return <code>{error, expired}<\/code> for an expired staging token or\n<code>{error, deleting}<\/code> while dispatched physical deletion is pending. Staging an\nidentical file can cancel an undispatched deletion using the filestore row fence;\nit never revives an object that an in-flight remote delete could remove. Pending\ncollection and failed deletion are rescheduled so they do not starve other objects.\nA filename already mapped\nto different bytes is a conflict. <code>clone_filename\/3<\/code> shares an existing mapping;\n<code>promote\/3<\/code> maps an available legacy file and returns <code>{ok, Hash}<\/code>. It adopts an\nexisting remote registry entry before allowing a canonical upload, and queues\nretryable cleanup of redundant legacy paths. <code>stage_original\/2<\/code> and\n<code>attach_original\/3<\/code> provide that same handoff for maintenance migration: newly\nstaged originals are not uploadable until remote adoption has been checked.\nBusy or unavailable backends defer adoption and cleanup; they do not trigger a\nsecond upload. Remote retirement follows the backend retention policy. Cleanup\nis queued in the publication transaction and defers local unlinking while legacy\nreaders remain. With local retention disabled, the backend queues adopted objects\nfor local offloading without another upload; active pins and readers defer it.<\/p>\n<p>Requests for owned non-CAFS originals schedule background promotion through\n<code>z_media_cafs_migrate<\/code>. Local bytes or the completed, locked download cache are\nused without starting another download or delaying the response for hashing.\nAttachment rechecks the medium under its row lock and records the original digest;\nreplaced\/deleted media and unowned files are not attached. Failed or overloaded\nattempts leave the legacy file usable, and successful attempts queue durable cleanup.<\/p>\n<h2>Shared previews and retention<\/h2>\n<p>On a miss for a revision-bearing URL, a current source, mediaclass and edit\nrevision may reuse the legacy URL for the exact same rendering operations. Only\n<code>editrev<\/code> is removed; dimensions, operation order, class checksum and output\nextension stay unchanged, and the legacy signature is reconstructed. Existing\nlocal or remote preview bytes are promoted without running the resizer when the\nsource hash is available. Unknown remote originals are not downloaded for this.\nBackground cleanup adopts the remote preview and removes redundant legacy copies;\nnew local outputs are uploaded only after remote absence is confirmed. Registered\ncurrent URLs resolve directly to their CAFS output without legacy path probes or\nusage writes, while checking the current source, edits, class and renderer version.<\/p>\n<p><code>derivative\/5<\/code> keys generation by source hash, ordered effective operations, and\nrenderer version. Its zero-argument generator returns a staged output from\n<code>stage_file\/2<\/code>. The result is <code>{ok, OutputHash}<\/code> or an error; concurrent callers\nshare a generation claim. Errors include <code>timeout<\/code>, <code>stale_claim<\/code>, and generator\nerrors. Include every output-affecting input in the key; do not sort operations.<\/p>\n<p><code>derivative\/6<\/code> additionally binds a validated preview usage key from\n<code>z_cafs_preview<\/code>. An expired usage returns <code>{error, enoent}<\/code>. Usage provenance is\nseparate from rendering identity, so resources with different histories can still\nshare identical output. Generated previews are retired lazily after source removal,\nmediaclass changes, or image-edit changes. Requests do not renew an obsolete URL&#39;s\ndeadline. Reverting settings can make a usage current again. Unversioned legacy\nURLs retain their first observed edit revision; the upgrade timestamp alone never\nexpires them. Expired usages also delete their exact legacy preview paths, with\nretry on failure. Rendering identity includes the actual metadata snapshot used\nby the converter, excluding resource IDs.<\/p>\n<p>Medium deletion retains filename mappings for the existing five-week recovery\nperiod. <code>retire_filename\/2<\/code> is a cleanup operation called after that period, not a\nreplacement for deleting a resource. <code>maintenance\/1<\/code> performs bounded reconciliation,\npreview retirement, upload queuing, and collection. Other filenames, derivatives,\nand pins preserve shared bytes. Remote deletion honors filestore retention and\nkeeps a tombstone until deletion is confirmed.<\/p>\n<p><code>pin\/3<\/code> creates a durable processing reference owned by a caller-supplied token;\nthe caller must balance it with <code>unpin\/3<\/code>, including cancellation and failure.\n<code>with_path_pin\/3<\/code> scopes a renewable pin to a callback for CAFS physical paths.\n<code>track\/2<\/code> and <code>readers\/2<\/code> manage file-entry cache subscriptions, not retention pins.<\/p>\n<h2>Site configuration<\/h2>\n<p>The core installer creates and upgrades the schema; no CAFS module needs activation.\nRemote storage requires the usual <code>mod_filestore<\/code> configuration.<\/p>\n<table class=\"table\"><thead><tr><th><code>m_config<\/code> key<\/th><th>Default<\/th><th>Meaning<\/th><\/tr><\/thead><tbody><tr><td><code>site.cafs_preview_retention_days<\/code><\/td><td><code>90<\/code><\/td><td>Obsolete-preview grace period, in days. Integers from 1 to 3650 are accepted; other values use the default.<\/td><\/tr><tr><td><code>site.cafs_renderer_version<\/code><\/td><td><code>1<\/code><\/td><td>Additional generation-version value used by image preview resolution. Change it after rendering changes not represented by the operations or renderer identity.<\/td><\/tr><\/tbody><\/table>\n<p>After changing the renderer version, call <code>z_file_sup:refresh\/0<\/code> to invalidate\nrunning file entries. These are per-site <code>m_config<\/code> settings, not system settings.\nThey are listed in the admin configuration catalog by <code>mod_base<\/code>, under the <code>site<\/code>\nnamespace. For example, to set a 180-day preview grace period:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">m_config:set_value(site, cafs_preview_retention_days, 180, Context).\n<\/code><\/pre>\n<p><code>queue_all\/1<\/code> marks every ready CAFS object for upload, including generated variants\nand durable task inputs. <code>queue_uploads\/1<\/code> submits a bounded batch to filestore.\nNeither function discovers or downloads legacy files.<\/p>","slug":"cafs","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"46ed6ba6e9855b4bdbf715514751e76d2cb623de0988d0f31cff2ef550c25978","is_featured":false,"content_group_id":{"id":2551,"is_a":["meta","content_group"],"name":"content_group_imported_docs","title":"Imported documentation","uri":"https:\/\/zotonic.com\/id\/content_group_imported_docs"},"category_id":{"id":322,"is_a":["meta","category"],"name":"model","title":"Models","uri":"https:\/\/test.zotonic.com\/id\/322"},"doc_source_path":"apps\/zotonic_core\/src\/models\/m_cafs.erl","publication_start":"2026-11-05T18:43:02Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_core\/src\/models\/m_cafs.erl","pivot_location_lng":null,"doc_source_kind":"model","name":"doc_model_model_cafs","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2026-11-05T18:43:02Z","uri":null,"doc_status":"current","is_dependent":false,"erlang_app":"zotonic_core","publication_end":"9999-06-01T00:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_source_commit":"becf161621dcc4de964bbf083a88aff6d3ab21ae\n","erlang_module":"m_cafs","creator_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"modified":"2026-11-05T18:43:03Z","title_slug":"cafs"},"uri":"https:\/\/zotonic.com\/id\/3141","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/3141"}},"status":"ok"}