HarmonyOS开发:云存储Cloud Storage

举报
Jack20 发表于 2026/06/25 20:58:32 2026/06/25
【摘要】 HarmonyOS开发:云存储Cloud Storage📌 核心要点:Cloud Storage是端云一体的对象存储服务,文件上传下载、断点续传、安全策略、CDN加速,图片视频音频文件一站搞定。 背景与动机你的App要上传用户头像。简单嘛,写个HTTP POST把图片发到服务器——然后呢?图片太大上传超时怎么办?上传到一半网络断了怎么办?不同用户只能访问自己的文件怎么办?图片加载太慢怎么...

HarmonyOS开发:云存储Cloud Storage

📌 核心要点:Cloud Storage是端云一体的对象存储服务,文件上传下载、断点续传、安全策略、CDN加速,图片视频音频文件一站搞定。

背景与动机

你的App要上传用户头像。简单嘛,写个HTTP POST把图片发到服务器——然后呢?

图片太大上传超时怎么办?上传到一半网络断了怎么办?不同用户只能访问自己的文件怎么办?图片加载太慢怎么办?文件存储在哪个目录?怎么清理过期文件?

一个"上传头像"的需求,背后藏着十几个问题。你自己搭服务器、写接口、配CDN、做权限控制,一套下来至少两周。而且后期运维——磁盘满了、带宽超了、图片被盗链了——每个都是半夜被叫起来的理由。

Cloud Storage就是来解决这些问题的。你不用管服务器、不用管CDN、不用管权限,SDK一集成,上传下载断点续传全有了。

但Cloud Storage不是简单的"网盘SDK"。它有安全策略要配、有CDN要调优、有存储桶要规划。用对了,图片秒开、视频流畅;用错了,流量费比服务器还贵。

核心原理

Cloud Storage的架构很简单:客户端直连云端存储桶,不经过你的业务服务器,省带宽省成本。

flowchart TD
    A[客户端上传文件] --> B{文件大小}
    B -->|小于10MB| C[直接上传]
    B -->|大于10MB| D[分片上传]
    
    C --> E[Cloud Storage服务端]
    D --> E
    
    E --> F{上传结果}
    F -->|成功| G[返回文件URL]
    F -->|失败| H{支持断点续传?}
    H -->|| I[保存断点信息]
    I --> J[下次从断点继续]
    H -->|| K[重新上传]
    
    G --> L[文件存储到Bucket]
    L --> M[CDN节点缓存]
    
    N[客户端下载文件] --> O{CDN命中?}
    O -->|| P[CDN返回,速度快]
    O -->|| Q[回源到存储桶]
    Q --> R[同时缓存到CDN]
    R --> P
    
    classDef upload fill:#1565C0,color:#fff,stroke:#0D47A1
    classDef storage fill:#2E7D32,color:#fff,stroke:#1B5E20
    classDef download fill:#E65100,color:#fff,stroke:#BF360C
    classDef error fill:#C62828,color:#fff,stroke:#B71C1C

    class A,B,C,D,upload
    class E,F,G,L,M,storage
    class N,O,P,Q,R,download
    class H,I,J,K,error

存储桶与目录结构

Cloud Storage用"桶(Bucket)"来组织文件,每个项目可以创建多个桶。桶内的文件用路径(如images/avatars/user123.jpg)组织,类似文件系统。

推荐的目录结构:

/
├── images/           # 图片
│   ├── avatars/      # 用户头像
│   ├── articles/     # 文章配图
│   └── products/     # 商品图片
├── videos/           # 视频
├── audios/           # 音频
├── documents/        # 文档
└── temp/             # 临时文件(定期清理)

安全策略

Cloud Storage的安全策略基于"存储安全规则",定义谁可以读写哪些文件。规则示例:

  • 用户只能读写自己目录下的文件:/images/avatars/{userId}/
  • 公开文件所有人可读,只有上传者可写
  • 临时文件24小时后自动删除

安全规则在AGC控制台配置,客户端SDK自动执行,不需要你在业务层做权限校验。

CDN加速

Cloud Storage默认集成CDN。文件首次下载时从存储桶回源,之后缓存在CDN边缘节点,后续访问直接从CDN返回,延迟从几百毫秒降到几十毫秒。

CDN缓存策略可以按文件类型设置:图片缓存7天、视频缓存1天、临时文件不缓存。

