Skip to content

版本控制

S3 版本控制功能可以保留、检索和恢复对象的每个版本,防止意外删除或覆盖。

🔖 什么是版本控制

版本控制为 Bucket 中的每个对象保留多个变体。当启用版本控制后:

  • 每次上传同名对象会创建新版本
  • 删除对象会创建删除标记,而不是真正删除
  • 可以恢复到任意历史版本

🎯 版本控制状态

Bucket 有三种版本控制状态:

  • 未版本控制(默认) - 不保留版本
  • 启用版本控制 - 保留所有版本
  • 暂停版本控制 - 停止创建新版本,保留现有版本

🚀 启用版本控制

Node.js

javascript
const AWS = require('aws-sdk');
const s3 = new AWS.S3();

// 启用版本控制
const enableVersioning = async (bucketName) => {
  const params = {
    Bucket: bucketName,
    VersioningConfiguration: {
      Status: 'Enabled'
    }
  };
  
  try {
    await s3.putBucketVersioning(params).promise();
    console.log('Versioning enabled');
  } catch (err) {
    console.error(err);
  }
};

// 暂停版本控制
const suspendVersioning = async (bucketName) => {
  const params = {
    Bucket: bucketName,
    VersioningConfiguration: {
      Status: 'Suspended'
    }
  };
  
  await s3.putBucketVersioning(params).promise();
  console.log('Versioning suspended');
};

// 获取版本控制状态
const getVersioningStatus = async (bucketName) => {
  const result = await s3.getBucketVersioning({
    Bucket: bucketName
  }).promise();
  
  console.log('Versioning status:', result.Status || 'Unversioned');
  console.log('MFA Delete:', result.MFADelete || 'Disabled');
  
  return result;
};

AWS CLI

bash
# 启用版本控制
aws s3api put-bucket-versioning \
  --bucket my-bucket \
  --versioning-configuration Status=Enabled

# 查看状态
aws s3api get-bucket-versioning --bucket my-bucket

📋 列出对象版本

列出所有版本

javascript
const listObjectVersions = async (bucketName, prefix = '') => {
  const params = {
    Bucket: bucketName,
    Prefix: prefix
  };
  
  try {
    const result = await s3.listObjectVersions(params).promise();
    
    console.log('Versions:');
    result.Versions?.forEach(version => {
      console.log(`  ${version.Key}`);
      console.log(`    Version ID: ${version.VersionId}`);
      console.log(`    Last Modified: ${version.LastModified}`);
      console.log(`    Size: ${version.Size} bytes`);
      console.log(`    Is Latest: ${version.IsLatest}`);
      console.log('---');
    });
    
    console.log('Delete Markers:');
    result.DeleteMarkers?.forEach(marker => {
      console.log(`  ${marker.Key}`);
      console.log(`    Version ID: ${marker.VersionId}`);
      console.log(`    Last Modified: ${marker.LastModified}`);
      console.log('---');
    });
    
    return result;
  } catch (err) {
    console.error(err);
  }
};

列出特定对象的所有版本

javascript
const listVersionsOfObject = async (bucketName, key) => {
  const params = {
    Bucket: bucketName,
    Prefix: key
  };
  
  const result = await s3.listObjectVersions(params).promise();
  
  // 过滤出精确匹配的对象
  const versions = result.Versions?.filter(v => v.Key === key) || [];
  
  console.log(`Versions of ${key}:`);
  versions.forEach((v, index) => {
    console.log(`${index + 1}. Version ${v.VersionId}`);
    console.log(`   Date: ${v.LastModified}`);
    console.log(`   Size: ${v.Size} bytes`);
    console.log(`   Latest: ${v.IsLatest}`);
  });
  
  return versions;
};

⬇️ 下载特定版本

javascript
// 下载最新版本(不指定版本ID)
const getLatestVersion = async (bucketName, key) => {
  const result = await s3.getObject({
    Bucket: bucketName,
    Key: key
  }).promise();
  
  console.log('Version ID:', result.VersionId);
  return result.Body;
};

// 下载特定版本
const getSpecificVersion = async (bucketName, key, versionId) => {
  const result = await s3.getObject({
    Bucket: bucketName,
    Key: key,
    VersionId: versionId
  }).promise();
  
  return result.Body;
};

