Skip to main content
Glama
index.ts12.7 kB
/** * Alibaba Cloud OSS (Object Storage Service) Provider * * This provider implements the Provider interface for Alibaba Cloud OSS. * SDK Repository: https://github.com/ali-sdk/ali-oss * Documentation: https://help.aliyun.com/product/31815.html */ import OSS from "ali-oss"; import { Provider, ProviderRegistry } from "../interface.js"; import type { SourceConfig, ProviderType } from "../../types/config.js"; import type { BucketInfo, ObjectInfo, ObjectContent, ObjectMetadata, ListObjectsResult, SearchFilter, SearchResult, } from "../../types/storage.js"; import type { ListObjectsOptions, GetObjectOptions } from "../interface.js"; import * as mime from "mime-types"; // Type helper for headers type HeadersType = Record<string, string | number | undefined>; /** * Alibaba Cloud OSS Provider Implementation */ class AliyunOSSProvider implements Provider { readonly id: ProviderType = "oss"; readonly name = "Alibaba Cloud OSS"; private client: OSS | null = null; private sourceId: string = ""; private config: SourceConfig | null = null; getId(): string { return this.sourceId; } clone(): Provider { return new AliyunOSSProvider(); } async connect(config: SourceConfig): Promise<void> { this.config = config; this.sourceId = config.id; // Initialize OSS client this.client = new OSS({ accessKeyId: config.access_key, accessKeySecret: config.secret_key, endpoint: config.endpoint, region: config.region, bucket: config.default_bucket, timeout: config.connection_timeout ? config.connection_timeout * 1000 : 60000, secure: config.ssl !== false, stsToken: config.security_token, }); // Test connection by listing buckets try { await this.client.listBuckets({}); } catch (error) { this.client = null; throw new Error(`Failed to connect to OSS: ${(error as Error).message}`); } } async disconnect(): Promise<void> { this.client = null; } private ensureClient(): OSS { if (!this.client) { throw new Error("OSS client not connected. Call connect() first."); } return this.client; } /** * Create a client for a specific bucket */ private getClientForBucket(bucket: string): OSS { const client = this.ensureClient(); if (this.config?.default_bucket === bucket) { return client; } // Clone client with different bucket return new OSS({ accessKeyId: this.config!.access_key, accessKeySecret: this.config!.secret_key, endpoint: this.config!.endpoint, region: this.config!.region, bucket: bucket, timeout: this.config!.connection_timeout ? this.config!.connection_timeout * 1000 : 60000, secure: this.config!.ssl !== false, stsToken: this.config!.security_token, }); } async listBuckets(): Promise<BucketInfo[]> { const client = this.ensureClient(); const result = await client.listBuckets({}); // The result is an array of buckets directly or has a buckets property const buckets = Array.isArray(result) ? result : (result as any).buckets || []; return buckets.map((bucket: any) => ({ name: bucket.name, creationDate: bucket.creationDate, location: bucket.region || bucket.location, })); } async listObjects(bucket: string, options?: ListObjectsOptions): Promise<ListObjectsResult> { const client = this.getClientForBucket(bucket); const params: OSS.ListObjectsQuery = { "max-keys": String(options?.maxKeys || 1000), }; if (options?.prefix) { params.prefix = options.prefix; } if (options?.delimiter) { params.delimiter = options.delimiter; } if (options?.continuationToken) { params.marker = options.continuationToken; } const result = await client.list(params, {}); const objects: ObjectInfo[] = (result.objects || []).map((obj: any) => ({ key: obj.name, size: obj.size, lastModified: obj.lastModified, etag: obj.etag?.replace(/"/g, ""), storageClass: obj.storageClass, owner: obj.owner ? { id: obj.owner.id, displayName: obj.owner.displayName, } : undefined, })); const commonPrefixes = result.prefixes || []; const resultAny = result as any; return { objects, prefix: resultAny.prefix, delimiter: resultAny.delimiter, isTruncated: result.isTruncated || false, nextContinuationToken: result.nextMarker, commonPrefixes: commonPrefixes.length > 0 ? commonPrefixes : undefined, keyCount: objects.length, maxKeys: parseInt(String(params["max-keys"]) || "1000", 10), }; } async getObject(bucket: string, key: string, options?: GetObjectOptions): Promise<ObjectContent> { const client = this.getClientForBucket(bucket); const getOptions: any = {}; // Handle range request if (options?.rangeStart !== undefined || options?.rangeEnd !== undefined) { const start = options.rangeStart ?? 0; const end = options.rangeEnd ?? ""; getOptions.headers = { Range: `bytes=${start}-${end}`, }; } const result = await client.get(key, undefined as any, getOptions); const headers = result.res.headers as HeadersType; const contentType = String(headers["content-type"] || mime.lookup(key) || "application/octet-stream"); const contentLength = parseInt(String(headers["content-length"] || "0"), 10); const body = result.content; // Determine if content is text-based const isText = this.isTextContent(contentType); let content: string | undefined; let contentBase64: string | undefined; let truncated = false; if (body) { const maxSize = options?.maxSize || 10 * 1024 * 1024; // Default 10MB const buffer = Buffer.isBuffer(body) ? body : Buffer.from(body); if (buffer.length > maxSize) { truncated = true; const truncatedBuffer = buffer.subarray(0, maxSize); if (isText) { content = truncatedBuffer.toString("utf-8"); } else { contentBase64 = truncatedBuffer.toString("base64"); } } else { if (isText) { content = buffer.toString("utf-8"); } else { contentBase64 = buffer.toString("base64"); } } } const etag = headers["etag"]; return { key, content, contentBase64, contentType, contentLength, truncated, lastModified: headers["last-modified"] ? String(headers["last-modified"]) : undefined, etag: etag ? String(etag).replace(/"/g, "") : undefined, metadata: this.extractMetadata(headers), }; } async getObjectMetadata(bucket: string, key: string): Promise<ObjectMetadata> { const client = this.getClientForBucket(bucket); const result = await client.head(key); const headers = result.res.headers as HeadersType; const etag = headers["etag"]; return { key, contentType: String(headers["content-type"] || "application/octet-stream"), contentLength: parseInt(String(headers["content-length"] || "0"), 10), lastModified: headers["last-modified"] ? String(headers["last-modified"]) : undefined, etag: etag ? String(etag).replace(/"/g, "") : undefined, storageClass: headers["x-oss-storage-class"] ? String(headers["x-oss-storage-class"]) : undefined, cacheControl: headers["cache-control"] ? String(headers["cache-control"]) : undefined, contentDisposition: headers["content-disposition"] ? String(headers["content-disposition"]) : undefined, contentEncoding: headers["content-encoding"] ? String(headers["content-encoding"]) : undefined, metadata: this.extractMetadata(headers), }; } async bucketExists(bucket: string): Promise<boolean> { const client = this.ensureClient(); try { await client.getBucketInfo(bucket); return true; } catch { return false; } } async objectExists(bucket: string, key: string): Promise<boolean> { const client = this.getClientForBucket(bucket); try { await client.head(key); return true; } catch { return false; } } async searchObjects(bucket: string, filter: SearchFilter): Promise<SearchResult> { const client = this.getClientForBucket(bucket); const matchingObjects: ObjectInfo[] = []; let continuationToken: string | undefined; let hasMore = false; const maxResults = filter.maxResults || 100; // List all objects and filter do { const params: OSS.ListObjectsQuery = { "max-keys": "1000", }; if (filter.prefix) { params.prefix = filter.prefix; } if (continuationToken) { params.marker = continuationToken; } const result = await client.list(params, {}); const objects = result.objects || []; for (const obj of objects) { if (matchingObjects.length >= maxResults) { hasMore = true; break; } const objInfo: ObjectInfo = { key: obj.name, size: obj.size, lastModified: obj.lastModified, etag: obj.etag?.replace(/"/g, ""), storageClass: obj.storageClass, }; if (this.matchesFilter(objInfo, filter)) { matchingObjects.push(objInfo); } } if (!result.isTruncated || matchingObjects.length >= maxResults) { break; } continuationToken = result.nextMarker || objects[objects.length - 1]?.name; } while (continuationToken); return { objects: matchingObjects, totalCount: matchingObjects.length, hasMore, filter, }; } getSampleEndpoint(): string { return "https://oss-cn-hangzhou.aliyuncs.com"; } isValidEndpoint(endpoint: string): boolean { try { const url = new URL(endpoint); return url.hostname.includes("oss") || url.hostname.includes("aliyuncs"); } catch { return false; } } /** * Extract custom metadata from headers */ private extractMetadata(headers: HeadersType): Record<string, string> { const metadata: Record<string, string> = {}; for (const [key, value] of Object.entries(headers)) { if (key.startsWith("x-oss-meta-") && value !== undefined) { metadata[key.replace("x-oss-meta-", "")] = String(value); } } return metadata; } /** * Check if content type is text-based */ private isTextContent(contentType: string): boolean { const textTypes = [ "text/", "application/json", "application/xml", "application/javascript", "application/typescript", "application/x-yaml", "application/yaml", "application/toml", "application/x-sh", "application/x-python", ]; return textTypes.some((t) => contentType.startsWith(t) || contentType.includes(t)); } /** * Check if an object matches the search filter */ private matchesFilter(obj: ObjectInfo, filter: SearchFilter): boolean { // Extension filter if (filter.extensions && filter.extensions.length > 0) { const ext = "." + obj.key.split(".").pop()?.toLowerCase(); if (!filter.extensions.some((e) => e.toLowerCase() === ext)) { return false; } } // Suffix filter if (filter.suffix && !obj.key.endsWith(filter.suffix)) { return false; } // Size filters if (filter.minSize !== undefined && obj.size < filter.minSize) { return false; } if (filter.maxSize !== undefined && obj.size > filter.maxSize) { return false; } // Date filters if (filter.modifiedAfter && obj.lastModified) { if (new Date(obj.lastModified) < new Date(filter.modifiedAfter)) { return false; } } if (filter.modifiedBefore && obj.lastModified) { if (new Date(obj.lastModified) > new Date(filter.modifiedBefore)) { return false; } } // Pattern filter (simple glob matching) if (filter.pattern) { const regex = this.globToRegex(filter.pattern); if (!regex.test(obj.key)) { return false; } } return true; } /** * Convert glob pattern to regex */ private globToRegex(glob: string): RegExp { const escaped = glob .replace(/[.+^${}()|[\]\\]/g, "\\$&") .replace(/\*/g, ".*") .replace(/\?/g, "."); return new RegExp(`^${escaped}$`, "i"); } } // Register the provider const ossProvider = new AliyunOSSProvider(); ProviderRegistry.register(ossProvider); export { AliyunOSSProvider };

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/zq940222/OssHub'

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