UNPKG

@teenth/sdk-tool

Version:

sdk-tool with R2 storage support

336 lines (335 loc) 12.2 kB
"use strict"; /* * Cloudflare R2 Storage SDK * * 使用示例: * * // 配置 CDN 域名 (推荐) * const config = { * accountId: "your-account-id", * accessKeyId: "your-access-key", * secretAccessKey: "your-secret-key", * bucket: "your-bucket", * cdnDomain: "https://cdn.example.com" // 自定义域名 * }; * * // 或使用 R2 的公共 URL (需要在 Cloudflare 控制台配置) * const config = { * accountId: "your-account-id", * accessKeyId: "your-access-key", * secretAccessKey: "your-secret-key", * bucket: "your-bucket", * cdnDomain: "https://pub-xxxxx.r2.dev" // R2 公共域名 * }; * * const r2Storage = createR2Storage(config); * * // 上传文件,会返回完整的 CDN 地址 * const result = await r2Storage.upload("image.jpg", file); * console.log(result.url); // https://cdn.example.com/image.jpg * * // 将第三方链接迁移到你的 CDN * const migrateResult = await r2Storage.migrateUrl("https://example.com/image.jpg"); * console.log(migrateResult.url); // https://cdn.example.com/file_1640995200000_abc123.jpg * * // 自定义文件名迁移 * const customResult = await r2Storage.migrateUrl("https://example.com/image.jpg", { * fileName: "my-custom-image.jpg" * }); * console.log(customResult.url); // https://cdn.example.com/my-custom-image.jpg */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createR2Storage = void 0; const client_s3_1 = require("@aws-sdk/client-s3"); const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); // 文件类型转换工具函数 async function convertToBuffer(data) { if (Buffer.isBuffer(data)) { return data; } if (typeof data === "string") { // 如果是字符串,假设它是文件路径 if (typeof fs !== "undefined" && fs.existsSync && fs.existsSync(data)) { return fs.readFileSync(data); } // 否则当作文本内容 return Buffer.from(data, "utf-8"); } if (data instanceof ArrayBuffer) { return Buffer.from(data); } if (data instanceof Uint8Array) { return Buffer.from(data); } if (data && typeof data === "object" && "arrayBuffer" in data) { // 浏览器环境下的 File 对象 const arrayBuffer = await data.arrayBuffer(); return Buffer.from(arrayBuffer); } throw new Error("Unsupported file type"); } // 自动检测文件类型 function detectContentType(fileName, data) { // 如果是 File 对象,直接使用其 type if (data && typeof data === "object" && "type" in data && data.type) { return data.type; } // 根据文件扩展名推断 const ext = path.extname(fileName).toLowerCase(); const mimeTypes = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml", ".pdf": "application/pdf", ".txt": "text/plain", ".md": "text/markdown", ".html": "text/html", ".css": "text/css", ".js": "application/javascript", ".json": "application/json", ".xml": "application/xml", ".zip": "application/zip", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".wav": "audio/wav", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".xls": "application/vnd.ms-excel", ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }; return mimeTypes[ext] || "application/octet-stream"; } // 从 Content-Type 获取文件扩展名 function getExtensionFromContentType(contentType) { const typeToExtension = { "image/jpeg": ".jpg", "image/jpg": ".jpg", "image/png": ".png", "image/gif": ".gif", "image/webp": ".webp", "image/svg+xml": ".svg", "application/pdf": ".pdf", "text/plain": ".txt", "text/markdown": ".md", "text/html": ".html", "text/css": ".css", "application/javascript": ".js", "application/json": ".json", "application/xml": ".xml", "application/zip": ".zip", "video/mp4": ".mp4", "audio/mpeg": ".mp3", "audio/wav": ".wav", }; // 去掉参数部分,如 "image/jpeg; charset=utf-8" -> "image/jpeg" const cleanContentType = contentType.split(";")[0].trim(); return typeToExtension[cleanContentType] || ""; } // 生成随机文件名 function generateRandomFileName() { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `file_${timestamp}_${random}`; } // 兼容 Node.js 和浏览器环境的 fetch async function universalFetch(url) { // 使用全局 fetch,如果不存在则抛出错误 if (typeof fetch === "undefined") { throw new Error("Fetch is not available. Please install node-fetch or use Node.js 18+"); } return fetch(url); } function createR2Storage(config) { const client = new client_s3_1.S3Client({ region: config.region || "auto", endpoint: config.endpoint || `https://${config.accountId}.r2.cloudflarestorage.com`, credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey, }, }); const get = async (fileName) => { try { const file = await client.send(new client_s3_1.GetObjectCommand({ Bucket: config.bucket, Key: fileName, })); if (!file.Body) { throw new Error("File not found"); } return file.Body; } catch (error) { console.error("[R2Storage] Get file error:", error); throw error; } }; const upload = async (fileName, data, options = {}) => { try { // 自动转换为 Buffer const buffer = await convertToBuffer(data); // 自动检测内容类型 const contentType = options.contentType || detectContentType(fileName, data); const command = new client_s3_1.PutObjectCommand({ Bucket: config.bucket, Key: fileName, Body: buffer, ContentType: contentType, }); await client.send(command); // 生成完整的 CDN 地址 let cdnUrl; if (config.cdnDomain) { // 使用自定义 CDN 域名 cdnUrl = `${config.cdnDomain.replace(/\/$/, "")}/${fileName}`; } else { // 如果没有配置 CDN 域名,使用 R2 的默认公共 URL // 注意:需要先在 Cloudflare 控制台中为 bucket 配置自定义域名或公共访问 console.warn("[R2Storage] No CDN domain configured, using bucket endpoint"); cdnUrl = `${config.endpoint || `https://${config.accountId}.r2.cloudflarestorage.com`}/${config.bucket}/${fileName}`; } return { url: cdnUrl, fileName, size: buffer.length, }; } catch (error) { console.error("[R2Storage] Upload error:", error); throw error; } }; const deleteFile = async (fileName) => { try { await client.send(new client_s3_1.DeleteObjectCommand({ Bucket: config.bucket, Key: fileName, })); } catch (error) { console.error("[R2Storage] Delete file error:", error); throw error; } }; const list = async (prefix, maxKeys = 1000) => { try { const command = new client_s3_1.ListObjectsV2Command({ Bucket: config.bucket, Prefix: prefix, MaxKeys: maxKeys, }); const response = await client.send(command); return response.Contents || []; } catch (error) { console.error("[R2Storage] List files error:", error); throw error; } }; const getSignedUrl = async (fileName, expiresIn = 3600) => { try { const command = new client_s3_1.GetObjectCommand({ Bucket: config.bucket, Key: fileName, }); return await (0, s3_request_presigner_1.getSignedUrl)(client, command, { expiresIn }); } catch (error) { console.error("[R2Storage] Get signed URL error:", error); throw error; } }; const exists = async (fileName) => { try { await client.send(new client_s3_1.GetObjectCommand({ Bucket: config.bucket, Key: fileName, })); return true; } catch (error) { return false; } }; const migrateUrl = async (url, options = {}) => { try { // 从 URL 下载文件 const response = await universalFetch(url); if (!response.ok) { throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`); } // 获取文件内容 const arrayBuffer = await response.arrayBuffer(); // 自动生成文件名或使用提供的文件名 let fileName = options.fileName; if (!fileName) { // 从 URL 提取文件名 const urlPath = new URL(url).pathname; const extractedName = path.basename(urlPath); // 如果 URL 没有文件扩展名,则从 Content-Type 推断 if (!path.extname(extractedName)) { const contentType = response.headers.get("content-type") || "application/octet-stream"; const extension = getExtensionFromContentType(contentType); fileName = `${extractedName || generateRandomFileName()}${extension}`; } else { fileName = extractedName || generateRandomFileName(); } } // 使用检测到的或提供的内容类型 const contentType = options.contentType || response.headers.get("content-type") || detectContentType(fileName, arrayBuffer); // 上传到 R2 const result = await upload(fileName, arrayBuffer, { contentType, expiresIn: options.expiresIn, }); return result; } catch (error) { console.error("[R2Storage] Migrate URL error:", error); throw error; } }; return { get, upload, delete: deleteFile, list, getSignedUrl, exists, migrateUrl, }; } exports.createR2Storage = createR2Storage;