【问题标题】:aws-sdk S3: best way to list all keys with listObjectsV2aws-sdk S3:使用 listObjectsV2 列出所有键的最佳方法
【发布时间】:2022-02-28 13:03:29
【问题描述】:

使用 listObjects API 调用的 v1 版本,您可以从 SO answer 执行类似的操作。

var allKeys = [];
function listAllKeys(marker, cb) {
  s3.listObjects({Bucket: s3bucket, Marker: marker}, function(err, data){
     allKeys.push(data.Contents);

    if(data.IsTruncated)
       listAllKeys(data.NextMarker, cb);
    else
       cb();
  });
}

listObjectsV2 函数的等价物是什么?

【问题讨论】:

  • 20 年更新:不需要为您的项目提供整个 sdk(就像大多数这些答案所暗示的那样),只需要必要的模块来减少 bundle 的未来大小。 let aws = require('aws-sdk/global');let S3 = require('aws-sdk/clients/s3');

标签: node.js amazon-s3


【解决方案1】:

在我看来这是最好的方法:

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

const listAllKeys = (params, out = []) => new Promise((resolve, reject) => {
  s3.listObjectsV2(params).promise()
    .then(({Contents, IsTruncated, NextContinuationToken}) => {
      out.push(...Contents);
      !IsTruncated ? resolve(out) : resolve(listAllKeys(Object.assign(params, {ContinuationToken: NextContinuationToken}), out));
    })
    .catch(reject);
});

listAllKeys({Bucket: 'bucket-name'})
  .then(console.log)
  .catch(console.log);

【讨论】:

  • 为什么这是最好的? (我问是因为我不知道。)
  • 主要是因为它简洁,所有需要的变量都是自包含的。我基本上保持代码干净。
  • 错误答案,你不应该使用promise作为递归,它们是不一样的。 Promise 将相互叠加,直到最后一个 Promise 解决。提供的解决方案将完成简单任务的工作,但是如果您的存储桶脚本中有大量记录,则首先会开始减慢并最终耗尽内存(在我的情况下,在检索前 5 百万后崩溃)。我设法通过将 async await Promise 调用放在 while 循环中来解决这个问题
  • 你是对的@Den,这是适合简单任务的解决方案,我还使用异步等待循环来完成更大的任务
【解决方案2】:

这是从存储桶中获取密钥列表的代码。

var params = {
    Bucket: 'bucket-name'    
};

var allKeys = [];
listAllKeys();
function listAllKeys() {
    s3.listObjectsV2(params, function (err, data) {
        if (err) {
            console.log(err, err.stack); // an error occurred
        } else {
            var contents = data.Contents;
            contents.forEach(function (content) {
                allKeys.push(content.Key);
            });

            if (data.IsTruncated) {
                params.ContinuationToken = data.NextContinuationToken;
                console.log("get further list...");
                listAllKeys();
            } 

        }
    });
}

【讨论】:

  • 你能删除所有问题,例如打印?
  • 按要求更新了答案。
  • 这不会等待 listAllKeys 因此如果您在函数声明后尝试使用它,则 allKeys 为空。
【解决方案3】:

在之前的答案的基础上,这是一种利用 Prefix 参数并行多次调用 s3.listObjectsV2() 的方法。

这为我带来了 2-15 倍的加速,具体取决于密钥分布的均匀程度以及代码是在本地运行还是在 AWS 上运行。

您应确保前缀涵盖存储桶的所有可能前缀。下面的代码涵盖了所有“安全”字符,但 S3 支持 wider range of UTF-8 characters

请注意,此示例使用 async/await,因此需要 ES2017/Node 8。该示例是一个 Node 8.10 Lambda 函数。

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

