@filefreak.io/sdk

中文文档
0.20.0发布于 2026/11/11Apache-2.0TypeScriptES Module零依赖近 30 天下载 4374

FileFreak's developer API for Node and the browser: every endpoint, uploads straight to storage in parts, downloads in ranges, and the changes to follow.

原始使用文档
文档目录12 节
  1. On a server
  2. Every download is checked
  3. A new version of a large file
  4. In a web page
  5. Previews
  6. An image at a smaller size
  7. Saving from a link
  8. Every endpoint
  9. Reading to the end
  10. When an upload stops
  11. When something is refused
  12. Licence

@filefreak.io/sdk

The FileFreak developer API, from TypeScript or JavaScript, in Node 20 and later and in browsers. No dependencies.

Every endpoint is a function, typed from the same table the API is built from. Around them: uploads sent straight to storage in parts and carried on after an interruption, a new version of a large file sending only what changed, downloads fetched in ranges and checked against the file's SHA-256, listings and changes read to the end, files under Shard Protection sealed as they are sent and read through their maps, what a share link holds saved with no key, and requests sent again only when that cannot make anything twice. The reference is at https://filefreak.io/developers/reference.

On a server

import { FileFreak } from "@filefreak.io/sdk";
import { fromPath, toPath } from "@filefreak.io/sdk/node";

const ff = new FileFreak({ apiKey: process.env.FILEFREAK_KEY });

const { item: folder } = await ff.folders.create({ name: "uploads", ifExists: "use" });
const file = await ff.upload(await fromPath("film.mp4"), {
  parentId: folder.id,
  onProgress: (sent, total) => console.log(`${Math.round((sent / total) * 100)}%`),
});

await ff.download(file.id, await toPath("copy.mp4"));
const bytes = await ff.bytes(file.id); // for a file small enough to hold in memory

fromPath reads the file a part at a time as it is sent, so a file larger than memory is fine. toPath writes beside the file and gives it its name only once every byte is there: a download that fails leaves nothing behind. A file under Shard Protection has no address, and download and bytes read it through its map instead, each fragment checked as it arrives. One written in version 2 of the shard format, in blocks cut by its content, is read the same way, its outline checked before any block is fetched. Into a folder under Shard Protection, upload seals each part with the file's own key before it is sent, so none of the file is ever plain in storage. The key is asked of the API for the upload and kept nowhere, and an upload carried on asks which fragments storage has before it sends any.

A file kept compressed in storage is put back together as it is read, from 0.7.0. download, bytes and get ask for its address saying they can, fetch each of its places as a range of that address, and inflate the ones stored compressed, each checked to give exactly the bytes the API's layout says it holds. A file that does not add up is refused and nothing is kept: layout when the layout itself does not, blob_size when storage holds less than it says, place_damaged when a place is not what was stored, and place_unavailable when storage will not give one after several tries.

A file uploaded into a public folder, or in place of a public file, is never stored compressed, and nor is an archive, a picture, a video or an installer (an AppImage, an msi or a pkg among them). Its publicUrl gives its bytes, for a download link or an <img>. One stored compressed before it was made public has packed: true, and its publicUrl opens its page, which puts it back together in the browser: upload it again into the public folder for an address that gives the bytes.

Keep keys on a server. A key sent from a web page is refused.

Every download is checked

Every upload works out its file's SHA-256 from the bytes it sends and records it with the file, from 0.15.0, whether or not sha256 is given; one given is checked against it. Every download, bytes and get hashes what it writes and compares it with the SHA-256 the API gives for the file, never one read from storage, so a file changed in storage or on its way is caught. One that does not match throws IntegrityError and its sink is aborted, so toPath leaves nothing behind.

import { IntegrityError } from "@filefreak.io/sdk";

const got = await ff.download(file.id, await toPath("copy.mp4"));
got.verification; // { verified: true, by: "sha256", sha256: "1a2b..." }

verification says how it was checked: by: "sha256"; by: "seals" for a file under Shard Protection, every piece of which is checked against its seal as it arrives; or verified: false with reason: "no_checksum" for a file uploaded without one, as files sent from the website before November 2026 were. get gives each file it saved a verification of its own.

A large file is fetched as several ranges at once, which arrive out of order. What arrives in order is hashed as it comes, and the rest is read back from the sink once every byte is written, reported to onVerifying. That needs the sink to have read, which toPath and bytes have; a sink of your own without it is given the file in order, as one range, which is slower. { verify: false } writes a download as it arrives, unchecked.

In a web page, @filefreak.io/sdk/badge shows it: "Verified" with a shield, and, when it is opened, what that means.

import { verifiedBadge } from "@filefreak.io/sdk/badge";

row.append(verifiedBadge(got.verification));
// After an IntegrityError: verifiedBadge(null, true), "Did not match".

It draws inside its own shadow root and has no dependencies. Its colours are --filefreak-verified-good, --filefreak-verified-bad and --filefreak-verified-muted. A checksum says the file is the one that was uploaded; it does not say the file is safe to open.

