跳到主要内容

云开发云存储

概述

云开发云存储为您提供高可用、高稳定、强安全的云端存储服务,支持任意数量和形式的非结构化数据存储,如视频和图片等。在云函数中,您可以通过 Node.js SDK 或 HTTP API 两种方式访问云存储,实现文件的上传、下载、删除和管理等操作。

云存储的特点

  • 安全可靠:提供多重数据备份,确保数据安全
  • 弹性扩展:支持海量文件存储,按需付费
  • 高性能:全球 CDN 加速,快速访问
  • 权限控制:灵活的访问权限管理
  • 多格式支持:支持图片、视频、文档等各种文件格式

Node.js SDK 访问

初始化配置

在云函数中使用云存储前,需要先初始化 SDK:

const tcb = require('@cloudbase/node-sdk');

// 初始化云开发
const app = tcb.init({
env: 'your-env-id',
secretId: 'your-secret-id',
secretKey: 'your-secret-key'
});

// 获取云存储实例
const storage = app.storage();

文件上传

exports.main = async (event, context) => {
try {
// 从事件中获取文件数据(Base64 编码)
const { fileContent, fileName, contentType } = event;

// 将 Base64 转换为 Buffer
const buffer = Buffer.from(fileContent, 'base64');

// 上传文件
const result = await storage.uploadFile({
cloudPath: `uploads/${Date.now()}_${fileName}`, // 云端路径
fileContent: buffer, // 文件内容
contentType: contentType || 'application/octet-stream' // 文件类型
});

return {
success: true,
fileID: result.fileID,
downloadURL: result.download_url,
message: '文件上传成功'
};

} catch (error) {
console.error('文件上传失败:', error);
return {
success: false,
error: error.message
};
}
};

文件下载

exports.main = async (event, context) => {
try {
const { fileID } = event;

// 获取文件下载链接
const result = await storage.getFileDownloadURL({
fileList: [fileID]
});

if (result.fileList && result.fileList.length > 0) {
const fileInfo = result.fileList[0];

return {
success: true,
downloadURL: fileInfo.download_url,
tempFileURL: fileInfo.tempFileURL,
maxAge: fileInfo.maxAge
};
} else {
throw new Error('文件不存在');
}

} catch (error) {
console.error('获取下载链接失败:', error);
return {
success: false,
error: error.message
};
}
};

文件删除

exports.main = async (event, context) => {
try {
const { fileID } = event;

// 删除文件
const result = await storage.deleteFile({
fileList: [fileID]
});

if (result.fileList && result.fileList.length > 0) {
const deleteResult = result.fileList[0];

if (deleteResult.code === 'SUCCESS') {
return {
success: true,
message: '文件删除成功'
};
} else {
throw new Error(deleteResult.message || '删除失败');
}
}

} catch (error) {
console.error('删除文件失败:', error);
return {
success: false,
error: error.message
};
}
};

HTTP API 访问

基础配置

使用 HTTP API 访问云存储需要先进行认证配置:

const axios = require('axios');

// HTTP API 基础配置
const config = {
baseURL: 'https://your-env-id.api.tcloudbasegateway.com',
accessToken: 'your-access-token' // 通过身份验证获取的访问令牌
};