代码实战

基础用法:文件上传与下载

最核心的操作:上传一个文件到云端,然后下载回来。

// CloudStorageManager.ets
import { cloudStorage } from '@kit.CloudStorageKit';
import { common } from '@kit.AbilityKit';

export class CloudStorageManager {
  private static instance: CloudStorageManager;
  private storage: cloudStorage.StorageReference | null = null;

  private constructor() {}

  static getInstance(): CloudStorageManager {
    if (!CloudStorageManager.instance) {
      CloudStorageManager.instance = new CloudStorageManager();
    }
    return CloudStorageManager.instance;
  }

  /**
   * 初始化Cloud Storage
   */
  init(): void {
    try {
      // 获取Storage实例
      const storageInstance = cloudStorage.getInstance();
      // 获取根目录引用
      this.storage = storageInstance.getStorageReference();
      console.info('[CloudStorage] 初始化完成');
    } catch (error) {
      console.error(`[CloudStorage] 初始化失败: ${JSON.stringify(error)}`);
    }
  }

  /**
   * 上传文件
   * 从本地路径上传到云端指定路径
   */
  async uploadFile(
    localPath: string,       // 本地文件路径
    cloudPath: string,       // 云端存储路径
    onProgress?: (percentage: number) => void  // 上传进度回调
  ): Promise<string | null> {
    if (!this.storage) {
      console.error('[CloudStorage] 未初始化');
      return null;
    }

    try {
      // 获取云端文件引用
      const fileRef = this.storage.child(cloudPath);

      // 创建上传任务
      const uploadTask = fileRef.putFile(localPath);

      // 监听上传进度
      uploadTask.on('progress', (snapshot: cloudStorage.UploadSnapshot) => {
        const percentage = Math.round(
          (snapshot.bytesTransferred / snapshot.totalBytes) * 100
        );
        console.info(`[CloudStorage] 上传进度: ${percentage}%`);
        onProgress?.(percentage);
      });

      // 监听上传完成
      uploadTask.on('success', (snapshot: cloudStorage.UploadSnapshot) => {
        console.info(`[CloudStorage] 上传完成: ${cloudPath}`);
      });

      // 监听上传失败
      uploadTask.on('fail', (snapshot: cloudStorage.UploadSnapshot) => {
        console.error(`[CloudStorage] 上传失败: ${cloudPath}`);
      });

      // 等待上传完成
      await uploadTask;

      // 获取下载URL
      const downloadUrl = await fileRef.getDownloadURL();
      console.info(`[CloudStorage] 下载URL: ${downloadUrl}`);
      return downloadUrl;
    } catch (error) {
      console.error(`[CloudStorage] 上传异常: ${JSON.stringify(error)}`);
      return null;
    }
  }

  /**
   * 下载文件
   * 从云端下载到本地指定路径
   */
  async downloadFile(
    cloudPath: string,       // 云端文件路径
    localPath: string,       // 本地保存路径
    onProgress?: (percentage: number) => void
  ): Promise<boolean> {
    if (!this.storage) return false;

    try {
      const fileRef = this.storage.child(cloudPath);

      // 创建下载任务
      const downloadTask = fileRef.getFile(localPath);

      // 监听下载进度
      downloadTask.on('progress', (snapshot: cloudStorage.DownloadSnapshot) => {
        const percentage = Math.round(
          (snapshot.bytesTransferred / snapshot.totalBytes) * 100
        );
        console.info(`[CloudStorage] 下载进度: ${percentage}%`);
        onProgress?.(percentage);
      });

      downloadTask.on('success', () => {
        console.info(`[CloudStorage] 下载完成: ${cloudPath}`);
      });

      downloadTask.on('fail', () => {
        console.error(`[CloudStorage] 下载失败: ${cloudPath}`);
      });

      await downloadTask;
      return true;
    } catch (error) {
      console.error(`[CloudStorage] 下载异常: ${JSON.stringify(error)}`);
      return false;
    }
  }

  /**
   * 删除云端文件
   */
  async deleteFile(cloudPath: string): Promise<boolean> {
    if (!this.storage) return false;

    try {
      const fileRef = this.storage.child(cloudPath);
      await fileRef.delete();
      console.info(`[CloudStorage] 删除成功: ${cloudPath}`);
      return true;
    } catch (error) {
      console.error(`[CloudStorage] 删除失败: ${JSON.stringify(error)}`);
      return false;
    }
  }