A new version of a large file

A file of 16 MiB or more is read through before it is sent and cut where its content says, and each part is one of its blocks. A new version of it, sent with onConflict: "replace", sends only the blocks the file does not have already: those it has are put together with the new ones inside storage. The SDK and the CLI cut a file alike, so the next version of a file either one sent keeps its blocks whichever sends it.

await ff.upload(await fromPath("disk.img"), { parentId: folder.id, onConflict: "replace" });

Into a folder under Shard Protection the same holds, with every block encrypted before it leaves. A protected file of more than 16 MiB is cut by its own key, so that nobody without the key can cut a copy of it the same way and compare, and its next version keeps the blocks the last one has when the key may read files; a key that may only write starts a new file key and sends every block. Each block is read again as it is sent and checked against what the upload began with, so a file changed while it is sent stops the upload (source_changed) rather than storing a mix of two.

In a web page

A page is given a token by your server instead. Your server makes it with its key:

// On your server, behind your own sign-in.
const { token } = await ff.tokens.create({
  scopes: ["files:read", "files:write"],
  folderId: customerFolderId,
  expiresInSeconds: 900,
});

and the page uses it as it would a key. Given a function rather than a string, the SDK asks your server for a new token when the last has run out:

import { FileFreak } from "@filefreak.io/sdk";

const ff = new FileFreak({
  token: async () => (await fetch("/my-server/filefreak-token")).text(),
});

const input = document.querySelector<HTMLInputElement>("input[type=file]")!;
const file = await ff.upload(input.files![0]!); // into the token's folder

A token may do what its key may, or less: the scopes asked for, inside the folder asked for, for a minute to an hour. It is checked against its key on every request, so revoking the key stops its tokens at once. A token cannot make another.

Previews

Once the service has made them, within a few minutes of an upload, a file has thumbnailUrl, a small WebP picture of an image, a photo from an iPhone, a PDF's first page, the cover in an audio file's tags or a frame of a video; or previewTextUrl, the first lines of a text or code file or a document, as text. Both are addresses on storage itself that need no key. Each is the same all day, so a browser keeps what it fetched, and fetching one counts as nothing. They are null until made, and for a file with none.

const { items } = await ff.files.list({ parent: folder.id });
for (const item of items) {
  if (item.thumbnailUrl) gallery.append(Object.assign(new Image(), { src: item.thumbnailUrl, alt: item.name }));
}

files.preview(item) fetches one and gives it back ready to use, as { kind: "picture", bytes, contentType: "image/webp" } or { kind: "text", text }, or null for a file with none. files.previewsOf(items) does the same for many at once, as a Map by id that leaves out the files with none.

const previews = await ff.files.previewsOf(items);
for (const [id, preview] of previews) {
  if (preview.kind === "text") console.log(id, preview.text.split("\n")[0]);
}

The service never reads a video, so a video's picture is a frame of it that you send. In a web page, upload draws one about a second in from the file the page has, and sends it once the file is stored, without the upload waiting for it; frame: false sends none. On a server, make a JPEG of a frame with anything that can, and send that:

import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { promisify } from "node:util";

// The frame a second in, 960 pixels wide, as a JPEG.
await promisify(execFile)("ffmpeg", ["-ss", "1", "-i", "film.mp4", "-frames:v", "1", "-vf", "scale=960:-2", "-y", "frame.jpg"]);
await ff.files.sendFrame(file.id, new Blob([await readFile("frame.jpg")]));

A video's picture is a JPEG of at most 2 MB, sent once. A file under Shard Protection has no previews.

An image at a smaller size

files.image(id, { size }) gives an address for a copy of an image no more than size pixels on its longest side, as WebP with the camera's metadata removed: 64, 128, 256, 512, 1024 or 2048. Each size is made once, the first time it is asked for, and kept. Until it has been made the answer is { status: "pending", retryAfterSeconds }, so ask again after that long:

async function sized(id: string, size: number) {
  for (;;) {
    const answer = await ff.files.image(id, { size });
    if (answer.status === "ready") return answer; // { url, width, height, expiresAt }
    await new Promise((r) => setTimeout(r, answer.retryAfterSeconds * 1000));
  }
}

Name the sizes as the image is uploaded and they are ready by the time they are wanted: ff.upload(file, { imageSizes: [512, 2048] }). A public file needs none of this: add ?size=512 to its publicUrl.

get saves what a share link or a public page holds. It needs no key, and sends nothing that says who is asking: no key, and from a browser no cookies. A file goes into one sink; a folder needs somewhere for each of its files, which toFolder makes on disk.

import { get } from "@filefreak.io/sdk";
import { toFolder, toPath } from "@filefreak.io/sdk/node";

await get("https://filefreak.link/share/...", await toPath("report.pdf"));

const got = await get("https://filefreak.link/share/...", toFolder("downloads"), {
  password: "open sesame", // for a link that has one
  onProgress: (written, total) => console.log(`${written} of ${total} bytes`),
});
console.log(`${got.files.length} files, ${got.sizeBytes} bytes`);

