Skip to main content
Glama
index.ts11.8 kB
/** * Huawei Cloud OBS (Object Storage Service) Provider * * This provider implements the Provider interface for Huawei Cloud OBS. * SDK Repository: https://github.com/huaweicloud/huaweicloud-sdk-nodejs-obs * Documentation: https://support.huaweicloud.com/obs/index.html */ import ObsClient from "esdk-obs-nodejs"; 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"; /** * Huawei Cloud OBS Provider Implementation */ class HuaweiOBSProvider implements Provider { readonly id: ProviderType = "obs"; readonly name = "Huawei Cloud OBS"; private client: ObsClient | null = null; private sourceId: string = ""; private config: SourceConfig | null = null; getId(): string { return this.sourceId; } clone(): Provider { return new HuaweiOBSProvider(); } async connect(config: SourceConfig): Promise<void> { this.config = config; this.sourceId = config.id; // Initialize OBS client this.client = new ObsClient({ access_key_id: config.access_key, secret_access_key: config.secret_key, server: config.endpoint, timeout: config.connection_timeout ? config.connection_timeout * 1000 : 60000, ssl_verify: config.ssl !== false, security_token: config.security_token, }); // Test connection by listing buckets try { const result = await this.client.listBuckets(); if (result.CommonMsg.Status >= 300) { throw new Error(`Connection failed: ${result.CommonMsg.Message}`); } } catch (error) { this.client = null; throw new Error(`Failed to connect to OBS: ${(error as Error).message}`); } } async disconnect(): Promise<void> { if (this.client) { this.client.close(); this.client = null; } } private ensureClient(): ObsClient { if (!this.client) { throw new Error("OBS client not connected. Call connect() first."); } return this.client; } async listBuckets(): Promise<BucketInfo[]> { const client = this.ensureClient(); const result = await client.listBuckets(); if (result.CommonMsg.Status >= 300) { throw new Error(`Failed to list buckets: ${result.CommonMsg.Message}`); } return (result.InterfaceResult?.Buckets || []).map((bucket: any) => ({ name: bucket.BucketName, creationDate: bucket.CreationDate, location: bucket.Location, })); } async listObjects(bucket: string, options?: ListObjectsOptions): Promise<ListObjectsResult> { const client = this.ensureClient(); const params: any = { Bucket: bucket, MaxKeys: 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.listObjects(params); if (result.CommonMsg.Status >= 300) { throw new Error(`Failed to list objects: ${result.CommonMsg.Message}`); } const ir = result.InterfaceResult; const objects: ObjectInfo[] = (ir?.Contents || []).map((obj: any) => ({ key: obj.Key, 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 = (ir?.CommonPrefixes || []).map((p: any) => p.Prefix); return { objects, prefix: ir?.Prefix, delimiter: ir?.Delimiter, isTruncated: ir?.IsTruncated === "true" || ir?.IsTruncated === true, nextContinuationToken: ir?.NextMarker, commonPrefixes: commonPrefixes.length > 0 ? commonPrefixes : undefined, keyCount: objects.length, maxKeys: ir?.MaxKeys || params.MaxKeys, }; } async getObject(bucket: string, key: string, options?: GetObjectOptions): Promise<ObjectContent> { const client = this.ensureClient(); const params: any = { Bucket: bucket, Key: key, }; // Handle range request if (options?.rangeStart !== undefined || options?.rangeEnd !== undefined) { const start = options.rangeStart ?? 0; const end = options.rangeEnd ?? ""; params.Range = `bytes=${start}-${end}`; } const result = await client.getObject(params); if (result.CommonMsg.Status >= 300) { throw new Error(`Failed to get object: ${result.CommonMsg.Message}`); } const ir = result.InterfaceResult; const contentType = ir?.ContentType || mime.lookup(key) || "application/octet-stream"; const contentLength = parseInt(ir?.ContentLength || "0", 10); const body = ir?.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"); } } } return { key, content, contentBase64, contentType, contentLength, truncated, lastModified: ir?.LastModified, etag: ir?.ETag?.replace(/"/g, ""), metadata: ir?.Metadata, }; } async getObjectMetadata(bucket: string, key: string): Promise<ObjectMetadata> { const client = this.ensureClient(); const result = await client.getObjectMetadata({ Bucket: bucket, Key: key, }); if (result.CommonMsg.Status >= 300) { throw new Error(`Failed to get object metadata: ${result.CommonMsg.Message}`); } const ir = result.InterfaceResult; return { key, contentType: ir?.ContentType || "application/octet-stream", contentLength: parseInt(ir?.ContentLength || "0", 10), lastModified: ir?.LastModified, etag: ir?.ETag?.replace(/"/g, ""), storageClass: ir?.StorageClass, metadata: ir?.Metadata, }; } async bucketExists(bucket: string): Promise<boolean> { const client = this.ensureClient(); try { const result = await client.headBucket({ Bucket: bucket }); return result.CommonMsg.Status < 300; } catch { return false; } } async objectExists(bucket: string, key: string): Promise<boolean> { const client = this.ensureClient(); try { const result = await client.getObjectMetadata({ Bucket: bucket, Key: key, }); return result.CommonMsg.Status < 300; } catch { return false; } } async searchObjects(bucket: string, filter: SearchFilter): Promise<SearchResult> { const client = this.ensureClient(); const matchingObjects: ObjectInfo[] = []; let continuationToken: string | undefined; let hasMore = false; const maxResults = filter.maxResults || 100; // List all objects and filter do { const params: any = { Bucket: bucket, MaxKeys: 1000, }; if (filter.prefix) { params.Prefix = filter.prefix; } if (continuationToken) { params.Marker = continuationToken; } const result = await client.listObjects(params); if (result.CommonMsg.Status >= 300) { throw new Error(`Failed to search objects: ${result.CommonMsg.Message}`); } const ir = result.InterfaceResult; const objects = ir?.Contents || []; for (const obj of objects) { if (matchingObjects.length >= maxResults) { hasMore = true; break; } const objInfo: ObjectInfo = { key: obj.Key, size: obj.Size, lastModified: obj.LastModified, etag: obj.ETag?.replace(/"/g, ""), storageClass: obj.StorageClass, }; if (this.matchesFilter(objInfo, filter)) { matchingObjects.push(objInfo); } } const isTruncated = ir?.IsTruncated === "true" || ir?.IsTruncated === true; if (!isTruncated || matchingObjects.length >= maxResults) { break; } continuationToken = ir?.NextMarker || objects[objects.length - 1]?.Key; } while (continuationToken); return { objects: matchingObjects, totalCount: matchingObjects.length, hasMore, filter, }; } getSampleEndpoint(): string { return "https://obs.cn-east-2.myhuaweicloud.com"; } isValidEndpoint(endpoint: string): boolean { try { const url = new URL(endpoint); return url.hostname.includes("obs") || url.hostname.includes("myhuaweicloud"); } catch { return false; } } /** * 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 obsProvider = new HuaweiOBSProvider(); ProviderRegistry.register(obsProvider); export { HuaweiOBSProvider };

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