Skip to content

对象操作

📄 对象概述

对象(Object)是 S3 中存储的基本实体,由数据、元数据和唯一标识符(Key)组成。

🔑 对象组成

对象结构:
├── Key: 对象的唯一标识符(类似文件路径)
│   例: images/2024/01/photo.jpg
├── Value: 对象数据(0 字节到 5TB)
├── Metadata: 元数据
│   ├── 系统元数据
│   │   - Content-Type
│   │   - Content-Length
│   │   - Last-Modified
│   │   - ETag (对象的 MD5 哈希)
│   └── 用户自定义元数据
│       - x-amz-meta-*
├── Version ID: 版本标识(如果启用版本控制)
├── Access Control: 访问控制信息
└── Storage Class: 存储类别

⬆️ 上传对象

简单上传(< 5GB)

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

// 上传本地文件
const uploadFile = async (bucketName, key, filePath) => {
  const fileContent = fs.readFileSync(filePath);
  
  const params = {
    Bucket: bucketName,
    Key: key,
    Body: fileContent,
    ContentType: 'image/jpeg',
    Metadata: {
      'author': 'John Doe',
      'created-by': 'my-app'
    }
  };
  
  try {
    const result = await s3.putObject(params).promise();
    console.log('Upload successful');
    console.log('ETag:', result.ETag);
    return result;
  } catch (err) {
    console.error('Upload failed:', err);
  }
};

// 上传 Buffer
const uploadBuffer = async (bucketName, key, buffer) => {
  const params = {
    Bucket: bucketName,
    Key: key,
    Body: buffer,
    ContentType: 'application/octet-stream'
  };
  
  await s3.putObject(params).promise();
};

// 上传 Stream
const uploadStream = async (bucketName, key, stream) => {
  const params = {
    Bucket: bucketName,
    Key: key,
    Body: stream
  };
  
  await s3.upload(params).promise();
};

// 上传并设置 ACL
const uploadPublicFile = async (bucketName, key, filePath) => {
  const fileContent = fs.readFileSync(filePath);
  
  const params = {
    Bucket: bucketName,
    Key: key,
    Body: fileContent,
    ACL: 'public-read',  // 公开可读
    ContentType: 'image/jpeg'
  };
  
  await s3.putObject(params).promise();
};

使用 upload() 方法(推荐)

upload() 方法会自动处理分片上传和并发。

javascript
const uploadLargeFile = async (bucketName, key, filePath) => {
  const fileStream = fs.createReadStream(filePath);
  
  const params = {
    Bucket: bucketName,
    Key: key,
    Body: fileStream,
    ContentType: 'video/mp4'
  };
  
  const options = {
    partSize: 10 * 1024 * 1024,  // 10MB 每个分片
    queueSize: 4  // 并发上传 4 个分片
  };
  
  try {
    const upload = s3.upload(params, options);
    
    // 监听进度
    upload.on('httpUploadProgress', (progress) => {
      const percentage = Math.round((progress.loaded / progress.total) * 100);
      console.log(`Upload progress: ${percentage}%`);
    });
    
    const result = await upload.promise();
    console.log('Upload completed:', result.Location);
    return result;
  } catch (err) {
    console.error('Upload failed:', err);
  }
};

AWS CLI

bash
# 上传文件
aws s3 cp file.txt s3://my-bucket/file.txt

# 上传目录
aws s3 cp ./local-dir s3://my-bucket/remote-dir --recursive

# 设置元数据
aws s3 cp file.txt s3://my-bucket/file.txt \
  --content-type "text/plain" \
  --metadata author="John Doe"

⬇️ 下载对象

下载到内存

javascript
// 获取对象内容
const getObject = async (bucketName, key) => {
  const params = {
    Bucket: bucketName,
    Key: key
  };
  
  try {
    const result = await s3.getObject(params).promise();
    
    console.log('Content-Type:', result.ContentType);
    console.log('Content-Length:', result.ContentLength);
    console.log('Metadata:', result.Metadata);
    
    // result.Body 是一个 Buffer
    return result.Body;
  } catch (err) {
    if (err.code === 'NoSuchKey') {
      console.error('Object does not exist');
    } else {
      console.error(err);
    }
  }
};

// 下载文本内容
const getTextContent = async (bucketName, key) => {
  const result = await s3.getObject({
    Bucket: bucketName,
    Key: key
  }).promise();
  
  return result.Body.toString('utf-8');
};

// 下载 JSON 内容
const getJSONContent = async (bucketName, key) => {
  const result = await s3.getObject({
    Bucket: bucketName,
    Key: key
  }).promise();
  
  return JSON.parse(result.Body.toString('utf-8'));
};

下载到文件

javascript
const downloadFile = async (bucketName, key, localPath) => {
  const params = {
    Bucket: bucketName,
    Key: key
  };
  
  try {
    const result = await s3.getObject(params).promise();
    fs.writeFileSync(localPath, result.Body);
    console.log(`File downloaded to ${localPath}`);
  } catch (err) {
    console.error(err);
  }
};

