rspace-online/server/ipfs.ts

103 lines
2.6 KiB
TypeScript

/**
* IPFS Client — Server-side pinning and gateway access via Kubo API.
*
* No encryption here — backups are already encrypted by the client,
* and generated files are intentionally public.
*/
const IPFS_API_URL = process.env.IPFS_API_URL || "http://collab-server-ipfs-1:5001";
const IPFS_GATEWAY_URL =
process.env.IPFS_GATEWAY_URL || "https://ipfs.jeffemmett.com";
/** Returns true if IPFS_API_URL is configured (always true with default). */
export function isIPFSEnabled(): boolean {
return !!IPFS_API_URL;
}
/** Public gateway URL for a CID. */
export function ipfsGatewayUrl(cid: string): string {
return `${IPFS_GATEWAY_URL}/ipfs/${cid}`;
}
/**
* Pin a blob to IPFS via Kubo's /api/v0/add endpoint.
* Returns the CID (content hash).
*/
export async function pinToIPFS(
data: Uint8Array,
filename: string,
): Promise<string> {
const formData = new FormData();
formData.append(
"file",
new Blob([data.buffer as ArrayBuffer]),
filename,
);
const res = await fetch(`${IPFS_API_URL}/api/v0/add?pin=true`, {
method: "POST",
body: formData,
});
if (!res.ok) {
const err = await res.text();
throw new Error(`IPFS pin failed (${res.status}): ${err}`);
}
const result = (await res.json()) as { Hash: string; Size: string };
return result.Hash;
}
/**
* Unpin a CID from the local Kubo node.
*/
export async function unpinFromIPFS(cid: string): Promise<void> {
const res = await fetch(
`${IPFS_API_URL}/api/v0/pin/rm?arg=${encodeURIComponent(cid)}`,
{ method: "POST" },
);
// 500 with "not pinned" is fine — idempotent unpin
if (!res.ok) {
const err = await res.text();
if (!err.includes("not pinned")) {
throw new Error(`IPFS unpin failed (${res.status}): ${err}`);
}
}
}
/**
* Fetch content from IPFS by CID (via gateway).
*/
export async function fetchFromIPFS(cid: string): Promise<Response> {
return fetch(`${IPFS_GATEWAY_URL}/ipfs/${cid}`);
}
/**
* Get IPFS node status — peer ID, repo size, number of objects.
*/
export async function getIPFSStatus(): Promise<{
peerId: string;
repoSize: number;
numObjects: number;
}> {
const [idRes, statRes] = await Promise.all([
fetch(`${IPFS_API_URL}/api/v0/id`, { method: "POST" }),
fetch(`${IPFS_API_URL}/api/v0/repo/stat`, { method: "POST" }),
]);
if (!idRes.ok) throw new Error(`IPFS id failed: ${idRes.status}`);
if (!statRes.ok) throw new Error(`IPFS repo/stat failed: ${statRes.status}`);
const id = (await idRes.json()) as { ID: string };
const stat = (await statRes.json()) as {
RepoSize: number;
NumObjects: number;
};
return {
peerId: id.ID,
repoSize: stat.RepoSize,
numObjects: stat.NumObjects,
};
}