使用 Lambda 节点从 S3 上的文件在 S3 上创建一个 zip 文件

Posted

技术标签:

【中文标题】使用 Lambda 节点从 S3 上的文件在 S3 上创建一个 zip 文件【英文标题】:Create a zip file on S3 from files on S3 using Lambda Node 【发布时间】:2016-12-02 16:20:51 【问题描述】:

我需要创建一个 Zip 文件,其中包含位于我的 s3 存储桶中的一系列文件(视频和图像)。

目前使用下面的代码的问题是我很快就达到了 Lambda 的内存限制。

async.eachLimit(files, 10, function(file, next) 
    var params = 
        Bucket: bucket, // bucket name
        Key: file.key
    ;
    s3.getObject(params, function(err, data) 
        if (err) 
            console.log('file', file.key);
            console.log('get image files err',err, err.stack); // an error occurred
         else 
            console.log('file', file.key);
            zip.file(file.key, data.Body);
            next();
        
    );
, 
function(err) 
    if (err) 
        console.log('err', err);
     else 
        console.log('zip', zip);
        content = zip.generateNodeStream(
            type: 'nodebuffer',
            streamFiles:true
        );
        var params = 
            Bucket: bucket, // name of dest bucket
            Key: 'zipped/images.zip',
            Body: content
        ;
        s3.upload(params, function(err, data) 
            if (err) 
                console.log('upload zip to s3 err',err, err.stack); // an error occurred
             else 
                console.log(data); // successful response
            
        );
    
);

这是否可以使用 Lambda,或者我应该看看不同的 方法?

是否可以即时写入压缩的 zip 文件,从而在一定程度上消除内存问题,还是需要在压缩之前收集文件?

任何帮助将不胜感激。

【问题讨论】:

【参考方案1】:

使用流可能会很棘手,因为我不确定如何将多个流通过管道传输到一个对象中。我已经使用标准文件对象做了几次。这是一个多步骤的过程,而且速度非常快。请记住,Lambda 在 Linux 中运行,因此您拥有所有 Linux 资源,包括系统 /tmp 目录。

    在 /tmp 中创建一个子目录,调用“transient”或任何适合您的方式 使用 s3.getObject() 并将文件对象写入 /tmp/transient 使用 GLOB 包从 /tmp/transient 生成一个路径数组[] 循环数组和 zip.addLocalFile(array[i]); zip.writeZip('tmp/files.zip');

【讨论】:

我能看到的唯一问题是 lambda 在 tmp 目录中的存储空间限制为 500mb。在这种情况下,它也会限制最终的 zip 大小。 不确定您是否在 .zip 进程旁边运行任何文件处理,但由于数据量如此之大,您需要确保您的函数可以在 5 分钟的执行时间内完成。我的最大数据量通常是每次执行大约 20-25mg。 @Rabona 您是否设法通过 lambda 解决了这个问题?我有同样的问题。我们需要压缩一个包含大约 100Mb 图像的 1.5GB 视频文件。我们的内存用完了。我们还尝试使用具有相同图像的较小视频文件(~1gb)并获得超时。希望您可能发现了一些有用的东西,也可以帮助我们。 我们最终使用 Java 流解决方案解决了这个问题。这使我们能够绕过内存问题。 您能否分享一下您是如何使用 Java 流解决方案解决的?【参考方案2】:

好的,我今天必须这样做,它有效。 Direct Buffer to Stream,不涉及磁盘。所以内存或磁盘限制在这里不会成为问题:

'use strict';

const AWS = require("aws-sdk");
AWS.config.update(  region: "eu-west-1"  );
const s3 = new AWS.S3(  apiVersion: '2006-03-01' );

const   _archiver = require('archiver');

//This returns us a stream.. consider it as a real pipe sending fluid to S3 bucket.. Don't forget it
const streamTo = (_bucket, _key) => 
	var stream = require('stream');
	var _pass = new stream.PassThrough();
	s3.upload(  Bucket: _bucket, Key: _key, Body: _pass , (_err, _data) =>  /*...Handle Errors Here*/  );
	return _pass;