// 使用 Stream 下载大文件
const downloadLargeFile = (bucketName, key, localPath) => {
  const params = {
    Bucket: bucketName,
    Key: key
  };
  
  const fileStream = fs.createWriteStream(localPath);
  const s3Stream = s3.getObject(params).createReadStream();
  
  s3Stream.pipe(fileStream);
  
  s3Stream.on('error', (err) => {
    console.error('Download error:', err);
  });
  
  fileStream.on('finish', () => {
    console.log('Download completed');
  });
};

部分下载(Range)

javascript
// 下载对象的一部分
const downloadRange = async (bucketName, key, start, end) => {
  const params = {
    Bucket: bucketName,
    Key: key,
    Range: `bytes=${start}-${end}`  // 例: bytes=0-1023
  };
  
  const result = await s3.getObject(params).promise();
  return result.Body;
};

// 实现断点续传
const downloadWithResume = async (bucketName, key, localPath) => {
  const stats = fs.existsSync(localPath) ? fs.statSync(localPath) : null;
  const startByte = stats ? stats.size : 0;
  
  // 获取对象总大小
  const head = await s3.headObject({ Bucket: bucketName, Key: key }).promise();
  const totalSize = head.ContentLength;
  
  if (startByte >= totalSize) {
    console.log('File already downloaded');
    return;
  }
  
  const params = {
    Bucket: bucketName,
    Key: key,
    Range: `bytes=${startByte}-${totalSize - 1}`
  };
  
  const result = await s3.getObject(params).promise();
  fs.appendFileSync(localPath, result.Body);
  console.log('Download resumed and completed');
};

AWS CLI

bash
# 下载文件
aws s3 cp s3://my-bucket/file.txt ./file.txt

# 下载目录
aws s3 cp s3://my-bucket/remote-dir ./local-dir --recursive

📋 列出对象

列出所有对象

javascript
// 使用 listObjectsV2(推荐)
const listObjects = async (bucketName, prefix = '') => {
  const params = {
    Bucket: bucketName,
    Prefix: prefix,
    MaxKeys: 1000
  };
  
  try {
    const result = await s3.listObjectsV2(params).promise();
    
    console.log(`Found ${result.Contents.length} objects`);
    result.Contents.forEach(obj => {
      console.log(`- ${obj.Key} (${obj.Size} bytes, ${obj.LastModified})`);
    });
    
    return result.Contents;
  } catch (err) {
    console.error(err);
  }
};

// 列出所有对象(包括分页)
const listAllObjects = async (bucketName, prefix = '') => {
  const allObjects = [];
  let continuationToken = undefined;
  
  do {
    const params = {
      Bucket: bucketName,
      Prefix: prefix,
      ContinuationToken: continuationToken,
      MaxKeys: 1000
    };
    
    const result = await s3.listObjectsV2(params).promise();
    allObjects.push(...result.Contents);
    
    continuationToken = result.NextContinuationToken;
  } while (continuationToken);
  
  console.log(`Total objects: ${allObjects.length}`);
  return allObjects;
};

// 按前缀分组列出(模拟目录结构)
const listByPrefix = async (bucketName, prefix = '', delimiter = '/') => {
  const params = {
    Bucket: bucketName,
    Prefix: prefix,
    Delimiter: delimiter
  };
  
  const result = await s3.listObjectsV2(params).promise();
  
  console.log('Directories:');
  result.CommonPrefixes?.forEach(p => {
    console.log(`  📁 ${p.Prefix}`);
  });
  
  console.log('Files:');
  result.Contents?.forEach(obj => {
    console.log(`  📄 ${obj.Key}`);
  });
  
  return {
    directories: result.CommonPrefixes || [],
    files: result.Contents || []
  };
};

AWS CLI

bash
# 列出对象
aws s3 ls s3://my-bucket/

# 递归列出
aws s3 ls s3://my-bucket/ --recursive

# 显示大小和总计
aws s3 ls s3://my-bucket/ --recursive --human-readable --summarize

📝 复制对象

同 Bucket 复制

javascript
const copyObject = async (bucketName, sourceKey, destKey) => {
  const params = {
    Bucket: bucketName,
    CopySource: `${bucketName}/${sourceKey}`,
    Key: destKey
  };
  
  try {
    await s3.copyObject(params).promise();
    console.log(`Copied from ${sourceKey} to ${destKey}`);
  } catch (err) {
    console.error(err);
  }
};

跨 Bucket 复制

javascript
const copyBetweenBuckets = async (sourceBucket, sourceKey, destBucket, destKey) => {
  const params = {
    Bucket: destBucket,
    CopySource: `${sourceBucket}/${sourceKey}`,
    Key: destKey
  };
  
  await s3.copyObject(params).promise();
};

复制并修改元数据

javascript
const copyWithMetadata = async (bucketName, sourceKey, destKey) => {
  const params = {
    Bucket: bucketName,
    CopySource: `${bucketName}/${sourceKey}`,
    Key: destKey,
    MetadataDirective: 'REPLACE',  // 替换元数据
    Metadata: {
      'author': 'Jane Doe',
      'version': '2.0'
    },
    ContentType: 'image/png'
  };
  
  await s3.copyObject(params).promise();
};

🗑️ 删除对象

删除单个对象