// 下载所有版本
const downloadAllVersions = async (bucketName, key) => {
  const versions = await listVersionsOfObject(bucketName, key);
  
  for (const version of versions) {
    const content = await getSpecificVersion(bucketName, key, version.VersionId);
    const filename = `${key}.${version.VersionId}`;
    
    // 保存到本地
    require('fs').writeFileSync(filename, content);
    console.log(`Downloaded: ${filename}`);
  }
};

🗑️ 删除操作

软删除(创建删除标记)

javascript
// 不指定版本ID的删除会创建删除标记
const softDelete = async (bucketName, key) => {
  const result = await s3.deleteObject({
    Bucket: bucketName,
    Key: key
    // 不指定 VersionId
  }).promise();
  
  console.log('Delete Marker ID:', result.VersionId);
  // 对象看起来被删除了,但所有版本仍然存在
};

硬删除(永久删除特定版本)

javascript
// 指定版本ID的删除会永久删除该版本
const hardDelete = async (bucketName, key, versionId) => {
  await s3.deleteObject({
    Bucket: bucketName,
    Key: key,
    VersionId: versionId
  }).promise();
  
  console.log(`Permanently deleted version ${versionId}`);
};

删除所有版本

javascript
const deleteAllVersions = async (bucketName, key) => {
  // 1. 列出所有版本
  const params = {
    Bucket: bucketName,
    Prefix: key
  };
  
  const result = await s3.listObjectVersions(params).promise();
  
  // 2. 删除所有版本和删除标记
  const objectsToDelete = [
    ...(result.Versions || [])
      .filter(v => v.Key === key)
      .map(v => ({
        Key: v.Key,
        VersionId: v.VersionId
      })),
    ...(result.DeleteMarkers || [])
      .filter(d => d.Key === key)
      .map(d => ({
        Key: d.Key,
        VersionId: d.VersionId
      }))
  ];
  
  if (objectsToDelete.length === 0) {
    console.log('No versions found');
    return;
  }
  
  // 3. 批量删除
  await s3.deleteObjects({
    Bucket: bucketName,
    Delete: {
      Objects: objectsToDelete
    }
  }).promise();
  
  console.log(`Deleted ${objectsToDelete.length} versions`);
};

♻️ 恢复已删除的对象

javascript
// 删除删除标记以恢复对象
const restoreDeletedObject = async (bucketName, key) => {
  // 1. 找到删除标记
  const result = await s3.listObjectVersions({
    Bucket: bucketName,
    Prefix: key
  }).promise();
  
  const deleteMarker = result.DeleteMarkers?.find(
    d => d.Key === key && d.IsLatest
  );
  
  if (!deleteMarker) {
    console.log('Object is not deleted');
    return;
  }
  
  // 2. 删除删除标记
  await s3.deleteObject({
    Bucket: bucketName,
    Key: key,
    VersionId: deleteMarker.VersionId
  }).promise();
  
  console.log('Object restored');
};

🔄 恢复到特定版本

javascript
// 通过复制旧版本来恢复
const restoreToVersion = async (bucketName, key, versionId) => {
  // 复制旧版本作为新的当前版本
  await s3.copyObject({
    Bucket: bucketName,
    CopySource: `${bucketName}/${key}?versionId=${versionId}`,
    Key: key
  }).promise();
  
  console.log(`Restored to version ${versionId}`);
};

🔒 MFA 删除

MFA (多因素认证) 删除提供额外的安全层,防止意外删除。

javascript
// 启用 MFA 删除(需要通过 AWS CLI 或控制台)
// 只有 Bucket 所有者的根账户可以启用

// AWS CLI 示例
// aws s3api put-bucket-versioning \
//   --bucket my-bucket \
//   --versioning-configuration Status=Enabled,MFADelete=Enabled \
//   --mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device 123456"

// 启用后,删除版本需要 MFA 码
const deleteWithMFA = async (bucketName, key, versionId, mfaCode) => {
  await s3.deleteObject({
    Bucket: bucketName,
    Key: key,
    VersionId: versionId,
    MFA: `arn:aws:iam::123456789012:mfa/device ${mfaCode}`
  }).promise();
};