exports.handler = async (event) => {
  // Prefixes are used to fetch data in parallel.
  const numbers = '0123456789'.split('');
  const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
  const special = "!-_'.*()".split(''); // "Safe" S3 special chars
  const prefixes = [...numbers, ...letters, ...special];

  // array of params used to call listObjectsV2 in parallel for each prefix above
  const arrayOfParams = prefixes.map((prefix) => {
    return { Bucket: 'YOUR-BUCKET-NAME', Prefix: prefix }
  });

  const allKeys = [];
  await Promise.all(arrayOfParams.map(params => getAllKeys(params, allKeys)));
  return allKeys.length;
};

async function getAllKeys(params,  allKeys = []){
  const response = await s3.listObjectsV2(params).promise();
  response.Contents.forEach(obj => allKeys.push(obj.Key));

  if (response.NextContinuationToken) {
    params.ContinuationToken = response.NextContinuationToken;
    await getAllKeys(params, allKeys); // RECURSIVE CALL
  }
  return allKeys;
}

另外,为了完整起见,这里有一个更简单、无前缀的 async/await 版本:

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

exports.handler = async (event) => {
  const allKeys = await getAllKeys({ Bucket: 'YOUR-BUCKET-NAME' });
  return allKeys.length;
};

async function getAllKeys(params,  allKeys = []){
  const response = await s3.listObjectsV2(params).promise();
  response.Contents.forEach(obj => allKeys.push(obj.Key));

  if (response.NextContinuationToken) {
    params.ContinuationToken = response.NextContinuationToken;
    await getAllKeys(params, allKeys); // RECURSIVE CALL
  }
  return allKeys;
}

【讨论】:

  • 谢谢,能发个非async/await的版本吗?谢谢
  • @loretoparisi 我今天碰巧需要同样的东西,并制作了一个不使用您列出的任何一个关键字的东西,如果您仍然需要它,请检查下面的答案:)
【解决方案4】:

我知道这已经回答了好几次了,但我想我会冒险尝试我的版本。它基于this answer,但做了一些看起来值得的更改:

  1. s3 作为参数,而不是从全局上下文中拉取它。

  2. 不必返回new Promises3.listObjectsV2().promise() 已经返回了一个承诺,可以捎带它。

  3. 连接返回值,而不是将其作为参数传递到调用堆栈。

  4. 检查NextContinuationToken 是否确实有值。如果出于某种原因IsTruncated 为真,但没有NextContinuationToken,除非您检查该值,否则该函数将永远递归。如果将MaxKeys 设置为小于对象总数的值,就会发生这种情况。

const listAllObjects = (s3, params) => {
    return s3.listObjectsV2(params).promise()
        .then(({ Contents, IsTruncated, NextContinuationToken }) => {
            return IsTruncated && NextContinuationToken
                ? listAllObjects(s3, Object.assign({}, params, { ContinuationToken: NextContinuationToken }))
                    .then(x => Contents.concat(x))
                : Contents
        })
}

这是一个有趣的测试:

test('Returns all results on multiple continuations', () => {
    expect.assertions(1)

    let numCalls = 0

    // mock
    const s3 = {
        listObjectsV2: params => {
            numCalls++

            return {
                promise: () => {
                    return new Promise((resolve, reject) => {
                        setTimeout(() => {
                            if(numCalls === 3) {
                                resolve({
                                    Contents: [numCalls],
                                    IsTruncated: false,
                                })
                            }
                            else {
                                resolve({
                                    Contents: [numCalls],
                                    IsTruncated: true,
                                    NextContinuationToken: 'blah'
                                })
                            }
                        }, 200)
                    })
                }
            }
        }
    }

    return listAllObjects(s3, {})
        .then(xs => {
            expect(xs).toEqual([1, 2, 3])
        })
})

