如何使用适用于 Node.js 的 AWS 开发工具包将 Amazon S3 中的所有对象从一个前缀复制/移动到另一个前缀

Posted

技术标签:

【中文标题】如何使用适用于 Node.js 的 AWS 开发工具包将 Amazon S3 中的所有对象从一个前缀复制/移动到另一个前缀【英文标题】:How to copy/move all objects in Amazon S3 from one prefix to other using the AWS SDK for Node.js 【发布时间】:2015-09-06 16:23:08 【问题描述】:

如何将所有对象从一个前缀复制到另一个?我已经尝试了所有可能的方法来将一个镜头中的所有对象从一个前缀复制到另一个,但似乎唯一可行的方法是遍历对象列表并一个一个地复制它们。这实在是太低效了。如果我在一个文件夹中有数百个文件,我需要拨打 100 次电话吗?

var params = 
         Bucket: bucket,
         CopySource: bucket+'/'+oldDirName+'/filename.txt',
         Key: newDirName+'/filename.txt',
 ;
s3.copyObject(params, function(err, data) 
  if (err) 
      callback.apply(this, [
          type: "error",
          message: "Error while renaming Directory",
          data: err
      ]);
   else 
      callback.apply(this, [
          type: "success",
          message: "Directory renamed successfully",
          data: data
      ]);
  
);

【问题讨论】:

【参考方案1】:

您需要创建一个AWS.S3.listObjects() 以列出您的对象并带有特定前缀。但是您是正确的,因为您需要为要从一个存储桶/前缀复制到相同或另一个存储桶/前缀的每个对象进行一次调用。

您还可以使用像 async 这样的实用程序库来管理您的请求。

var AWS = require('aws-sdk');
var async = require('async');
var bucketName = 'foo';
var oldPrefix = 'abc/';
var newPrefix = 'xyz/';
var s3 = new AWS.S3(params: Bucket: bucketName, region: 'us-west-2');

var done = function(err, data) 
  if (err) console.log(err);
  else console.log(data);
;

s3.listObjects(Prefix: oldPrefix, function(err, data) 
  if (data.Contents.length) 
    async.each(data.Contents, function(file, cb) 
      var params = 
        Bucket: bucketName,
        CopySource: bucketName + '/' + file.Key,
        Key: file.Key.replace(oldPrefix, newPrefix)
      ;
      s3.copyObject(params, function(copyErr, copyData)
        if (copyErr) 
          console.log(copyErr);
        
        else 
          console.log('Copied: ', params.Key);
          cb();
        
      );
    , done);
  
);

希望这会有所帮助!

【讨论】:

顺便说一句,我使用了aws cli,它在做这类事情时效率更高 如何移动一个对象?我必须先调用复制然后删除对象吗? 小东西,但不是完成功能,您也可以将 console.log 作为函数传递:) 使用此代码 - 每个文件承诺(等待)在处理下一个文件之前不会排队吗?使用@PeterPeng 解决方案,它对 Promise 执行相同的操作,但它是并行执行的。不知道你.. 您可能会使用new Regex("^" + oldPrefix) 而不仅仅是oldPrefix,以确保您要替换的是实际前缀,而不是密钥字符串的任何其他部分。【参考方案2】:

对 Aditya Manohar 代码的一个小改动,改进了 s3.copyObject 函数中的错误处理,并在执行复制请求后通过删除源文件来真正完成“移动”请求:

const AWS = require('aws-sdk');
const async = require('async');
const bucketName = 'foo';
const oldPrefix = 'abc/';
const newPrefix = 'xyz/';

const s3 = new AWS.S3(
    params: 
        Bucket: bucketName
    ,
    region: 'us-west-2'
);