;
      
exports.handler = async (_req, _ctx, _cb) => 
	var _keys = ['list of your file keys in s3'];
	
    var _list = await Promise.all(_keys.map(_key => new Promise((_resolve, _reject) => 
            s3.getObject(Bucket:'bucket-name', Key:_key)
                .then(_data => _resolve(  data: _data.Body, name: `$_key.split('/').pop()`  ));
        
    ))).catch(_err =>  throw new Error(_err)  );

    await new Promise((_resolve, _reject) =>  
        var _myStream = streamTo('bucket-name', 'fileName.zip');		//Now we instantiate that pipe...
        var _archive = _archiver('zip');
        _archive.on('error', err =>  throw new Error(err);  );
        
        //Your promise gets resolved when the fluid stops running... so that's when you get to close and resolve
        _myStream.on('close', _resolve);
        _myStream.on('end', _resolve);
        _myStream.on('error', _reject);
        
        _archive.pipe(_myStream);			//Pass that pipe to _archive so it can push the fluid straigh down to S3 bucket
        _list.forEach(_itm => _archive.append(_itm.data,  name: _itm.name  ) );		//And then we start adding files to it
        _archive.finalize();				//Tell is, that's all we want to add. Then when it finishes, the promise will resolve in one of those events up there
    ).catch(_err =>  throw new Error(_err)  );
    
    _cb(null,   );		//Handle response back to server
;

【讨论】:

是否可以将 zip 作为响应直接发送给“用户”? @bobmoff,对于 Lambda,否,因为我们无法直接访问 HTTP 响应流。在常规的 NodeJs 环境中,是的,这是可能的。查看 NodeJs 文档,HTTP 请求和响应 是 Writable Streams 对象的一部分:link 就像 zlib、fs 等,但我们无法在 Lambda 中直接访问它;我们只使用回调函数与客户端通信。 我刚刚注意到您没有从 s3 流式传输对象,如果下载很多文件可能会导致内存问题,对吧?我不确定 .get 方法在游览示例中的作用,因为它在 aws-sdk 上不存在。猜你的意思是getObject。但无论如何,当您等待所有下载的对象完成时,这意味着所有这些对象必须同时存在于内存中。 Lambda 的内存有限,所以我想它必须改用 .getReadStream 并将它们附加到存档器中,还是我遗漏了什么? @bobmoff 是的,抱歉,我们有所有 s3 操作的包装器,应该是 getObject 并且你应该传入一个返回函数。 我刚刚使用 s3.getObject().createReadStream() 进行了尝试,对于我对 3-10mb 左右的 100 个文件的测试,内存使用停止在 170mb 左右。与没有流相比,它高达 730mb。它也更快,因为它不必等待首先下载所有对象然后启动流到存储桶。【参考方案3】:

我根据@iocoker 格式化了代码。

主条目

// index.js

'use strict';
const S3Zip = require('./s3-zip')

const params = 
  files: [
    
      fileName: '1.jpg',
      key: 'key1.JPG'
    ,
    
      fileName: '2.jpg',
      key: 'key2.JPG'
    
  ],
  zippedFileKey: 'zipped-file-key.zip'


exports.handler = async event => 
  const s3Zip = new S3Zip(params);
  await s3Zip.process();

  return 
    statusCode: 200,
    body: JSON.stringify(
      
        message: 'Zip file successfully!'
      
    )
  ;



压缩文件工具

// s3-zip.js

'use strict';
const fs = require('fs');
const AWS = require("aws-sdk");

const Archiver = require('archiver');
const Stream = require('stream');

const https = require('https');
const sslAgent = new https.Agent(
  KeepAlive: true,
  rejectUnauthorized: true
);
sslAgent.setMaxListeners(0);
AWS.config.update(
  httpOptions: 
    agent: sslAgent,
  ,
  region: 'us-east-1'
);