【讨论】:

    【解决方案5】:

    https://stackoverflow.com/a/57540786/8784402

    使用异步生成器

    导入 S3

    const { S3 } = require("aws-sdk");
    const s3 = new S3();
    

    创建一个生成器函数来检索所有文件列表

    async function* listAllKeys(opts) {
      opts = { ...opts };
      do {
        const data = await s3.listObjectsV2(opts).promise();
        opts.ContinuationToken = data.NextContinuationToken;
        yield data;
      } while (opts.ContinuationToken);
    }
    

    准备aws参数,基于api docs

    const opts = {
      Bucket: "bucket-xyz" /* required */,
      // ContinuationToken: 'STRING_VALUE',
      // Delimiter: 'STRING_VALUE',
      // EncodingType: url,
      // FetchOwner: true || false,
      // MaxKeys: 'NUMBER_VALUE',
      // Prefix: 'STRING_VALUE',
      // RequestPayer: requester,
      // StartAfter: 'STRING_VALUE'
    };
    

    使用生成器

    async function main() {
      // using for of await loop
      for await (const data of listAllKeys(opts)) {
        console.log(data.Contents);
      }
    }
    main();
    

    就是这样

    或延迟加载

    async function main() {
      const keys = listAllKeys(opts);
      console.log(await keys.next());
      // {value: {…}, done: false}
      console.log(await keys.next());
      // {value: {…}, done: false}
      console.log(await keys.next());
      // {value: undefined, done: true}
    }
    main();
    

    或者使用生成器制作 Observable 函数

    const lister = (opts) => (o$) => {
      let needMore = true;
      const process = async () => {
        for await (const data of listAllKeys(opts)) {
          o$.next(data);
          if (!needMore) break;
        }
        o$.complete();
      };
      process();
      return () => (needMore = false);
    };
    

    在 RXJS 中使用这个 observable 函数

    // Using Rxjs
    
    const { Observable } = require("rxjs");
    const { flatMap } = require("rxjs/operators");
    
    function listAll() {
      return Observable.create(lister(opts))
        .pipe(flatMap((v) => v.Contents))
        .subscribe(console.log);
    }
    
    listAll();
    

    或者在 Nodejs EventEmitter 中使用这个 observable 函数

    const EventEmitter = require("events");
    
    const _eve = new EventEmitter();
    
    async function onData(data) {
      // will be called for each set of data
      console.log(data);
    }
    async function onError(error) {
      // will be called if any error
      console.log(error);
    }
    async function onComplete() {
      // will be called when data completely received
    }
    _eve.on("next", onData);
    _eve.on("error", onError);
    _eve.on("complete", onComplete);
    
    const stop = lister(opts)({
      next: (v) => _eve.emit("next", v),
      error: (e) => _eve.emit("error", e),
      complete: (v) => _eve.emit("complete", v),
    });
    

    使用 Typescript 和 AWS-SDK v3 + Deno

    import {
      paginateListObjectsV2,
      S3Client,
      S3ClientConfig,
    } from "@aws-sdk/client-s3";
    
    /* // For Deno
    import {
      paginateListObjectsV2,
      S3Client,
      S3ClientConfig,
    } from "https://deno.land/x/aws_sdk@v3.14.0.0/client-s3/mod.ts"; */
    
    const s3Config: S3ClientConfig = {
      credentials: {
        accessKeyId: "accessKeyId",
        secretAccessKey: "secretAccessKey",
      },
      region: "us-east-1",
    };
    
    const client = new S3Client(s3Config);
    const s3Opts = { Bucket: "bucket-xyz" };
    
    async function getAllS3Files() {
      const totalFiles = [];
      for await (const data of paginateListObjectsV2({ client }, s3Opts)) {
        totalFiles.push(...(data.Contents ?? []));
      }
      return totalFiles;
    }
    
    

    【讨论】:

      【解决方案6】:

      对于那些不喜欢递归承诺的人来说,这是一个使用 do/while 的简单 TypeScript 3 实现:)

      export async function listKeys(
        s3client: AWS.S3,
        bucket: string,
        prefix: string,
      ): Promise<AWS.S3.ObjectList> {
        let token: string = undefined;
        let objectList: AWS.S3.ObjectList = [];
        do {
          const res = await s3client
            .listObjectsV2({
              Prefix: prefix,
              Bucket: bucket,
              ContinuationToken: token,
            })
            .promise();
          token = res.NextContinuationToken;
          objectList = objectList.concat(res.Contents);
        } while (token !== undefined);
        return objectList;
      }
      

      【讨论】:

        【解决方案7】:

        从这里使用了我需要的one of the answers,并进行了调整,以便从内部返回结果,而不是通过作为参数传递的外部数组,我将把它留在这里,以防有人发现它有用:

        const bucket = {Bucket: '<bucket name here>'};
        

        ...

        s3files(bucket).then(array => {
                console.log(_.map(array, entry => {
                    return entry.Key;
                }));
            });
        

        ...

        let s3files = function (config) {
        
            const tmp = Object.assign({}, config);
        
            return new Promise(resolve => {
        
                s3.listObjectsV2(tmp).promise().then(response => {
        
                    if (response.IsTruncated) {
        
                        tmp.ContinuationToken = response.NextContinuationToken;
        
                        s3files(tmp).then(array => {
                            resolve(response.Contents.concat(array));
                        });
        
                    } else {
                        resolve(response.Contents);
                    }
                });
        
            });
        };
        

        【讨论】:

          【解决方案8】:

          我稍微修改了上面的递归解决方案以获得这个版本,它添加了一个 sorting 附加组件(不是 listObjectsV2 功能),start-after 选项(StartAfter 在 NodeJS SDK 中),然后中断MaxKeys.

           function formatSizeUnits(bytes) {
              if (bytes >= 1099511627776) { bytes = (bytes / 1099511627776).toFixed(4) + ' PB'; }
              else if (bytes >= 1073741824) { bytes = (bytes / 1073741824).toFixed(4) + ' GB'; }
              else if (bytes >= 1048576) { bytes = (bytes / 1048576).toFixed(4) + ' MB'; }
              else if (bytes >= 1024) { bytes = (bytes / 1024).toFixed(4) + ' KB'; }
              else if (bytes > 1) { bytes = bytes + ' bytes'; }
              else if (bytes == 1) { bytes = bytes + ' byte'; }
              else { bytes = '0 byte'; }
              return bytes;
          }//formatSizeUnits
          var listFiles = function (params, callback) {
                  var self = this;
          
                  var options = {
                      Bucket: self._options.s3.Bucket,
                      Prefix: self._options.s3.Key,
                      MaxKeys: 100,
                      StartAfter: '',
                      Desc: true
                  };
                  for (var attrname in params) { options[attrname] = params[attrname]; }
                  var desc = options.Desc;
                  delete (options.Desc);
                  function listAllKeys(token, results, callback) {
                      if (token) options.ContinuationToken = token;
                      s3.listObjectsV2(options, (error, data) => {
                          if (error) {
                              return callback(error);
                          } else {
                              for (var index in data.Contents) {
                                  var bucket = data.Contents[index];
                                  if (bucket.Size > 0) {
                                      var components = bucket.Key.split('/');
                                      var name = components[components.length - 1];
                                      results.push({
                                          name: name,
                                          path: bucket.Key,
                                          mtime: bucket.LastModified,
                                          size: bucket.Size,
                                          sizehr: formatSizeUnits(bucket.Size)
                                      });
                                  }
                              }
                              if (results.length >= options.MaxKeys) { // exit max results
                                  results = results.slice(0, options.MaxKeys);
                                  return callback(null, results);
                              } else if (data.IsTruncated) { // truncated page
                                  return listAllKeys(data.NextContinuationToken, results, callback);
                              } else { // all pages
                                  return callback(null, results);
                              }
                          }
                      });
                  }//listAllKeys
                  var results = [];
                  listAllKeys('', results, (error, results) => {
                      if(!Util.empty(results)) {
                          if (desc) results.sort((a, b) => (b.mtime).getTime() - (a.mtime).getTime());
                          else results.sort((a, b) => (a.mtime).getTime() - (b.mtime).getTime());
                      }
                      return callback(error, results);
                  });
              }//listFiles
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-06-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-06-04
            • 2018-08-11
            相关资源
            最近更新 更多