forked from maik/IPFS-portal
218 lines
6.0 KiB
TypeScript
218 lines
6.0 KiB
TypeScript
|
|
/* ── IPFS Portal API Layer ── */
|
||
|
|
|
||
|
|
const API_BASE = ''; // nginx proxy /api/* → backend
|
||
|
|
|
||
|
|
export interface IPFSNodeInfo {
|
||
|
|
version: string;
|
||
|
|
peerId: string;
|
||
|
|
peers: number;
|
||
|
|
storageUsed: string;
|
||
|
|
storageMax: string;
|
||
|
|
uptime: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IPFSPin {
|
||
|
|
cid: string;
|
||
|
|
name: string;
|
||
|
|
size: number;
|
||
|
|
created: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IPFSUser {
|
||
|
|
username: string;
|
||
|
|
created: string;
|
||
|
|
active: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Generic fetch wrapper ── */
|
||
|
|
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
||
|
|
const res = await fetch(`${API_BASE}/api${path}`, {
|
||
|
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||
|
|
...options,
|
||
|
|
credentials: 'include',
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const text = await res.text().catch(() => '');
|
||
|
|
throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 200)}`);
|
||
|
|
}
|
||
|
|
return res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Node info ── */
|
||
|
|
export async function getNodeInfo(): Promise<IPFSNodeInfo> {
|
||
|
|
return apiFetch('/node/info');
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function getPeers(): Promise<{ id: string; addr: string; latency: string }[]> {
|
||
|
|
const raw = await apiFetch<{ Peers?: { Peer: string; Addr: string; Latency: string }[] }>('/peers');
|
||
|
|
return (raw.Peers || []).map((p: { Peer: string; Addr: string; Latency: string }) => ({
|
||
|
|
id: p.Peer,
|
||
|
|
addr: p.Addr,
|
||
|
|
latency: p.Latency || '—',
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Storage ── */
|
||
|
|
export async function listPins(): Promise<IPFSPin[]> {
|
||
|
|
const raw = await apiFetch<{ Keys?: Record<string, { Type: string }> }>('/pins');
|
||
|
|
if (!raw.Keys) return [];
|
||
|
|
return Object.entries(raw.Keys).map(([cid, info]) => ({
|
||
|
|
cid,
|
||
|
|
name: '',
|
||
|
|
size: 0,
|
||
|
|
created: info.Type || '',
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function addPin(cid: string, name?: string): Promise<{ cid: string }> {
|
||
|
|
return apiFetch('/pins', {
|
||
|
|
method: 'POST',
|
||
|
|
body: JSON.stringify({ cid, name }),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function removePin(cid: string): Promise<void> {
|
||
|
|
await apiFetch(`/pins/${cid}`, { method: 'DELETE' });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Files ── */
|
||
|
|
export async function uploadFile(file: File): Promise<{ cid: string; size: number }> {
|
||
|
|
const form = new FormData();
|
||
|
|
form.append('file', file);
|
||
|
|
const res = await fetch(`${API_BASE}/api/files/upload`, {
|
||
|
|
method: 'POST',
|
||
|
|
body: form,
|
||
|
|
credentials: 'include',
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error(`Upload failed: ${res.statusText}`);
|
||
|
|
return res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function listFiles(): Promise<{ name: string; cid: string; size: number; modified: string }[]> {
|
||
|
|
return apiFetch('/files');
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Users (admin) ── */
|
||
|
|
export async function listUsers(): Promise<IPFSUser[]> {
|
||
|
|
const raw = await apiFetch<IPFSUser[] | { users: IPFSUser[] }>('/users');
|
||
|
|
// Handle both [{...}] and {users: [{...}]} response formats
|
||
|
|
if (Array.isArray(raw)) return raw;
|
||
|
|
if (raw && Array.isArray(raw.users)) return raw.users;
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function createUser(username: string, password: string): Promise<{ username: string }> {
|
||
|
|
return apiFetch('/users', {
|
||
|
|
method: 'POST',
|
||
|
|
body: JSON.stringify({ username, password }),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function deleteUser(username: string): Promise<void> {
|
||
|
|
await apiFetch(`/users/${encodeURIComponent(username)}`, { method: 'DELETE' });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── IPFS Explorer ── */
|
||
|
|
|
||
|
|
export interface IPFSEntry {
|
||
|
|
name: string;
|
||
|
|
type: 'file' | 'dir';
|
||
|
|
size: number;
|
||
|
|
hash: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function explorerLs(cid: string): Promise<IPFSEntry[]> {
|
||
|
|
return apiFetch(`/explorer/ls/${encodeURIComponent(cid)}`, { method: 'POST' });
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function explorerCat(cid: string): Promise<string> {
|
||
|
|
const res = await fetch(`${API_BASE}/api/explorer/cat/${encodeURIComponent(cid)}`, {
|
||
|
|
method: 'POST',
|
||
|
|
credentials: 'include',
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const text = await res.text().catch(() => '');
|
||
|
|
throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 200)}`);
|
||
|
|
}
|
||
|
|
return res.text();
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function explorerStat(cid: string): Promise<Record<string, unknown>> {
|
||
|
|
return apiFetch(`/explorer/stat/${encodeURIComponent(cid)}`, { method: 'POST' });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── IPNS ── */
|
||
|
|
|
||
|
|
export interface IPNSKey {
|
||
|
|
name: string;
|
||
|
|
id: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function listIPNSKeys(): Promise<IPNSKey[]> {
|
||
|
|
const res = await apiFetch<{ Keys: IPNSKey[] }>('/ipns/keys');
|
||
|
|
return res.Keys || [];
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function ipnsPublish(cid: string, key = 'self'): Promise<{ name: string; value: string }> {
|
||
|
|
return apiFetch(`/ipns/publish?arg=${encodeURIComponent(cid)}&key=${encodeURIComponent(key)}`, {
|
||
|
|
method: 'POST',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function ipnsResolve(name: string): Promise<string> {
|
||
|
|
const res = await apiFetch<{ Path: string }>(`/ipns/resolve/${encodeURIComponent(name)}`);
|
||
|
|
return res.Path || '';
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function ipnsGenKey(name: string): Promise<IPNSKey> {
|
||
|
|
return apiFetch(`/ipns/keys/gen/${encodeURIComponent(name)}`, { method: 'POST' });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Repo / Bandwidth Stats ── */
|
||
|
|
|
||
|
|
export interface RepoStats {
|
||
|
|
repoSize: number;
|
||
|
|
storageMax: number;
|
||
|
|
numObjects: number;
|
||
|
|
repoPath: string;
|
||
|
|
version: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface BWStats {
|
||
|
|
totalIn: number;
|
||
|
|
totalOut: number;
|
||
|
|
rateIn: number;
|
||
|
|
rateOut: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function getRepoStats(): Promise<RepoStats> {
|
||
|
|
const raw = await apiFetch<{
|
||
|
|
RepoSize?: number; StorageMax?: number; NumObjects?: number;
|
||
|
|
RepoPath?: string; Version?: string;
|
||
|
|
}>('/repo/stats');
|
||
|
|
return {
|
||
|
|
repoSize: raw.RepoSize ?? 0,
|
||
|
|
storageMax: raw.StorageMax ?? 0,
|
||
|
|
numObjects: raw.NumObjects ?? 0,
|
||
|
|
repoPath: raw.RepoPath ?? '',
|
||
|
|
version: raw.Version ?? '',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function getBWStats(): Promise<BWStats> {
|
||
|
|
const raw = await apiFetch<{
|
||
|
|
TotalIn?: number; TotalOut?: number; RateIn?: number; RateOut?: number;
|
||
|
|
}>('/bw/stats');
|
||
|
|
return {
|
||
|
|
totalIn: raw.TotalIn ?? 0,
|
||
|
|
totalOut: raw.TotalOut ?? 0,
|
||
|
|
rateIn: raw.RateIn ?? 0,
|
||
|
|
rateOut: raw.RateOut ?? 0,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Health ── */
|
||
|
|
export async function checkHealth(): Promise<{ status: string; node: string }> {
|
||
|
|
return apiFetch('/health');
|
||
|
|
}
|