Skip to main content
Glama
upload-primitives.ts•6.93 kB
import axios from "axios"; import { api } from "../api"; import { FileType, FileSource } from "../api"; export type UploadStatus = "idle" | "preparing" | "uploading" | "finalizing" | "completed" | "error"; export type FileWithPreview = File & { preview?: string }; // Node.js File-like interface for MCP export interface NodeFileData { buffer: Buffer; name: string; size: number; type: string; lastModified: number; slice(start: number, end: number): { arrayBuffer(): Promise<ArrayBuffer> }; } // Union type for browser File or Node.js file data export type FileOrNodeFile = FileWithPreview | NodeFileData; const extractEtagFromHeaders = (headers: Record<string, any>) => { const etag = headers.etag || headers.ETag || headers["etag"]; if (!etag) return `""`; if (typeof etag === "string") return etag; return etag.toString(); }; const uploadFileChunk = async (params: { url: string; data: ArrayBuffer; onProgress: (progress: number) => void; signal?: AbortSignal; }) => { const { url, data, onProgress, signal } = params; const response = await axios.request({ method: "PUT", url, data, signal, onUploadProgress: (progressEvent) => { if (progressEvent.total) { const progress = progressEvent.loaded / progressEvent.total; onProgress(progress); } }, }); if (response.status !== 200) { throw new Error("Failed to upload chunk"); } return JSON.parse(extractEtagFromHeaders(response.headers)) as string; }; const detectFileType = (mimeType: string): FileType => { if (mimeType.startsWith("image/")) { if (mimeType === "image/gif") return FileType.Gif; return FileType.Image; } if (mimeType.startsWith("video/")) return FileType.Video; return FileType.Unknown; }; export class UploadPrimitives { public file: FileOrNodeFile | null = null; public fileId: string | null = null; public fileType: FileType | null = null; public metadata: Record<string, any> | null = null; public status: UploadStatus = "idle"; public progress = 0; public error: Error | null = null; public totalChunks = 0; public currentChunk = 0; public completedChunks: Array<{ eTag: string; partNumber: number }> = []; public uploadInfo: { chunkCount: number; chunkSize: number; fileUploadId: string; uploadPartUrls: Array<{ url: string; partNumber: number }>; } | null = null; constructor(private options?: { disableThumbnail?: boolean; isPrivate?: boolean }) {} reset() { this.file = null; this.fileId = null; this.fileType = null; this.metadata = null; this.status = "idle"; this.progress = 0; this.error = null; this.totalChunks = 0; this.currentChunk = 0; this.completedChunks = []; this.uploadInfo = null; } async prepareUpload(params: { file: FileOrNodeFile; metadata: string; fileType: FileType; source: FileSource; deviceId: string; }) { const { file, metadata, fileType, source, deviceId } = params; this.status = "preparing"; this.error = null; this.file = file; this.fileType = fileType; try { const response = await api.assets.prepareNewFile({ metadata, deviceId, type: fileType, source, fileSize: file.size, isPrivate: this.options?.isPrivate, }); if (response.status !== "ok") { throw new Error("Failed to prepare file upload"); } this.fileId = response.fileId; this.uploadInfo = { chunkCount: response.chunkCount, chunkSize: response.chunkSize, fileUploadId: response.fileUploadId, uploadPartUrls: response.uploadPartUrls, }; this.totalChunks = response.chunkCount; return response; } catch (error) { this.status = "error"; this.error = error instanceof Error ? error : new Error(String(error)); throw error; } } // Helper function to check if file is NodeFileData private isNodeFile(file: FileOrNodeFile): file is NodeFileData { return "buffer" in file; } async uploadChunk(chunkIndex: number): Promise<{ eTag: string; partNumber: number }> { if (!this.file || !this.uploadInfo) { throw new Error("File or upload info not available"); } this.currentChunk = chunkIndex + 1; const part = this.uploadInfo.uploadPartUrls[chunkIndex]; if (!part) { throw new Error("Invalid chunk index"); } const { url, partNumber } = part; const { chunkSize } = this.uploadInfo; const start = chunkIndex * chunkSize; const end = Math.min(start + chunkSize, this.file.size); const buffer = await this.file.slice(start, end).arrayBuffer(); let retries = 3; while (retries > 0) { try { const eTag = await uploadFileChunk({ url, data: buffer, onProgress: (chunkProgress) => { const overallProgress = (chunkIndex + chunkProgress) / this.uploadInfo!.chunkCount; this.progress = overallProgress; }, }); const completedChunk = { eTag, partNumber }; this.completedChunks.push(completedChunk); return completedChunk; } catch (error) { retries--; if (retries === 0) { this.status = "error"; this.error = error instanceof Error ? error : new Error(String(error)); throw new Error(`Failed to upload chunk ${chunkIndex + 1}`); } } } throw new Error(`Failed to upload chunk ${chunkIndex + 1} - all retries exhausted`); } async uploadAllChunks(): Promise<Array<{ eTag: string; partNumber: number }>> { if (!this.uploadInfo) { throw new Error("Upload info not available"); } this.status = "uploading"; const results: Array<{ eTag: string; partNumber: number }> = []; for (let i = 0; i < this.uploadInfo.chunkCount; i++) { try { const result = await this.uploadChunk(i); results.push(result); } catch (error) { this.status = "error"; throw error; } } return results; } async finalizeUpload() { if (!this.fileId || !this.file || this.completedChunks.length === 0) { throw new Error("Missing data for upload finalization"); } this.status = "finalizing"; try { const response = await api.assets.completeUpload({ fileId: this.fileId, chunks: this.completedChunks, mimeType: this.file.type, disableThumbnail: this.options?.disableThumbnail, }); if (response.status !== "ok") { throw new Error("Failed to finalize upload"); } this.status = "completed"; return response; } catch (error) { this.status = "error"; this.error = error instanceof Error ? error : new Error(String(error)); throw error; } } } export const detectFileTypeFromMimeType = detectFileType;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/xkelxmc/uranium-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server