// 发送 HTTP 请求的通用方法
async function callStorageAPI(endpoint, method = 'POST', data = null) {
try {
const response = await axios({
method: method,
url: `${config.baseURL}${endpoint}`,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.accessToken}`
},
data: data
});

return response.data;
} catch (error) {
console.error('HTTP API 调用失败:', error);
throw error;
}
}

获取对象上传信息

exports.main = async (event, context) => {
try {
const { objectIds } = event; // 对象 ID 数组

// 构建请求体
const requestBody = objectIds.map(objectId => ({
objectId: objectId
}));

// 调用获取对象上传信息 API
const result = await callStorageAPI('/v1/storages/get-objects-upload-info', 'POST', requestBody);

// 处理响应结果
const uploadInfos = result.map(item => {
if (item.code) {
// 失败的情况
return {
objectId: item.objectId,
success: false,
error: {
code: item.code,
message: item.message
}
};
} else {
// 成功的情况
return {
objectId: item.objectId,
success: true,
uploadUrl: item.uploadUrl,
downloadUrl: item.downloadUrl,
downloadUrlEncoded: item.downloadUrlEncoded,
token: item.token,
authorization: item.authorization,
cloudObjectMeta: item.cloudObjectMeta,
cloudObjectId: item.cloudObjectId
};
}
});

return {
success: true,
uploadInfos: uploadInfos
};

} catch (error) {
console.error('获取上传信息失败:', error);
return {
success: false,
error: error.message
};
}
};

文件下载 API

exports.main = async (event, context) => {
try {
const { objectId } = event;

// 获取上传信息(包含下载链接)
const result = await callStorageAPI('/v1/storages/get-objects-upload-info', 'POST', [
{ objectId: objectId }
]);

if (result.length === 0 || result[0].code) {
throw new Error(result[0]?.message || '获取文件信息失败');
}

const fileInfo = result[0];

return {
success: true,
objectId: objectId,
downloadUrl: fileInfo.downloadUrl,
downloadUrlEncoded: fileInfo.downloadUrlEncoded,
cloudObjectId: fileInfo.cloudObjectId
};

} catch (error) {
console.error('获取下载链接失败:', error);
return {
success: false,
error: error.message
};
}
};

文件删除 API

// 注意:官方文档中的获取上传信息 API 主要用于上传,删除操作需要使用其他 API
// 这里提供一个通用的删除方法示例

exports.main = async (event, context) => {
try {
const { fileIds } = event; // 云存储文件 ID 数组

// 调用删除文件 API(具体端点根据实际 API 文档调整)
const result = await callStorageAPI('/v1/storages/delete-files', 'POST', {
fileIds: fileIds
});

return {
success: true,
result: result,
message: '文件删除请求已发送'
};

} catch (error) {
console.error('删除文件失败:', error);
return {
success: false,
error: error.message
};
}
};

文件操作

图片处理

exports.main = async (event, context) => {
try {
const { fileID, width, height, quality } = event;

// 获取原始下载链接
const urlResult = await storage.getFileDownloadURL({
fileList: [fileID]
});

const originalURL = urlResult.fileList[0].download_url;

// 构建图片处理参数
const imageParams = [];
if (width) imageParams.push(`w_${width}`);
if (height) imageParams.push(`h_${height}`);
if (quality) imageParams.push(`q_${quality}`);

// 生成处理后的图片链接
const processedURL = `${originalURL}?imageView2/2/${imageParams.join('/')}`;

return {
success: true,
originalURL: originalURL,
processedURL: processedURL,
params: imageParams.join('/')
};

} catch (error) {
console.error('图片处理失败:', error);
return {
success: false,
error: error.message
};
}
};

文件信息查询

exports.main = async (event, context) => {
try {
const { fileID } = event;

// 获取文件信息
const urlResult = await storage.getFileDownloadURL({
fileList: [fileID]
});

if (urlResult.fileList && urlResult.fileList.length > 0) {
const fileInfo = urlResult.fileList[0];

// 获取文件详细信息
const response = await axios.head(fileInfo.download_url);

return {
success: true,
fileID: fileID,
downloadURL: fileInfo.download_url,
size: response.headers['content-length'],
contentType: response.headers['content-type'],
lastModified: response.headers['last-modified'],
etag: response.headers['etag']
};
} else {
throw new Error('文件不存在');
}

} catch (error) {
console.error('获取文件信息失败:', error);
return {
success: false,
error: error.message
};
}
};

文件列表查询

exports.main = async (event, context) => {
try {
const { prefix, limit, offset } = event;

// 注意:云存储 SDK 不直接支持文件列表查询
// 需要通过 COS SDK 或维护文件索引来实现

// 这里提供一个通过数据库维护文件索引的示例
const db = app.database();
const collection = db.collection('file_index');

let query = collection;

// 按前缀过滤
if (prefix) {
query = query.where({
cloudPath: db.RegExp({
regexp: `^${prefix}`,
options: 'i'
})
});
}

// 分页查询
const result = await query
.skip(offset || 0)
.limit(limit || 20)
.orderBy('uploadTime', 'desc')
.get();

return {
success: true,
files: result.data,
total: result.data.length,
hasMore: result.data.length === (limit || 20)
};

} catch (error) {
console.error('查询文件列表失败:', error);
return {
success: false,
error: error.message
};
}
};

高级功能

权限管理

exports.main = async (event, context) => {
try {
const { fileID } = event;

// 获取下载链接(永久有效)
const result = await storage.getFileDownloadURL({
fileList: [{
fileID: fileID,
maxAge: 7200 // 2小时有效期
}]
});

if (result.fileList && result.fileList.length > 0) {
const fileInfo = result.fileList[0];

return {
success: true,
fileID: fileID,
publicURL: fileInfo.download_url,
maxAge: fileInfo.maxAge
};
}

} catch (error) {
console.error('设置公开读权限失败:', error);
return {
success: false,
error: error.message
};
}
};

CDN 加速

exports.main = async (event, context) => {
try {
const { fileID, enableCDN } = event;

// 获取文件下载链接
const result = await storage.getFileDownloadURL({
fileList: [fileID]
});

let downloadURL = result.fileList[0].download_url;

if (enableCDN) {
// 替换为 CDN 域名(需要在云开发控制台配置)
downloadURL = downloadURL.replace(
/https:\/\/[^.]+\.tcb\.qcloud\.la/,
'https://your-cdn-domain.com'
);
}

return {
success: true,
originalURL: result.fileList[0].download_url,
cdnURL: downloadURL,
cdnEnabled: enableCDN
};

} catch (error) {
console.error('CDN 配置失败:', error);
return {
success: false,
error: error.message
};
}
};

文件同步

exports.main = async (event, context) => {
try {
const { sourceFileID, targetPath, targetEnv } = event;

// 下载源文件
const downloadResult = await storage.getFileDownloadURL({
fileList: [sourceFileID]
});

const downloadURL = downloadResult.fileList[0].download_url;

// 下载文件内容
const response = await axios({
method: 'GET',
url: downloadURL,
responseType: 'arraybuffer'
});

// 初始化目标环境
const targetApp = tcb.init({
env: targetEnv,
secretId: process.env.SECRET_ID,
secretKey: process.env.SECRET_KEY
});

const targetStorage = targetApp.storage();

// 上传到目标环境
const uploadResult = await targetStorage.uploadFile({
cloudPath: targetPath,
fileContent: Buffer.from(response.data)
});

return {
success: true,
sourceFileID: sourceFileID,
targetFileID: uploadResult.fileID,
targetURL: uploadResult.download_url
};

} catch (error) {
console.error('文件同步失败:', error);
return {
success: false,
error: error.message
};
}
};

定时清理

exports.main = async (event, context) => {
try {
const { daysToKeep = 30, pathPrefix = 'temp/' } = event;

// 获取需要清理的文件列表(从数据库索引中查询)
const db = app.database();
const collection = db.collection('file_index');

const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);

const result = await collection
.where({
cloudPath: db.RegExp({
regexp: `^${pathPrefix}`,
options: 'i'
}),
uploadTime: db.command.lt(cutoffDate)
})
.get();

const filesToDelete = result.data;

if (filesToDelete.length === 0) {
return {
success: true,
message: '没有需要清理的文件',
deletedCount: 0
};
}

// 批量删除文件
const fileIDs = filesToDelete.map(file => file.fileID);
const deleteResult = await storage.deleteFile({
fileList: fileIDs
});

// 从数据库中删除记录
const deletePromises = filesToDelete.map(file =>
collection.doc(file._id).remove()
);

await Promise.all(deletePromises);

return {
success: true,
deletedCount: fileIDs.length,
message: `成功清理 ${fileIDs.length} 个过期文件`
};

} catch (error) {
console.error('定时清理失败:', error);
return {
success: false,
error: error.message
};
}
};

最佳实践

文件命名规范

// 生成规范的文件路径
function generateFilePath(category, userId, originalName) {
const timestamp = Date.now();
const randomStr = Math.random().toString(36).substr(2, 8);
const ext = path.extname(originalName);
const baseName = path.basename(originalName, ext);

// 清理文件名中的特殊字符
const cleanName = baseName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_');

return `${category}/${userId}/${timestamp}_${randomStr}_${cleanName}${ext}`;
}

// 使用示例
const filePath = generateFilePath('avatars', 'user123', '用户头像.jpg');
// 输出: avatars/user123/1640995200000_abc12345_用户头像.jpg

文件类型验证

// 文件类型验证
function validateFileType(fileName, allowedTypes) {
const ext = path.extname(fileName).toLowerCase();
const mimeTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
};

return allowedTypes.includes(ext) && mimeTypes[ext];
}

// 文件大小验证
function validateFileSize(fileContent, maxSizeMB) {
const buffer = Buffer.from(fileContent, 'base64');
const sizeMB = buffer.length / (1024 * 1024);

return sizeMB <= maxSizeMB;
}

// 使用示例
exports.main = async (event, context) => {
const { fileName, fileContent } = event;

// 验证文件类型
const allowedTypes = ['.jpg', '.jpeg', '.png', '.gif'];
const contentType = validateFileType(fileName, allowedTypes);

if (!contentType) {
return {
success: false,
error: '不支持的文件类型'
};
}

// 验证文件大小(限制 5MB)
if (!validateFileSize(fileContent, 5)) {
return {
success: false,
error: '文件大小超过限制(5MB)'
};
}

// 继续上传逻辑...
};

错误处理和重试

// 带重试机制的文件操作
async function executeWithRetry(operation, maxRetries = 3) {
let lastError;

for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error;

// 判断是否为可重试的错误
if (isRetryableError(error) && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // 指数退避
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}

throw error;
}
}

throw lastError;
}

function isRetryableError(error) {
const retryableCodes = [
'ECONNRESET',
'ETIMEDOUT',
'ENOTFOUND',
'NETWORK_ERROR'
];

return retryableCodes.includes(error.code) ||
error.message.includes('timeout') ||
error.message.includes('network');
}

// 使用示例
exports.main = async (event, context) => {
try {
const result = await executeWithRetry(async () => {
return await storage.uploadFile({
cloudPath: event.path,
fileContent: Buffer.from(event.content, 'base64')
});
});

return { success: true, result };
} catch (error) {
console.error('文件操作最终失败:', error);
return { success: false, error: error.message };
}
};

性能优化

// 文件上传性能优化
class FileUploadOptimizer {
constructor() {
this.uploadQueue = [];
this.processing = false;
this.maxConcurrent = 3;
}

async addUpload(uploadTask) {
return new Promise((resolve, reject) => {
this.uploadQueue.push({ task: uploadTask, resolve, reject });
this.processQueue();
});
}

async processQueue() {
if (this.processing || this.uploadQueue.length === 0) {
return;
}

this.processing = true;

while (this.uploadQueue.length > 0) {
const batch = this.uploadQueue.splice(0, this.maxConcurrent);

const promises = batch.map(async ({ task, resolve, reject }) => {
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
}
});

await Promise.all(promises);
}

this.processing = false;
}
}

// 全局上传优化器
const uploadOptimizer = new FileUploadOptimizer();

exports.main = async (event, context) => {
const { files } = event; // 多个文件

try {
const uploadTasks = files.map(file => () =>
storage.uploadFile({
cloudPath: generateFilePath('uploads', context.requestId, file.name),
fileContent: Buffer.from(file.content, 'base64')
})
);

const results = await Promise.all(
uploadTasks.map(task => uploadOptimizer.addUpload(task))
);

return {
success: true,
files: results.map(result => ({
fileID: result.fileID,
downloadURL: result.download_url
}))
};

} catch (error) {
console.error('批量上传失败:', error);
return {
success: false,
error: error.message
};
}
};

相关文档

提示
  • 建议为不同类型的文件设置不同的存储路径
  • 使用 CDN 加速可以显著提升文件访问速度
  • 定期清理临时文件和过期文件以节省存储成本
  • 重要文件建议设置备份和版本管理
注意
  • 上传文件前务必进行类型和大小验证
  • 敏感文件应设置适当的访问权限
  • 避免在文件名中使用特殊字符
  • 生产环境中建议配置文件访问监控和告警