javascript
const deleteObject = async (bucketName, key) => {
  const params = {
    Bucket: bucketName,
    Key: key
  };
  
  try {
    await s3.deleteObject(params).promise();
    console.log(`Object ${key} deleted`);
  } catch (err) {
    console.error(err);
  }
};

批量删除

javascript
const deleteMultipleObjects = async (bucketName, keys) => {
  const params = {
    Bucket: bucketName,
    Delete: {
      Objects: keys.map(key => ({ Key: key })),
      Quiet: false  // false 返回删除结果
    }
  };
  
  try {
    const result = await s3.deleteObjects(params).promise();
    
    console.log('Deleted:');
    result.Deleted?.forEach(obj => {
      console.log(`  ✓ ${obj.Key}`);
    });
    
    if (result.Errors && result.Errors.length > 0) {
      console.log('Errors:');
      result.Errors.forEach(err => {
        console.log(`  ✗ ${err.Key}: ${err.Message}`);
      });
    }
    
    return result;
  } catch (err) {
    console.error(err);
  }
};

// 删除前缀下的所有对象
const deleteByPrefix = async (bucketName, prefix) => {
  const objects = await listAllObjects(bucketName, prefix);
  const keys = objects.map(obj => obj.Key);
  
  // 分批删除(每批最多 1000 个)
  const batchSize = 1000;
  for (let i = 0; i < keys.length; i += batchSize) {
    const batch = keys.slice(i, i + batchSize);
    await deleteMultipleObjects(bucketName, batch);
  }
  
  console.log(`Deleted ${keys.length} objects`);
};

🔍 检查对象

获取对象元数据(不下载内容)

javascript
const getObjectMetadata = async (bucketName, key) => {
  const params = {
    Bucket: bucketName,
    Key: key
  };
  
  try {
    const result = await s3.headObject(params).promise();
    
    console.log('Metadata:');
    console.log('  Content-Type:', result.ContentType);
    console.log('  Content-Length:', result.ContentLength);
    console.log('  ETag:', result.ETag);
    console.log('  Last-Modified:', result.LastModified);
    console.log('  Storage-Class:', result.StorageClass);
    console.log('  Metadata:', result.Metadata);
    
    return result;
  } catch (err) {
    if (err.code === 'NotFound') {
      console.log('Object does not exist');
      return null;
    }
    throw err;
  }
};

// 检查对象是否存在
const objectExists = async (bucketName, key) => {
  try {
    await s3.headObject({ Bucket: bucketName, Key: key }).promise();
    return true;
  } catch (err) {
    if (err.code === 'NotFound') {
      return false;
    }
    throw err;
  }
};

📊 对象元数据

设置自定义元数据

javascript
// 上传时设置
const uploadWithMetadata = async (bucketName, key, data) => {
  const params = {
    Bucket: bucketName,
    Key: key,
    Body: data,
    Metadata: {
      'user-id': '12345',
      'upload-source': 'mobile-app',
      'processed': 'false'
    }
  };
  
  await s3.putObject(params).promise();
};

// 更新现有对象的元数据(需要复制对象)
const updateMetadata = async (bucketName, key, newMetadata) => {
  const params = {
    Bucket: bucketName,
    CopySource: `${bucketName}/${key}`,
    Key: key,
    MetadataDirective: 'REPLACE',
    Metadata: newMetadata
  };
  
  await s3.copyObject(params).promise();
};

💡 最佳实践

1. Key 命名

javascript
// ✅ 好的 Key 命名
'users/12345/avatars/2024-01-15-abc123.jpg'
'invoices/2024/01/INV-001.pdf'
'logs/application/2024/01/15/app.log'

// ❌ 避免
'用户头像.jpg'  // 中文
'file (1).jpg'  // 空格和括号
'../../../etc/passwd'  // 路径穿越

// 性能优化:添加随机前缀避免热点分区
const hash = crypto.createHash('md5').update(userId).digest('hex').substring(0, 4);
const key = `${hash}/users/${userId}/avatar.jpg`;

2. 分片上传阈值

javascript
// 文件大小 > 100MB 使用分片上传
const uploadFile = async (bucketName, key, filePath) => {
  const stats = fs.statSync(filePath);
  const fileSizeInBytes = stats.size;
  const fileSizeInMB = fileSizeInBytes / (1024 * 1024);
  
  if (fileSizeInMB > 100) {
    // 使用分片上传
    return uploadLargeFile(bucketName, key, filePath);
  } else {
    // 普通上传
    const fileContent = fs.readFileSync(filePath);
    return s3.putObject({
      Bucket: bucketName,
      Key: key,
      Body: fileContent
    }).promise();
  }
};

3. 设置正确的 Content-Type

javascript
const mime = require('mime-types');

const uploadWithCorrectContentType = async (bucketName, key, filePath) => {
  const contentType = mime.lookup(filePath) || 'application/octet-stream';
  
  const params = {
    Bucket: bucketName,
    Key: key,
    Body: fs.readFileSync(filePath),
    ContentType: contentType
  };
  
  await s3.putObject(params).promise();
};

📖 相关资源


下一步:学习 SDK 使用 🛠️