  /**
   * 获取文件下载URL
   * 不下载文件,只获取可访问的URL
   */
  async getDownloadUrl(cloudPath: string): Promise<string | null> {
    if (!this.storage) return null;

    try {
      const fileRef = this.storage.child(cloudPath);
      return await fileRef.getDownloadURL();
    } catch (error) {
      console.error(`[CloudStorage] 获取URL失败: ${JSON.stringify(error)}`);
      return null;
    }
  }

  /**
   * 获取文件元数据
   */
  async getMetadata(cloudPath: string): Promise<cloudStorage.FileMetadata | null> {
    if (!this.storage) return null;

    try {
      const fileRef = this.storage.child(cloudPath);
      return await fileRef.getMetadata();
    } catch (error) {
      console.error(`[CloudStorage] 获取元数据失败: ${JSON.stringify(error)}`);
      return null;
    }
  }
}

进阶用法:断点续传与批量操作

大文件上传必须支持断点续传。网络断了、App被杀了、用户切到后台了——下次打开时从断点继续,不用从头来。

// ResumableUpload.ets
import { cloudStorage } from '@kit.CloudStorageKit';

// 上传任务状态
interface UploadTaskInfo {
  cloudPath: string;           // 云端路径
  localPath: string;           // 本地路径
  sessionId: string;           // 上传会话ID,用于断点续传
  bytesTransferred: number;    // 已传输字节数
  totalBytes: number;          // 总字节数
  status: 'pending' | 'uploading' | 'paused' | 'completed' | 'failed';
}

export class ResumableUpload {
  private storage: cloudStorage.StorageReference;
  private activeTasks: Map<string, cloudStorage.UploadTask> = new Map();

  constructor(storage: cloudStorage.StorageReference) {
    this.storage = storage;
  }

  /**
   * 上传文件(支持断点续传)
   */
  async uploadWithResume(
    localPath: string,
    cloudPath: string,
    onProgress?: (info: UploadTaskInfo) => void
  ): Promise<string | null> {
    try {
      const fileRef = this.storage.child(cloudPath);

      // 检查是否有未完成的上传任务
      const savedSession = await this.loadUploadSession(cloudPath);

      let uploadTask: cloudStorage.UploadTask;

      if (savedSession) {
        // 从断点继续上传
        console.info(`[ResumableUpload] 从断点继续: ${cloudPath}, 已上传${savedSession.bytesTransferred}字节`);
        uploadTask = fileRef.putFile(localPath, {
          // 传入上次的会话信息,SDK自动从断点继续
          offset: savedSession.bytesTransferred
        });
      } else {
        // 全新上传
        uploadTask = fileRef.putFile(localPath);
      }

      // 保存任务引用
      this.activeTasks.set(cloudPath, uploadTask);

      // 任务信息
      const taskInfo: UploadTaskInfo = {
        cloudPath: cloudPath,
        localPath: localPath,
        sessionId: `upload_${Date.now()}`,
        bytesTransferred: 0,
        totalBytes: 0,
        status: 'uploading'
      };

      // 监听进度
      uploadTask.on('progress', (snapshot: cloudStorage.UploadSnapshot) => {
        taskInfo.bytesTransferred = snapshot.bytesTransferred;
        taskInfo.totalBytes = snapshot.totalBytes;
        taskInfo.status = 'uploading';

        // 保存断点信息
        this.saveUploadSession(cloudPath, taskInfo);

        onProgress?.(taskInfo);
      });

      uploadTask.on('success', async () => {
        taskInfo.status = 'completed';
        onProgress?.(taskInfo);

        // 上传完成,清除断点信息
        this.clearUploadSession(cloudPath);
        this.activeTasks.delete(cloudPath);
      });

      uploadTask.on('fail', () => {
        taskInfo.status = 'failed';
        onProgress?.(taskInfo);
        this.activeTasks.delete(cloudPath);
      });

      // 等待完成
      await uploadTask;

      // 返回下载URL
      return await fileRef.getDownloadURL();
    } catch (error) {
      console.error(`[ResumableUpload] 上传失败: ${JSON.stringify(error)}`);
      return null;
    }
  }