💰 成本管理

查看版本占用的存储

javascript
const calculateVersionStorage = async (bucketName) => {
  const result = await s3.listObjectVersions({
    Bucket: bucketName
  }).promise();
  
  let totalSize = 0;
  let versionCount = 0;
  
  result.Versions?.forEach(v => {
    totalSize += v.Size;
    versionCount++;
  });
  
  console.log(`Total versions: ${versionCount}`);
  console.log(`Total size: ${(totalSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
  
  return { versionCount, totalSize };
};

使用生命周期管理旧版本

javascript
const setVersionLifecycle = async (bucketName) => {
  const params = {
    Bucket: bucketName,
    LifecycleConfiguration: {
      Rules: [
        {
          Id: 'Delete old versions',
          Status: 'Enabled',
          NoncurrentVersionExpiration: {
            NoncurrentDays: 30  // 非当前版本保留30天
          },
          AbortIncompleteMultipartUpload: {
            DaysAfterInitiation: 7
          }
        }
      ]
    }
  };
  
  await s3.putBucketLifecycleConfiguration(params).promise();
  console.log('Version lifecycle configured');
};

💡 最佳实践

1. 选择性启用版本控制

javascript
// ✅ 适合启用版本控制的场景
- 重要文档
- 配置文件
- 数据库备份
- 需要审计跟踪的数据

// ❌ 不适合版本控制的场景
- 日志文件(太多版本)
- 临时文件
- 大型媒体文件(成本高)

2. 设置版本过期策略

javascript
// 自动清理旧版本以控制成本
const smartVersionLifecycle = {
  Rules: [
    {
      Id: 'Manage versions',
      Status: 'Enabled',
      NoncurrentVersionTransitions: [
        {
          NoncurrentDays: 30,
          StorageClass: 'STANDARD_IA'  // 旧版本转低频
        },
        {
          NoncurrentDays: 90,
          StorageClass: 'GLACIER'  // 更旧的版本归档
        }
      ],
      NoncurrentVersionExpiration: {
        NoncurrentDays: 365  // 1年后删除旧版本
      }
    }
  ]
};

3. 监控版本数量

javascript
// 定期检查并清理不需要的版本
const cleanupOldVersions = async (bucketName, keepVersions = 5) => {
  const result = await s3.listObjectVersions({
    Bucket: bucketName
  }).promise();
  
  // 按对象分组
  const objectVersions = {};
  result.Versions?.forEach(v => {
    if (!objectVersions[v.Key]) {
      objectVersions[v.Key] = [];
    }
    objectVersions[v.Key].push(v);
  });
  
  // 删除超出保留数量的版本
  for (const [key, versions] of Object.entries(objectVersions)) {
    if (versions.length > keepVersions) {
      // 按日期排序,保留最新的
      versions.sort((a, b) => 
        new Date(b.LastModified) - new Date(a.LastModified)
      );
      
      const toDelete = versions.slice(keepVersions);
      
      for (const version of toDelete) {
        await s3.deleteObject({
          Bucket: bucketName,
          Key: key,
          VersionId: version.VersionId
        }).promise();
        
        console.log(`Deleted old version of ${key}`);
      }
    }
  }
};

4. 版本对比

javascript
// 对比两个版本的差异
const compareVersions = async (bucketName, key, version1, version2) => {
  const [content1, content2] = await Promise.all([
    getSpecificVersion(bucketName, key, version1),
    getSpecificVersion(bucketName, key, version2)
  ]);
  
  const str1 = content1.toString('utf-8');
  const str2 = content2.toString('utf-8');
  
  if (str1 === str2) {
    console.log('Versions are identical');
  } else {
    console.log('Versions differ');
    // 使用 diff 库进行详细对比
  }
};

⚠️ 注意事项

  1. 版本控制无法关闭 - 只能暂停,已有版本会保留
  2. 存储成本 - 每个版本都占用存储空间并计费
  3. 删除标记 - 软删除会创建删除标记,也占用空间
  4. 列举性能 - 大量版本会影响 listObjectVersions 性能
  5. 最终一致性 - 高并发下版本列表可能不是最新的

📖 相关资源


恭喜!你已经完成 AWS S3 的所有核心知识学习!🎉