lewtun's picture
lewtun HF Staff
Add dataset uploads to Hub (#255)
2b4c539 unverified
/**
* Centralized API utilities.
*
* In production: HttpOnly cookie (hf_access_token) is sent automatically.
* In development: auth is bypassed on the backend.
*/
import { triggerLogin } from '@/hooks/useAuth';
export interface ApiUploadProgress {
loaded: number;
total: number | null;
percent: number | null;
}
async function handleUnauthorized(response: Response): Promise<void> {
if (response.status !== 401) return;
try {
const authStatus = await fetch('/auth/status', { credentials: 'include' });
const data = await authStatus.json();
if (data.auth_enabled) {
triggerLogin();
throw new Error('Authentication required — redirecting to login.');
}
} catch (e) {
if (e instanceof Error && e.message.includes('redirecting')) throw e;
}
}
/** Wrapper around fetch with credentials and common headers. */
export async function apiFetch(
path: string,
options: RequestInit = {}
): Promise<Response> {
const headers = new Headers(options.headers);
const isFormData = options.body instanceof FormData;
if (!isFormData && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
const response = await fetch(path, {
...options,
headers,
credentials: 'include', // Send cookies with every request
});
await handleUnauthorized(response);
return response;
}
function headersFromXhr(rawHeaders: string): Headers {
const headers = new Headers();
rawHeaders.trim().split(/[\r\n]+/).forEach((line) => {
const separator = line.indexOf(':');
if (separator <= 0) return;
headers.append(
line.slice(0, separator).trim(),
line.slice(separator + 1).trim(),
);
});
return headers;
}
export async function apiUpload(
path: string,
formData: FormData,
options: { onProgress?: (progress: ApiUploadProgress) => void } = {},
): Promise<Response> {
return new Promise<Response>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', path);
xhr.withCredentials = true;
xhr.upload.onprogress = (event) => {
const total = event.lengthComputable ? event.total : null;
const percent = total
? Math.min(100, Math.round((event.loaded / total) * 100))
: null;
options.onProgress?.({ loaded: event.loaded, total, percent });
};
xhr.onerror = () => reject(new Error('Network error while uploading.'));
xhr.onabort = () => reject(new Error('Dataset upload was canceled.'));
xhr.onload = () => {
const response = new Response(xhr.responseText, {
status: xhr.status,
statusText: xhr.statusText,
headers: headersFromXhr(xhr.getAllResponseHeaders()),
});
handleUnauthorized(response).then(() => resolve(response)).catch(reject);
};
xhr.send(formData);
});
}