  /**
   * 暂停上传
   */
  pauseUpload(cloudPath: string): boolean {
    const task = this.activeTasks.get(cloudPath);
    if (task) {
      task.cancel();
      this.activeTasks.delete(cloudPath);
      console.info(`[ResumableUpload] 已暂停: ${cloudPath}`);
      return true;
    }
    return false;
  }

  /**
   * 批量上传文件
   * 控制并发数,避免同时上传太多文件
   */
  async batchUpload(
    files: Array<{ localPath: string; cloudPath: string }>,
    maxConcurrent: number = 3,
    onProgress?: (completed: number, total: number) => void
  ): Promise<Map<string, string | null>> {
    const results = new Map<string, string | null>();
    let completedCount = 0;

    // 分批执行,控制并发
    for (let i = 0; i < files.length; i += maxConcurrent) {
      const batch = files.slice(i, i + maxConcurrent);
      const promises = batch.map(async (file) => {
        const url = await this.uploadWithResume(file.localPath, file.cloudPath);
        results.set(file.cloudPath, url);
        completedCount++;
        onProgress?.(completedCount, files.length);
      });

      await Promise.all(promises);
    }

    return results;
  }

  // 断点信息持久化(使用Preferences存储)
  private async saveUploadSession(cloudPath: string, info: UploadTaskInfo): Promise<void> {
    try {
      // 实际项目中用Preferences或本地数据库存储
      console.info(`[ResumableUpload] 保存断点: ${cloudPath}, ${info.bytesTransferred}/${info.totalBytes}`);
    } catch (error) {
      console.warn(`[ResumableUpload] 保存断点失败: ${error}`);
    }
  }

  private async loadUploadSession(cloudPath: string): Promise<UploadTaskInfo | null> {
    // 从本地存储读取断点信息
    return null; // 简化实现
  }

  private async clearUploadSession(cloudPath: string): Promise<void> {
    // 清除断点信息
  }
}

完整示例:带CDN优化和安全策略的图片上传系统

把上传、压缩、CDN缓存、安全校验串成完整链路。

// ImageUploadService.ets
import { cloudStorage } from '@kit.CloudStorageKit';
import { image } from '@kit.ImageKit';

// 图片上传配置
interface ImageUploadConfig {
  maxWidth: number;          // 最大宽度,超过则压缩
  maxHeight: number;         // 最大高度,超过则压缩
  quality: number;           // 压缩质量 0-100
  maxFileSize: number;       // 最大文件大小(字节)
  allowedTypes: string[];    // 允许的图片类型
}

const DEFAULT_IMAGE_CONFIG: ImageUploadConfig = {
  maxWidth: 1920,
  maxHeight: 1080,
  quality: 80,
  maxFileSize: 5 * 1024 * 1024,  // 5MB
  allowedTypes: ['image/jpeg', 'image/png', 'image/webp']
};

export class ImageUploadService {
  private storage: cloudStorage.StorageReference;

  constructor(storage: cloudStorage.StorageReference) {
    this.storage = storage;
  }

  /**
   * 上传用户头像
   * 自动压缩 + 校验 + 上传 + 返回CDN URL
   */
  async uploadAvatar(
    userId: string,
    localPath: string,
    config: Partial<ImageUploadConfig> = {}
  ): Promise<{ url: string | null; error?: string }> {
    const finalConfig = { ...DEFAULT_IMAGE_CONFIG, ...config };

    // 1. 校验文件类型
    const typeCheck = this.validateImageType(localPath, finalConfig.allowedTypes);
    if (!typeCheck.valid) {
      return { url: null, error: typeCheck.message };
    }

    // 2. 校验文件大小
    const sizeCheck = await this.validateFileSize(localPath, finalConfig.maxFileSize);
    if (!sizeCheck.valid) {
      return { url: null, error: sizeCheck.message };
    }

    // 3. 压缩图片(如果需要)
    const compressedPath = await this.compressImage(localPath, finalConfig);

    // 4. 生成云端路径
    const cloudPath = `images/avatars/${userId}/${this.generateFileName()}.jpg`;

    // 5. 上传到Cloud Storage
    try {
      const fileRef = this.storage.child(cloudPath);
      const uploadTask = fileRef.putFile(compressedPath || localPath);
      await uploadTask;

      // 6. 获取CDN加速的下载URL
      const downloadUrl = await fileRef.getDownloadURL();

      // 7. 更新用户头像URL(写Cloud DB)
      // await userRepository.updateAvatar(userId, downloadUrl);

      console.info(`[ImageUpload] 头像上传成功: ${downloadUrl}`);
      return { url: downloadUrl };
    } catch (error) {
      console.error(`[ImageUpload] 上传失败: ${JSON.stringify(error)}`);
      return { url: null, error: '上传失败,请重试' };
    }
  }

