TinyStore
Blobs / Copies and expiry
On this page

Blobs / Copies and expiry

Copies and expiry

A copy costs a row, whatever the file's size: stored bytes never change, so two files can share them. A rename is a copy and a delete in one write. A file can expire a span after it was written, and its bytes go once nothing names them.

await docs.copy('notes.md', 'notes (copy).md') // FileInfo; no bytes are copied
await docs.rename('draft.md', 'final.md') // ConflictError if final.md is there
await docs.copy("notes.md", "notes (copy).md")
await docs.rename("draft.md", "final.md")
_, err = docs.Copy(ctx, "notes.md", "notes (copy).md")
_, err = docs.Rename(ctx, "draft.md", "final.md")
docs.copy("notes.md", "notes (copy).md")?;
docs.rename("draft.md", "final.md")?;

Copies never replace by surprise

put and an upload are writes of a path and replace what it held. copy and rename move a file that exists onto a path that may hold one, so a path that holds a file is conflict, unless ifMatch names the version to replace:

const final = await docs.head('final.md')
await docs.rename('draft.md', 'final.md', { ifMatch: final!.etag }) // replaces exactly that version
final = await docs.head("final.md")
await docs.rename("draft.md", "final.md", if_match=final.etag)
final, found, err := docs.Head(ctx, "final.md")
_, err = docs.Rename(ctx, "draft.md", "final.md", blobs.IfMatch(final.ETag))
let final_ = docs.head("final.md")?.expect("final.md");
docs.key("final.md").if_match(&final_.etag).rename_from("draft.md")?;
CallSource missingTarget holds a file
put, upload—replaced
copy, renamenot foundconflict, unless ifMatch
create—nothing written, false

A copy and its source are two files from then on: replacing or deleting one leaves the other, and a copy outlives its source (contract BLOB-05). Both stay within one set of files; between two sets, read and put.

Expiry

const reports = store.files('reports', { ttl: '24h' })

await reports.put('report.csv', csv) // expires 24 hours from now
await reports.put('report.csv', csv, { ttl: '7d' }) // its own term
await reports.expire('report.csv', '1h') // from now; a Date for a time
reports = store.files("reports", ttl="24h")

await reports.put("report.csv", csv)
await reports.put("report.csv", csv, ttl="7d")
await reports.expire("report.csv", "1h")  # a datetime for a time
reports, err := blobs.OpenFiles(store, "reports", blobs.TTL(24*time.Hour))

_, err = reports.Put(ctx, "report.csv", csv)
_, err = reports.Put(ctx, "report.csv", csv, blobs.TTL(7*24*time.Hour))
err = reports.Expire(ctx, "report.csv", time.Hour) // ExpireAt for a time
let reports = store.files("reports").ttl(Duration::from_hours(24)).open()?;

reports.put("report.csv", &csv)?;
reports.key("report.csv").ttl(Duration::from_hours(24 * 7)).put(&csv)?;
reports.expire("report.csv", Duration::from_hours(1))?;
  • The set's ttl is the term of every file written without its own, not a ceiling: a write that gives its own keeps it.
  • A term counts from the write, and every write gets its own: an export written again lives its day again.
  • An expired file is gone for readers at once: get and head give nothing. Its bytes are removed in the background, within a minute.
  • expires on a file says when it goes.

A backup does not belong in files with a ttl.

Deleting

await docs.delete('notes.md') // nothing to delete is not an error
await docs.folder('users', 42).clear() // a folder, at once, however many files
await docs.delete("notes.md")
await docs.folder("users", 42).clear()
err = docs.Delete(ctx, "notes.md")
err = docs.Folder("users", 42).Clear(ctx)
docs.delete("notes.md")?;
docs.folder("users").folder(42).clear()?;

A delete, a clear, an expiry or a replace drops the file's name in one commit. The bytes stay as long as a copy or a reader that opened them still holds them, then go in the background.

See also

  • Blobs: the engine's overview.
  • Integrity: ifMatch and create for writes that must not cross.

Edit this page on GitHub Updated 19 October 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results