import { request } from "../utils/request"; import { getTenantCode } from "../utils/tenant"; export const commonApi = { normalizeUploadType: (type, file) => { const normalized = (type || "").toLowerCase(); const mime = (file && file.type) || ""; if ( normalized === "video" || normalized === "audio" || normalized === "image" ) { return normalized; } if (mime.startsWith("video/")) return "video"; if (mime.startsWith("audio/")) return "audio"; return "image"; }, getOptions: () => request("/common/options"), checkHash: (hash) => request(`/upload/check?hash=${hash}`), deleteMedia: (id) => request(`/media-assets/${id}`, { method: "DELETE" }), upload: (file, type) => { const normalizedType = commonApi.normalizeUploadType(type, file); const formData = new FormData(); formData.append("file", file); formData.append("type", normalizedType); return request("/upload", { method: "POST", body: formData }); }, uploadMultipart: (file, hash, type, onProgress) => { const controller = new AbortController(); const signal = controller.signal; const normalizedType = commonApi.normalizeUploadType(type, file); const promise = (async () => { // 1. Check Hash try { const res = await commonApi.checkHash(hash); if (res) { if (onProgress) onProgress(100); return res; } } catch (e) { // Ignore hash check errors } if (signal.aborted) throw new Error("Aborted"); // 2. Init const initRes = await request("/upload/init", { method: "POST", body: { filename: file.name, size: file.size, mime_type: file.type, hash: hash, type: normalizedType, }, signal, }); const { upload_id, chunk_size } = initRes; const totalChunks = Math.ceil(file.size / chunk_size); // 3. Upload Parts for (let i = 0; i < totalChunks; i++) { if (signal.aborted) throw new Error("Aborted"); const start = i * chunk_size; const end = Math.min(start + chunk_size, file.size); const chunk = file.slice(start, end); const formData = new FormData(); formData.append("file", chunk); formData.append("upload_id", upload_id); formData.append("part_number", i + 1); // request helper with FormData handles content-type, but we need signal await request("/upload/part", { method: "POST", body: formData, signal, }); if (onProgress) { const percent = Math.round(((i + 1) / totalChunks) * 100); onProgress(percent); } } // 4. Complete return request("/upload/complete", { method: "POST", body: { upload_id }, signal, }); })(); return { promise, abort: () => controller.abort() }; }, uploadWithProgress: (file, type, onProgress) => { const normalizedType = commonApi.normalizeUploadType(type, file); let xhr; const promise = new Promise((resolve, reject) => { const formData = new FormData(); formData.append("file", file); formData.append("type", normalizedType); xhr = new XMLHttpRequest(); const tenantCode = getTenantCode(); if (!tenantCode) { reject(new Error("Tenant code missing in URL")); return; } xhr.open("POST", `/t/${tenantCode}/v1/upload`); const token = localStorage.getItem("token"); if (token) { xhr.setRequestHeader("Authorization", `Bearer ${token}`); } xhr.upload.onprogress = (event) => { if (event.lengthComputable && onProgress) { const percentComplete = (event.loaded / event.total) * 100; onProgress(percentComplete); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.responseText); resolve(response); } catch (e) { reject(e); } } else { reject(new Error(xhr.statusText || "Upload failed")); } }; xhr.onerror = () => reject(new Error("Network Error")); xhr.onabort = () => reject(new Error("Aborted")); xhr.send(formData); }); return { promise, abort: () => xhr.abort() }; }, };