  /**
   * 批量上传文章配图
   */
  async uploadArticleImages(
    articleId: string,
    imagePaths: string[],
    onProgress?: (completed: number, total: number) => void
  ): Promise<string[]> {
    const urls: string[] = [];
    let completed = 0;

    for (const localPath of imagePaths) {
      try {
        const compressedPath = await this.compressImage(localPath, DEFAULT_IMAGE_CONFIG);
        const cloudPath = `images/articles/${articleId}/${this.generateFileName()}.jpg`;

        const fileRef = this.storage.child(cloudPath);
        await fileRef.putFile(compressedPath || localPath);
        const url = await fileRef.getDownloadURL();
        urls.push(url);
      } catch (error) {
        console.error(`[ImageUpload] 图片上传失败: ${localPath}`);
        urls.push(''); // 上传失败的用空字符串占位
      }

      completed++;
      onProgress?.(completed, imagePaths.length);
    }

    return urls;
  }

  /**
   * 校验图片类型
   */
  private validateImageType(
    filePath: string,
    allowedTypes: string[]
  ): { valid: boolean; message: string } {
    const extension = filePath.split('.').pop()?.toLowerCase() || '';
    const typeMap: Record<string, string> = {
      'jpg': 'image/jpeg',
      'jpeg': 'image/jpeg',
      'png': 'image/png',
      'webp': 'image/webp',
      'gif': 'image/gif'
    };

    const mimeType = typeMap[extension];
    if (!mimeType || !allowedTypes.includes(mimeType)) {
      return {
        valid: false,
        message: `不支持的图片格式: ${extension},仅支持 ${allowedTypes.join(', ')}`
      };
    }

    return { valid: true, message: '' };
  }

  /**
   * 校验文件大小
   */
  private async validateFileSize(
    filePath: string,
    maxSize: number
  ): Promise<{ valid: boolean; message: string }> {
    try {
      // 获取文件大小(简化实现,实际用fs.statSync)
      const fileSize = 0; // 实际读取文件大小
      if (fileSize > maxSize) {
        return {
          valid: false,
          message: `文件大小超过限制(最大${Math.round(maxSize / 1024 / 1024)}MB)`
        };
      }
      return { valid: true, message: '' };
    } catch (error) {
      return { valid: false, message: '无法读取文件信息' };
    }
  }

  /**
   * 压缩图片
   */
  private async compressImage(
    filePath: string,
    config: ImageUploadConfig
  ): Promise<string | null> {
    try {
      // 使用HarmonyOS图片处理API压缩
      const imageSource = image.createImageSource(filePath);
      const imageInfo = await imageSource.getImageInfo();

      // 判断是否需要压缩
      const needResize = imageInfo.size.width > config.maxWidth || 
                         imageInfo.size.height > config.maxHeight;

      if (!needResize) {
        return null; // 不需要压缩
      }

      // 计算压缩后的尺寸
      let targetWidth = imageInfo.size.width;
      let targetHeight = imageInfo.size.height;

      if (targetWidth > config.maxWidth) {
        const ratio = config.maxWidth / targetWidth;
        targetWidth = config.maxWidth;
        targetHeight = Math.round(targetHeight * ratio);
      }

      if (targetHeight > config.maxHeight) {
        const ratio = config.maxHeight / targetHeight;
        targetHeight = config.maxHeight;
        targetWidth = Math.round(targetWidth * ratio);
      }

      // 创建压缩后的图片
      const packingOption: image.PackingOption = {
        format: 'image/jpeg',
        quality: config.quality
      };

      const imagePackerApi = image.createImagePacker();
      // 实际压缩逻辑...
      
      console.info(`[ImageUpload] 图片压缩: ${imageInfo.size.width}x${imageInfo.size.height}${targetWidth}x${targetHeight}`);
      
      // 返回压缩后的文件路径
      return filePath; // 简化实现
    } catch (error) {
      console.warn(`[ImageUpload] 图片压缩失败,使用原图: ${error}`);
      return null;
    }
  }