// 1) List all the objects in the source "directory"
s3.listObjects(
    Prefix: oldPrefix
, function (err, data) 



    if (data.Contents.length) 

        // Build up the paramters for the delete statement
        let paramsS3Delete = 
            Bucket: bucketName,
            Delete: 
                Objects: []
            
        ;

        // Expand the array with all the keys that we have found in the ListObjects function call, so that we can remove all the keys at once after we have copied all the keys
        data.Contents.forEach(function (content) 
            paramsS3Delete.Delete.Objects.push(
                Key: content.Key
            );
        );

        // 2) Copy all the source files to the destination
        async.each(data.Contents, function (file, cb) 
            var params = 
                CopySource: bucketName + '/' + file.Key,
                Key: file.Key.replace(oldPrefix, newPrefix)
            ;
            s3.copyObject(params, function (copyErr, copyData) 

                if (copyErr) 
                    console.log(err);
                 else 
                    console.log('Copied: ', params.Key);
                
                cb();
            );
        , function (asyncError, asyncData) 
            // All the requests for the file copy have finished
            if (asyncError) 
                return console.log(asyncError);
             else 
                console.log(asyncData);

                // 3) Now remove the source files - that way we effectively moved all the content
                s3.deleteObjects(paramsS3Delete, (deleteError, deleteData) => 
                    if (deleteError) return console.log(deleteError);

                    return console.log(deleteData);
                )

            
        );
    
);

请注意,我已将 cb() 回调函数移到 if-then-else 循环之外。这样即使发生错误,异步模块也会触发 done() 函数。

【讨论】:

有没有办法像我们上面的删除一样,在一个请求中移动或复制多个文件? @Raghavendra:不太确定你在找什么。如果您想要复制而不是移动文件,则只需跳过步骤 (3) “s3.deleteObjects()”。如果您想避免对每个文件进行多个 HTTP 请求,那么我认为唯一的方法就是依赖 AWS CLI。 AWS CLI 具有 cp() 方法,可让您一次性复制多个文件或完整的“目录”:docs.aws.amazon.com/cli/latest/reference/s3/cp.html 这些方法不允许删除多个它们采用模式前缀来复制或删除我有一组文件 在你的代码中 asyncData 总是未定义? asyncError 也总是未定义? @bpavlov:我猜你对 asyncData 对象的看法是正确的。但是根据异步文档 (caolan.github.io/async/docs.html#each),只要迭代例程中发生错误,asyncError 对象就会被填充 - 在这种情况下是 s3.copyObject() 逻辑。因此,当s3.copyObject() 逻辑没有问题时,asyncError 对象将为空。希望这会有所帮助!【参考方案3】:

这是一个以“异步等待”方式执行的代码 sn-p:

const AWS = require('aws-sdk');
AWS.config.update(
  credentials: new AWS.Credentials(....), // credential parameters
);
AWS.config.setPromisesDependency(require('bluebird'));
const s3 = new AWS.S3();

... ...

const bucketName = 'bucketName';        // example bucket
const folderToMove = 'folderToMove/';   // old folder name
const destinationFolder = 'destinationFolder/'; // new destination folder 
try 
    const listObjectsResponse = await s3.listObjects(
        Bucket: bucketName,
        Prefix: folderToMove,
        Delimiter: '/',
    ).promise();

    const folderContentInfo = listObjectsResponse.Contents;
    const folderPrefix = listObjectsResponse.Prefix;

    await Promise.all(
      folderContentInfo.map(async (fileInfo) => 
        await s3.copyObject(
          Bucket: bucketName,
          CopySource: `$bucketName/$fileInfo.Key`,  // old file Key
          Key: `$destinationFolder/$fileInfo.Key.replace(folderPrefix, '')`, // new file Key
        ).promise();
    
        await s3.deleteObject(
          Bucket: bucketName,
          Key: fileInfo.Key,
        ).promise();
      )
    );
 catch (err) 
  console.error(err); // error handling

【讨论】:

【参考方案4】:

对递归复制文件夹的原始代码进行了更多更新。一些限制是代码不能处理每个前缀超过 1000 个对象,当然还有深度限制,如果您的文件夹非常深。

import AWS from 'aws-sdk';

AWS.config.update( region: 'ap-southeast-1' );

/**
 * Copy s3 folder
 * @param string bucket Params for the first argument
 * @param string source for the 2nd argument
 * @param string dest for the 2nd argument
 * @returns promise the get object promise
 */
export default async function s3CopyFolder(bucket, source, dest) 
  // sanity check: source and dest must end with '/'
  if (!source.endsWith('/') || !dest.endsWith('/')) 
    return Promise.reject(new Error('source or dest must ends with fwd slash'));
  

  const s3 = new AWS.S3();

  // plan, list through the source, if got continuation token, recursive
  const listResponse = await s3.listObjectsV2(
    Bucket: bucket,
    Prefix: source,
    Delimiter: '/',
  ).promise();

  // copy objects
  await Promise.all(
    listResponse.Contents.map(async (file) => 
      await s3.copyObject(
        Bucket: bucket,
        CopySource: `$bucket/$file.Key`,
        Key: `$dest$file.Key.replace(listResponse.Prefix, '')`,
      ).promise();
    ),
  );

  // recursive copy sub-folders
  await Promise.all(
    listResponse.CommonPrefixes.map(async (folder) => 
      await s3CopyFolder(
        bucket,
        `$folder.Prefix`,
        `$dest$folder.Prefix.replace(listResponse.Prefix, '')`,
      );
    ),
  );

  return Promise.resolve('ok');

【讨论】:

唯一的递归解决方案 这不包括描述中建议的继续令牌处理:)【参考方案5】:

以上都不能处理大型目录,因为list-objects-v2 命令一次返回不超过 1000 个结果,提供了一个继续令牌来访问其他“页面”。

这是使用现代 v3 sdk 的解决方案:

const copyAll = async (
  s3Client,
  sourceBucket,
  targetBucket = sourceBucket,
  sourcePrefix,
  targetPrefix,
  concurrency = 1,
  deleteSource = false,
) => 
  let ContinuationToken;

  const copyFile = async (sourceKey) => 
    const targetKey = sourceKey.replace(sourcePrefix, targetPrefix);

    await s3Client.send(
      new CopyObjectCommand(
        Bucket: targetBucket,
        Key: targetKey,
        CopySource: `$sourceBucket/$sourceKey`,
      ),
    );

    if (deleteSource) 
      await s3Client.send(
        new DeleteObjectCommand(
          Bucket: sourceBucket,
          Key: sourceKey,
        ),
      );
    
  ;

  do 
    const  Contents = [], NextContinuationToken  = await s3Client.send(
      new ListObjectsV2Command(
        Bucket: sourceBucket,
        Prefix: sourcePrefix,
        ContinuationToken,
      ),
    );

    const sourceKeys = Contents.map(( Key ) => Key);

    await Promise.all(
      new Array(concurrency).fill(null).map(async () => 
        while (sourceKeys.length) 
          await copyFile(sourceKeys.pop());
        
      ),
    );

    ContinuationToken = NextContinuationToken;
   while (ContinuationToken);
;

如果Promise.all部分不清楚,那只是一个穷人的“线程池”,允许你同时复制多个文件,这可以大大加快速度。这些不使用任何带宽,因为内容是在 AWS 中复制的,所以我对 concurrency 的值为 20 或更大没有任何问题。为清楚起见,它只是以下的并行版本:

const sourceKeys = Contents.map(( Key ) => Key);

while (sourceKeys.length) 
  await copyFile(sourceKeys.pop());

【讨论】:

以上是关于如何使用适用于 Node.js 的 AWS 开发工具包将 Amazon S3 中的所有对象从一个前缀复制/移动到另一个前缀的主要内容,如果未能解决你的问题,请参考以下文章

在 ECS 中运行时,适用于 node.js 的 AWS 开发工具包未获取凭证

为啥我在 s3 上的访问被拒绝(使用适用于 Node.js 的 aws-sdk)?

如何包含适用于 Node.js 的 Amazon EC2 库?

如何使用适用于 node.js 的单实例弹性负载均衡器设置 HTTPS?

在 aws opsworks 上升级 node.js

如何使用适用于 DynamoDb 的 AWS Rust 开发工具包编写惯用的 Rust 错误处理?