It takes a share link, a public file's or folder's page, and a file's direct address. A folder comes whole: each file goes to the sink the function makes for it, given its name and the folders it is in. Each file saved is one of the link's downloads, as on its page, and a link with fewer left than there are files saves nothing (not_enough_downloads). A file under Shard Protection is read through the link's map, and counted once. Every file is checked as download checks one, against the SHA-256 the link's own site gives for it.

Every name a link gives is its sharer's, and can be anything: ../../.bashrc, or one that reads backwards. toFolder makes each one safe with safeName before writing, and never writes outside the folder it is given. A program that names files from a link itself should use safeName too.

Every endpoint

The functions are grouped as the reference groups the endpoints: ff.files, ff.folders, ff.tree, ff.changes, ff.uploads, ff.usage, ff.me, ff.tokens and ff.shares (share links, which only a device signed in with filefreak login may make), and ff.sites for an app's sites, with a key allowed sites:deploy. Ids in the address come first, then the body or the query, then options:

const { items, nextCursor } = await ff.files.list({ parent: folder.id, limit: 50 });
await ff.files.update(file.id, { name: "renamed.mp4" });
await ff.files.delete(file.id, { permanent: true });
await ff.sites.update(site.id, { singlePage: false }); // missing pages are 404s again

ff.call("files.get", { params: { id } }) reaches any endpoint by its name.

Reading to the end

for await (const item of ff.files.each({ parent: folder.id })) console.log(item.name);

// Everything under a folder, then only what changes.
const walk = ff.tree.walk({ parent: folder.id });
for await (const item of walk) keep(item);
for await (const answer of ff.changes.follow(walk.changesCursor)) {
  for (const change of answer.changes) apply(change);
  save(answer.cursor); // to carry on from after a restart
}

follow waits on the API for the next change, a minute at a time, and goes on until the loop is left or its signal is aborted.

A copy that also sends its own changes back names the file as it read it, so that it never writes over, or deletes, a change it has not seen. Either is refused with 409 revision_changed if the file has moved on:

await ff.upload(source, { name: file.name, parentId: file.parentId, onConflict: "replace", replacing: { id: file.id, revision: file.revision } });
await ff.files.delete(file.id, { ifRevision: file.revision });

When an upload stops

What reached storage is kept until the upload expires. Keep the upload's id from onBegun, and carry on with the same file: only the parts storage does not have are sent. A file cut by its content is carried on block by block, each checked against what the upload began with, so carrying on with another file is refused at its first block.

import { UploadStopped } from "@filefreak.io/sdk";

try {
  await ff.upload(source, { name: "big.iso", onBegun: (u) => remember(u.id) });
} catch (e) {
  if (e instanceof UploadStopped) await ff.upload(source, { resume: e.uploadId });
  else throw e;
}

When something is refused

A refusal is a FileFreakError, with the API's status, a code that does not change and is what to branch on, and a message for a person.

import { FileFreakError } from "@filefreak.io/sdk";

try {
  await ff.folders.create({ name: "reports" });
} catch (e) {
  if (e instanceof FileFreakError && e.code === "name_taken") {
    // already there
  } else throw e;
}

The SDK sends a request again only when that cannot make anything twice: a read; a request with an Idempotency-Key, which it makes for every endpoint that honours one unless you give your own ({ idempotencyKey } in the options); completing an upload or making a token; or an answer saying nothing was done, such as 429. While the service is waking up after a quiet spell it waits, for up to a few minutes. Retry-After is honoured.

Licence

Apache-2.0.

版本与发布记录

@filefreak.io/sdk 共发布 23 个版本,首次发布于 2026/10/26;过去 12 个月发布了 23 个版本。

版本发布日期距今
0.20.0latest2026/11/113 天前
0.19.02026/11/104 天前
0.18.02026/11/095 天前
0.17.02026/11/086 天前
0.16.02026/11/068 天前
0.15.02026/11/068 天前

关于 @filefreak.io/sdk 的常见问题

@filefreak.io/sdk 是什么?

FileFreak's developer API for Node and the browser: every endpoint, uploads straight to storage in parts, downloads in ranges, and the changes to follow.

如何安装 @filefreak.io/sdk?

在项目目录运行 npm install @filefreak.io/sdk,或使用 pnpm add / yarn add / bun add。

@filefreak.io/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@filefreak.io/sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@filefreak.io/sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@filefreak.io/sdk 可以商用吗?

@filefreak.io/sdk 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@filefreak.io/sdk 的下载量有多少?

根据 npm 官方下载统计,@filefreak.io/sdk 在 2026/10/15 – 2026/11/13 这 30 天内被下载 4,374 次,日均约 146 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@filefreak.io/sdk 还在维护吗?

最新版本 0.20.0 发布于 2026/11/11(3 天前);至今共发布 23 个版本,其中过去 12 个月发布了 23 个;首次发布于 2026/10/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。