  /**
   * 生成唯一文件名
   */
  private generateFileName(): string {
    return `${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
  }
}

踩坑与注意事项

坑1:文件路径必须用正斜杠

Cloud Storage的路径分隔符是/,不是Windows的\。你在Windows上开发时路径可能是images\avatars\user.jpg,上传到Cloud Storage会变成一个文件名叫images\avatars\user.jpg的文件,而不是images/avatars/目录下的user.jpg

必须用/拼接路径,或者用path.posix.join()

坑2:上传的文件默认是私有的

Cloud Storage的文件默认只有上传者可以访问。如果你想让图片公开可访问(比如头像、文章配图),需要在AGC控制台配置安全规则,或者上传时设置文件为公开读。

// 设置文件为公开读
const metadata: cloudStorage.FileMetadata = {
  cacheControl: 'public, max-age=604800', // CDN缓存7天
  contentDisposition: 'inline'
};
await fileRef.updateMetadata(metadata);

坑3:CDN缓存导致更新不及时

你更新了一个文件,但CDN还在返回旧版本。这是因为CDN有缓存,默认缓存时间可能很长。

解决方案

  • 在文件名中加入版本号或哈希值,如avatar_v2.jpg
  • 设置较短的Cache-Control头
  • 手动刷新CDN缓存(AGC控制台操作)

坑4:大文件上传超时

超过50MB的文件,直接上传容易超时。必须用分片上传,每片5-10MB,并行上传。Cloud Storage SDK内部会自动处理分片,但你需要确保网络稳定。

坑5:下载URL的有效期

getDownloadURL()返回的URL不是永久有效的,默认有效期1小时。如果你把URL存到数据库,1小时后访问就404了。

解决方案:每次需要展示图片时重新获取URL,或者使用带签名的长期URL。

坑6:存储费用容易被忽略

Cloud Storage按存储量和流量计费。如果你存了大量临时文件没清理,或者图片被恶意刷流量,账单可能让你大吃一惊。

建议

  • 临时文件设置TTL,过期自动删除
  • 配置防盗链,防止外部网站盗用你的图片
  • 监控流量异常,设置告警

HarmonyOS 6适配说明

HarmonyOS 6对Cloud Storage Kit做了以下更新:

  1. 服务端加密(SSE):新增服务端加密选项,文件上传时自动加密存储。支持AGC托管的密钥和用户自定义密钥(KMS)。之前文件在服务端是明文存储的,HarmonyOS 6默认开启SSE。

  2. 图片处理能力:Cloud Storage新增了图片处理参数,下载时可以实时裁剪、缩放、格式转换,不需要在客户端处理。

// HarmonyOS 6图片处理
// 下载时自动缩放到200x200,转为webp格式
const processedUrl = await fileRef.getDownloadURL({
  imageSize: '200x200',
  format: 'webp',
  quality: 80
});
  1. 分片上传优化:分片大小从固定的5MB改为自适应,根据网络状况动态调整。弱网环境下用更小的分片,强网环境下用更大的分片,上传速度提升约30%。

  2. 跨区域复制:新增跨区域复制能力,文件上传后自动复制到其他区域的存储桶。适合全球化应用,用户访问最近的区域获取文件。

  3. 生命周期管理:新增文件生命周期规则,可以设置"30天后自动删除"、"60天后自动归档到低频存储"等规则。之前需要手动清理临时文件,现在自动完成。

总结

Cloud Storage看起来就是"上传下载文件",但真要在生产环境用好,你得把安全策略、CDN缓存、断点续传、费用控制全都考虑进去。

核心记住三点:

  • 文件路径统一用正斜杠,Windows开发时最容易踩这个坑
  • CDN缓存是双刃剑,加载快但更新慢,文件名加版本号是最简单的解决方案
  • 存储费用要监控,临时文件不清理、防盗链不配置,账单分分钟教你做人
评估维度 说明
学习难度 ⭐⭐⭐ 基础上传下载简单,断点续传和CDN优化需要经验
使用频率 ⭐⭐⭐⭐⭐ 几乎所有应用都有文件存储需求
重要程度 ⭐⭐⭐⭐⭐ 没有Cloud Storage,文件管理就是噩梦

Cloud Storage不是可选项。你的App要存文件,不用它就得自己搭一套——你确定你有那个精力?

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。