module.exports = class S3Zip 
  constructor(params, bucketName = 'default-bucket') 
    this.params = params;
    this.BucketName = bucketName;
  

  async process() 
    const  params, BucketName  = this;
    const s3 = new AWS.S3( apiVersion: '2006-03-01', params:  Bucket: BucketName  );

    // create readstreams for all the output files and store them
    const createReadStream = fs.createReadStream;
    const s3FileDwnldStreams = params.files.map(item => 
      const stream = s3.getObject( Key: item.key ).createReadStream();
      return 
        stream,
        fileName: item.fileName
      
    );

    const streamPassThrough = new Stream.PassThrough();
    // Create a zip archive using streamPassThrough style for the linking request in s3bucket
    const uploadParams = 
      ACL: 'private',
      Body: streamPassThrough,
      ContentType: 'application/zip',
      Key: params.zippedFileKey
    ;

    const s3Upload = s3.upload(uploadParams, (err, data) => 
      if (err) 
        console.error('upload err', err)
       else 
        console.log('upload data', data);
      
    );

    s3Upload.on('httpUploadProgress', progress => 
      // console.log(progress); //  loaded: 4915, total: 192915, part: 1, key: 'foo.jpg' 
    );

    // create the archiver
    const archive = Archiver('zip', 
      zlib:  level: 0 
    );
    archive.on('error', (error) => 
      throw new Error(`$error.name $error.code $error.message $error.path $error.stack`);
    );

    // connect the archiver to upload streamPassThrough and pipe all the download streams to it
    await new Promise((resolve, reject) => 
      console.log("Starting upload of the output Files Zip Archive");

      streamPassThrough.on('close', resolve());
      streamPassThrough.on('end', resolve());
      streamPassThrough.on('error', reject());

      archive.pipe(streamPassThrough);
      s3FileDwnldStreams.forEach((s3FileDwnldStream) => 
        archive.append(s3FileDwnldStream.stream,  name: s3FileDwnldStream.fileName )
      );
      archive.finalize();

    ).catch((error) => 
      throw new Error(`$error.code $error.message $error.data`);
    );

    // Finally wait for the uploader to finish
    await s3Upload.promise();

  

【讨论】:

像魅力一样工作。非常感谢李振琪。我发现这个答案的结构很好,比这里的其他答案更容易理解。 @HardikShah 欢迎您。我很高兴能够提供帮助。 更新:因此这适用于压缩 600-700 个文件(所有文件的大小约为 2mb),但一旦文件数超过该数量,则不会创建 zip,也不会记录错误。所有被压缩的文件都很小~2-3 mb。知道可能出了什么问题吗? 我这边没有问题,即使单个文件达到7MB。而我的一般配置是:内存(3008MB),超时(15分钟)。您可以检查您的常规配置或调试代码。 @HardikShah @zhenqili,我已经创建了一个 lambda 函数并放置了这个代码和配置,但是 lambda 函数抛出了这个错误“trace”:[“Runtime.ImportModuleError: Error: Cannot find module 'archiver'” could请帮忙解决?【参考方案4】:

其他解决方案非常适合文件不多(小于~60)。如果他们处理更多文件,他们就会毫无错误地退出。这是因为他们打开了太多的流。

这个解决方案的灵感来自https://gist.github.com/amiantos/16bacc9ed742c91151fcf1a41012445e

这是一个有效的解决方案,即使处理许多文件 (+300) 也能很好地工作,并将预签名的 URL 返回到包含文件的 zip。

主 Lambda:

const AWS = require('aws-sdk');
const S3 = new AWS.S3(
  apiVersion: '2006-03-01',
  signatureVersion: 'v4',
  httpOptions: 
    timeout: 300000 // 5min Should Match Lambda function timeout
  
);
const archiver = require('archiver');
import stream from 'stream';

const UPLOAD_BUCKET_NAME = "my-s3-bucket";
const URL_EXPIRE_TIME = 5*60;

export async function getZipSignedUrl(event) 
  const prefix = `uploads/id123123/`;   //replace this with your S3 prefix
  let files = ["12314123.png", "56787567.png"]  //replace this with your files

  if (files.length == 0) 
    console.log("No files to zip");
    return result(404, "No pictures to download");
  
  console.log("Files to zip: ", files);

  try 
    files = files.map(file => 
        return 
            fileName: file,
            key: prefix + '/' + file,
            type: "file"
        ;
    );
    const destinationKey = prefix + '/' + 'uploads.zip'
    console.log("files: ", files);
    console.log("destinationKey: ", destinationKey);

    await streamToZipInS3(files, destinationKey);
    const presignedUrl = await getSignedUrl(UPLOAD_BUCKET_NAME, destinationKey, URL_EXPIRE_TIME, "uploads.zip");
    console.log("presignedUrl: ", presignedUrl);

    if (!presignedUrl) 
      return result(500, null);
    
    return result(200, presignedUrl);
  
  catch(error) 
    console.error(`Error: $error`);
    return result(500, null);
  

辅助函数:

export function result(code, message) 
  return 
    statusCode: code,
    body: JSON.stringify(
      
        message: message
      
    )
  


export async function streamToZipInS3(files, destinationKey) 
  await new Promise(async (resolve, reject) => 
    var zipStream = streamTo(UPLOAD_BUCKET_NAME, destinationKey, resolve);
    zipStream.on("error", reject);

    var archive = archiver("zip");
    archive.on("error", err => 
      throw new Error(err);
    );
    archive.pipe(zipStream);

    for (const file of files) 
      if (file["type"] == "file") 
        archive.append(getStream(UPLOAD_BUCKET_NAME, file["key"]), 
          name: file["fileName"]
        );
      
    
    archive.finalize();
  )
  .catch(err => 
    console.log(err);
    throw new Error(err);
  );


function streamTo(bucket, key, resolve) 
  var passthrough = new stream.PassThrough();
  S3.upload(
    
      Bucket: bucket,
      Key: key,
      Body: passthrough,
      ContentType: "application/zip",
      ServerSideEncryption: "AES256"
    ,
    (err, data) => 
      if (err) 
        console.error('Error while uploading zip')
        throw new Error(err);
        reject(err)
        return
      
      console.log('Zip uploaded')
      resolve()
    
  ).on("httpUploadProgress", progress => 
    console.log(progress)
  );
  return passthrough;


function getStream(bucket, key) 
  let streamCreated = false;
  const passThroughStream = new stream.PassThrough();

  passThroughStream.on("newListener", event => 
    if (!streamCreated && event == "data") 
      const s3Stream = S3
        .getObject( Bucket: bucket, Key: key )
        .createReadStream();
      s3Stream
        .on("error", err => passThroughStream.emit("error", err))
        .pipe(passThroughStream);

      streamCreated = true;
    
  );

  return passThroughStream;


export async function getSignedUrl(bucket: string, key: string, expires: number, downloadFilename?: string): Promise<string> 
    const exists = await objectExists(bucket, key);
    if (!exists) 
        console.info(`Object $bucket/$key does not exists`);
        return null
    

    let params = 
        Bucket: bucket,
        Key: key,
        Expires: expires,
    ;
    if (downloadFilename) 
        params['ResponseContentDisposition'] = `inline; filename="$encodeURIComponent(downloadFilename)"`; 
    
    
    try 
        const url = s3.getSignedUrl('getObject', params);
        return url;
     catch (err) 
        console.error(`Unable to get URL for $bucket/$key`, err);
        return null;
    
;

【讨论】:

使用这个 sn-p 作为我自己需求的灵感来源。非常有用,效果很好,非常感谢! 干杯!很高兴听到它有效!这就是我把它放在这里的原因!

以上是关于使用 Lambda 节点从 S3 上的文件在 S3 上创建一个 zip 文件的主要内容,如果未能解决你的问题,请参考以下文章

Zip 文件在使用节点和 AWS lambda 从 SFTP 服务器发布到 S3 后无法展开

AWS Lambda 函数写入 S3

Python 脚本作为 AWS S3 存储桶上的 Cron

如何从 S3 加载泡菜文件以在 AWS Lambda 中使用?

如何使用 lambda 函数从 AWS s3 获取文本文件的内容?

使用 lambda 中的 pandas 从 s3 读取 excel 文件并转换为 csv