Why files get left behind
You store a file's ID in your database. When the record is deleted, the file is still there, still counted against your plan and still reachable by anyone who has the link. Cleaning up needs two things: a way to find the files that belonged to the record, and a way to delete them. The management API does both from your server, with your private key.
These calls need your private key, so make them from your server, never from a browser. They only ever see your own app's files.
Set up the client with a private key
import { Fileboy } from "@cd2/fileboy-node"
const fileboy = Fileboy({
publicKey: process.env.FILEBOY_PUBLIC_KEY!,
privateKey: process.env.FILEBOY_PRIVATE_KEY!,
})require "fileboy"
fileboy = Fileboy::Client.new(
public_key: ENV.fetch("FILEBOY_PUBLIC_KEY"),
private_key: ENV["FILEBOY_PRIVATE_KEY"]
)import os
import fileboy
client = fileboy.Client(
public_key=os.environ["FILEBOY_PUBLIC_KEY"],
private_key=os.environ.get("FILEBOY_PRIVATE_KEY"),
)use Fileboy\Client;
$fileboy = new Client(
publicKey: getenv('FILEBOY_PUBLIC_KEY'),
privateKey: getenv('FILEBOY_PRIVATE_KEY') ?: null,
);Without a private key the client raises an error before it sends anything. A public key, or a token limited to uploads, is refused by the API with private_key_required.
Tag files when you upload them
The easiest way to find a record's files later is to say whose they are when you upload. Put your own IDs in the file's metadata.
const file = await fileboy.uploadFileFromPath("./exports/report.pdf", {
metadata: { customerId: "cus_123", kind: "monthly-report" },
})
console.log(file.id) // save thisfile = fileboy.upload_file(
"./exports/report.pdf",
metadata: { customerId: "cus_123", kind: "monthly-report" }
)
puts file.id # save thisfile = client.upload_file(
"./exports/report.pdf",
metadata={"customerId": "cus_123", "kind": "monthly-report"},
)
print(file.id) # save this$file = $fileboy->uploadFile(
'./exports/report.pdf',
metadata: ['customerId' => 'cus_123', 'kind' => 'monthly-report'],
);
echo $file->id; // save thiscurl https://api.fileboy.io/files/upload \
-H "Authorization: public YOUR_PUBLIC_KEY" \
-F "file=@./exports/report.pdf" \
-F 'data={"name":"report.pdf","metadata":{"kind":"monthly-report"}}'Find files
Filter by metadata, type prefix, label, name, hash and creation date. Results come one page at a time with a cursor, newest first unless you sort differently. The cursor stays correct if you delete files while you page through.
// one page, newest first. Needs the private key.
const page = await fileboy.listFiles({
mime: "image/",
metadata: { orderId: "42" },
limit: 25,
})
page.data.forEach(file => console.log(file.id, file.name, file.labels))
// the next page
if (page.nextCursor) {
await fileboy.listFiles({ mime: "image/", metadata: { orderId: "42" }, cursor: page.nextCursor })
}# one page, newest first. Needs the private key.
page = fileboy.list_files(mime: "image/", metadata: { orderId: "42" }, limit: 25)
page.items.each { |file| puts "#{file.id} #{file.name} #{file.labels}" }
# or walk every page
fileboy.each_file(mime: "image/") { |file| puts file.name }# one page, newest first. Needs the private key.
page = client.list_files(mime="image/", metadata={"orderId": "42"}, limit=25)
for file in page.items:
print(file.id, file.name, file.labels)
# or walk every page
for file in client.iter_files(mime="image/"):
print(file.name)// one page, newest first. Needs the private key.
$page = $fileboy->listFiles(['mime' => 'image/', 'metadata' => ['orderId' => '42'], 'limit' => 25]);
foreach ($page->data as $file) {
echo $file->id, ' ', $file->name, "
";
}
// or walk every page
foreach ($fileboy->eachFile(['mime' => 'image/']) as $file) {
echo $file->name, "
";
}curl -G https://api.fileboy.io/files \
-H "Authorization: token YOUR_SIGNED_TOKEN" \
--data-urlencode "mime=image/" \
--data-urlencode "metadata[orderId]=42" \
--data-urlencode "limit=25"Delete the files of a record
Delete one file by ID, or up to 100 at once. Deleting removes the stored data, including any resized copies Fileboy cached for an image, and sends a file.deleted webhook. Run this when the record is destroyed, for example in a model callback or a background job.
import { notFoundError } from "@cd2/fileboy-node"
try {
await fileboy.destroyFile(file.id)
} catch (e) {
if (!notFoundError.is(e)) throw e // already gone is fine
}begin
fileboy.delete_file(file.id)
rescue Fileboy::NotFoundError
# already gone is fine
endtry:
client.delete_file(file.id)
except fileboy.NotFoundError:
pass # already gone is finetry {
$fileboy->deleteFile($file->id);
} catch (\Fileboy\Exception\NotFoundException $e) {
// already gone is fine
}curl -X DELETE https://api.fileboy.io/files/FILE_ID \
-H "Authorization: token YOUR_SIGNED_TOKEN"
# 204 when deleted, 404 when there is no such file// up to 100 ids. Unknown ids are reported, not thrown.
const { deleted, notFound, failed } = await fileboy.destroyFiles(ids)# up to 100 ids. Unknown ids are reported, not raised.
result = fileboy.delete_files(ids)
result.deleted # ids that were deleted
result.not_found # unknown or already deleted
result.failed # still there because storage was unreachable: send again# up to 100 ids. Unknown ids are reported, not raised.
result = client.delete_files(ids)
result.deleted # ids that were deleted
result.not_found # unknown or already deleted
result.failed # still there because storage was unreachable: send again// up to 100 ids. Unknown ids are reported, not thrown.
$result = $fileboy->deleteFiles($ids);
$result->deleted; // ids that were deleted
$result->notFound; // unknown or already deleted
$result->failed; // still there because storage was unreachable: send againcurl -X POST https://api.fileboy.io/files/delete \
-H "Authorization: token YOUR_SIGNED_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids":["FILE_ID_1","FILE_ID_2"]}'// delete every file for an order that was cancelled, 100 at a time
let cursor: string | undefined
do {
const page = await fileboy.listFiles({ metadata: { orderId }, limit: 100, cursor })
if (page.data.length) await fileboy.destroyFiles(page.data.map(f => f.id))
cursor = page.nextCursor ?? undefined
} while (cursor)# delete every file for an order that was cancelled, 100 at a time
fileboy.each_file(metadata: { orderId: order.id }).each_slice(100) do |files|
fileboy.delete_files(files.map(&:id))
end# delete every file for an order that was cancelled, 100 at a time
batch = []
for file in client.iter_files(metadata={"orderId": order.id}):
batch.append(file.id)
if len(batch) == 100:
client.delete_files(batch)
batch = []
if batch:
client.delete_files(batch)// delete every file for an order that was cancelled, 100 at a time
$batch = [];
foreach ($fileboy->eachFile(['metadata' => ['orderId' => (string) $order->id]]) as $file) {
$batch[] = $file->id;
if (count($batch) === 100) {
$fileboy->deleteFiles($batch);
$batch = [];
}
}
if ($batch) {
$fileboy->deleteFiles($batch);
}Deleting is safe to repeat. A single delete of a file that is already gone is a not found error you can ignore. Bulk delete lists unknown IDs under notFound instead of failing. If storage is briefly unreachable the file is left exactly as it was and shows up under failed, so you send it again.
Rename a file or change its metadata
Updating changes only what you send. Metadata is merged into what is there, a null value removes a key, and you can ask for it to be replaced instead. Labels are names that point at a single file, so adding a label that is on another file moves it.
// metadata is merged: a null value removes a key
const updated = await fileboy.updateFileData(file.id, {
name: "invoice-1042-final.pdf",
metadata: { status: "paid", draft: null },
labels: { add: ["latest-invoice"] },
})# metadata is merged: nil removes a key
updated = fileboy.update_file(
file.id,
name: "invoice-1042-final.pdf",
metadata: { status: "paid", draft: nil },
labels: { add: ["latest-invoice"] }
)# metadata is merged: None removes a key
updated = client.update_file(
file.id,
name="invoice-1042-final.pdf",
metadata={"status": "paid", "draft": None},
add_labels=["latest-invoice"],
)// metadata is merged: null removes a key
$updated = $fileboy->updateFile($file->id, [
'name' => 'invoice-1042-final.pdf',
'metadata' => ['status' => 'paid', 'draft' => null],
'labels' => ['add' => ['latest-invoice']],
]);curl -X PATCH https://api.fileboy.io/files/FILE_ID \
-H "Authorization: token YOUR_SIGNED_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"invoice-1042-final.pdf","metadata":{"status":"paid","draft":null},"labels":{"add":["latest-invoice"]}}'Videos, labels and usage
const { data: videos } = await fileboy.listVideos({ status: "ready", limit: 20 })
await fileboy.updateVideo(videos[0].id, { name: "Launch day" })
await fileboy.destroyVideo(videos[0].id) // original, renditions, posters and transcriptspage = fileboy.list_videos(status: "ready", limit: 20)
fileboy.update_video(page.items.first.id, name: "Launch day")
fileboy.delete_video(page.items.first.id) # original, renditions, posters and transcriptspage = client.list_videos(status="ready", limit=20)
client.update_video(page.items[0].id, name="Launch day")
client.delete_video(page.items[0].id) # original, renditions, posters and transcripts$page = $fileboy->listVideos(['status' => 'ready', 'limit' => 20]);
$fileboy->updateVideo($page->data[0]->id, 'Launch day');
$fileboy->deleteVideo($page->data[0]->id); // original, renditions, posters and transcriptscurl -G https://api.fileboy.io/videos -H "Authorization: token YOUR_SIGNED_TOKEN" -d status=ready
curl -X DELETE https://api.fileboy.io/videos/VIDEO_ID -H "Authorization: token YOUR_SIGNED_TOKEN"const labels = await fileboy.listLabels()
await fileboy.createLabel("hero", { fileId: file.id })
await fileboy.destroyLabel("hero") // the file is keptlabels = fileboy.list_labels
fileboy.create_label("hero", file_id: file.id)
fileboy.delete_label("hero") # the file is keptlabels = client.list_labels()
client.create_label("hero", file_id=file.id)
client.delete_label("hero") # the file is kept$labels = $fileboy->listLabels();
$fileboy->createLabel('hero', $file->id);
$fileboy->deleteLabel('hero'); // the file is keptcurl -X POST https://api.fileboy.io/labels \
-H "Authorization: token YOUR_SIGNED_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"hero","fileId":"FILE_ID"}'const usage = await fileboy.getUsage()
console.log(usage.files, usage.storageBytes, usage.limits.storageBytes)usage = fileboy.fetch_usage
puts "#{usage.files} files, #{usage.storage_bytes} bytes"usage = client.get_usage()
print(usage.files, usage.storage_bytes, usage.limits.storage_bytes)$usage = $fileboy->getUsage();
echo $usage->files, ' files, ', $usage->storageBytes, " bytes
";curl https://api.fileboy.io/usage -H "Authorization: token YOUR_SIGNED_TOKEN"Checked against the current libraries in November 2026. Found something that no longer works? Tell us.