@teenth/sdk-tool
Version:
sdk-tool with R2 storage support
336 lines (335 loc) • 12.2 kB
JavaScript
;
/*
* 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;