ไพพ์สตรีมไปที่ s3.upload ()


95

ฉันกำลังใช้ปลั๊กอิน node.js ที่เรียกว่าs3-upload-streamเพื่อสตรีมไฟล์ขนาดใหญ่มากไปยัง Amazon S3 มันใช้ API หลายส่วนและส่วนใหญ่จะทำงานได้ดีมาก

อย่างไรก็ตามโมดูลนี้กำลังแสดงอายุและฉันต้องทำการแก้ไขแล้ว (ผู้เขียนได้เลิกใช้งานแล้วเช่นกัน) วันนี้ฉันพบปัญหาอื่นกับ Amazon และฉันอยากจะทำตามคำแนะนำของผู้เขียนและเริ่มใช้ aws-sdk อย่างเป็นทางการเพื่ออัปโหลดให้สำเร็จ

แต่.

SDK s3.upload()ที่อย่างเป็นทางการดูเหมือนจะไม่สนับสนุนการท่อ ลักษณะของ s3.upload คือคุณต้องส่งสตรีมที่อ่านได้เป็นอาร์กิวเมนต์ไปยังตัวสร้าง S3

ฉันมีโมดูลรหัสผู้ใช้ประมาณ 120+ รายการที่ประมวลผลไฟล์ต่างๆและไม่เชื่อเรื่องพระเจ้าไปยังปลายทางสุดท้ายของผลลัพธ์ เครื่องยนต์ส่งสตรีมเอาท์พุตที่เขียนได้แบบท่อให้พวกเขาและพวกมันก็ต่อท่อไป ฉันไม่สามารถมอบAWS.S3วัตถุให้พวกเขาและขอให้พวกเขาเรียกupload()ใช้โดยไม่ต้องเพิ่มรหัสให้กับโมดูลทั้งหมด เหตุผลที่ฉันใช้s3-upload-streamก็เพราะว่ามันรองรับท่อ

มีวิธีสร้าง aws-sdk ที่s3.upload()ฉันสามารถส่งกระแสข้อมูลไปได้ไหม

คำตอบ:


137

ตัดupload()ฟังก์ชันS3 ด้วยstream.PassThrough()สตรีมnode.js

นี่คือตัวอย่าง:

inputStream
  .pipe(uploadFromStream(s3));

function uploadFromStream(s3) {
  var pass = new stream.PassThrough();

  var params = {Bucket: BUCKET, Key: KEY, Body: pass};
  s3.upload(params, function(err, data) {
    console.log(err, data);
  });

  return pass;
}

2
เยี่ยมมากนี่แก้แฮ็คที่น่าเกลียดมากของฉัน = -) คุณช่วยอธิบายได้ไหมว่า stream.PassThrough () ทำอะไรได้จริง?
mraxus

6
สตรีม PassThrough ของคุณปิดเมื่อคุณทำสิ่งนี้หรือไม่ ฉันมีเวลาพอสมควรในการคาดการณ์การปิดใน s3.upload เพื่อเข้าสู่สตรีม PassThrough ของฉัน
สี่ 43

7
ขนาดของไฟล์ที่อัปโหลดคือ 0 ไบต์ ถ้าฉันไพพ์ข้อมูลเดียวกันจากสตรีมต้นทางไปยังระบบไฟล์ทั้งหมดจะทำงานได้ดี ความคิดใด ๆ ?
Radar155

3
สตรีม passthrough จะใช้ไบต์ที่เขียนไปและส่งออก วิธีนี้ช่วยให้คุณส่งคืนสตรีมที่เขียนได้ซึ่ง aws-sdk จะอ่านจากที่คุณเขียนไป ฉันจะส่งคืนอ็อบเจ็กต์การตอบกลับจาก s3.upload () ด้วยเพราะมิฉะนั้นคุณจะไม่สามารถมั่นใจได้ว่าการอัปโหลดจะเสร็จสมบูรณ์
Reconbot

1
สิ่งนี้ไม่เหมือนกับการส่งสตรีมที่อ่านได้ไปยัง Body แต่มีรหัสมากกว่าหรือไม่? AWS SDK จะยังคงเรียก read () บนสตรีม PassThrough ดังนั้นจึงไม่มีการวางท่อจริงไปจนถึง S3 ข้อแตกต่างเพียงอย่างเดียวคือมีสตรีมพิเศษอยู่ตรงกลาง
ShadowChaser

96

คำตอบช้าไปหน่อยมันอาจช่วยคนอื่นได้ คุณสามารถส่งคืนได้ทั้งสตรีมที่เขียนได้และสัญญาดังนั้นคุณจะได้รับข้อมูลตอบกลับเมื่อการอัปโหลดเสร็จสิ้น

const AWS = require('aws-sdk');
const stream = require('stream');

const uploadStream = ({ Bucket, Key }) => {
  const s3 = new AWS.S3();
  const pass = new stream.PassThrough();
  return {
    writeStream: pass,
    promise: s3.upload({ Bucket, Key, Body: pass }).promise(),
  };
}

และคุณสามารถใช้ฟังก์ชันดังต่อไปนี้:

const { writeStream, promise } = uploadStream({Bucket: 'yourbucket', Key: 'yourfile.mp4'});
const readStream = fs.createReadStream('/path/to/yourfile.mp4');

const pipeline = readStream.pipe(writeStream);

ตอนนี้คุณสามารถตรวจสอบสัญญา:

promise.then(() => {
  console.log('upload completed successfully');
}).catch((err) => {
  console.log('upload failed.', err.message);
});

หรือเป็นstream.pipe()สตรีมส่งคืนเขียนปลายทาง (ตัวแปร writeStream ด้านบน) ซึ่งอนุญาตให้มีห่วงโซ่ของไปป์เรายังสามารถใช้เหตุการณ์:

 pipeline.on('close', () => {
   console.log('upload successful');
 });
 pipeline.on('error', (err) => {
   console.log('upload failed', err.message)
 });

มันดูดี แต่ในด้านของฉันฉันได้รับข้อผิดพลาดนี้stackoverflow.com/questions/62330721/…
Arco Voltaico

เพียงตอบคำถามของคุณ หวังว่ามันจะช่วยได้
Ahmet Cetin

49

ในคำตอบที่ยอมรับฟังก์ชั่นจะสิ้นสุดลงก่อนที่การอัปโหลดจะเสร็จสมบูรณ์ดังนั้นจึงไม่ถูกต้อง โค้ดด้านล่างไปป์อย่างถูกต้องจากสตรีมที่อ่านได้

อัปโหลดข้อมูลอ้างอิง

async function uploadReadableStream(stream) {
  const params = {Bucket: bucket, Key: key, Body: stream};
  return s3.upload(params).promise();
}

async function upload() {
  const readable = getSomeReadableStream();
  const results = await uploadReadableStream(readable);
  console.log('upload complete', results);
}

คุณยังสามารถก้าวไปอีกขั้นและแสดงข้อมูลความคืบหน้าโดยใช้สิ่งต่อไปManagedUploadนี้:

const manager = s3.upload(params);
manager.on('httpUploadProgress', (progress) => {
  console.log('progress', progress) // { loaded: 4915, total: 192915, part: 1, key: 'foo.jpg' }
});

ข้อมูลอ้างอิง ManagedUpload

รายการเหตุการณ์ที่มีอยู่


1
ตอนนี้ aws-sdk เสนอสัญญาที่สร้างขึ้นใน 2.3.0+ ดังนั้นคุณไม่ต้องยกพวกเขาอีกต่อไป s3.upload (params) .promise () แล้ว (data => data) .catch (error => error);
DBrown

1
@DBrown ขอบคุณสำหรับตัวชี้! ฉันได้อัปเดตคำตอบแล้วตามนั้น
tsuz

1
@tsuz พยายามใช้โซลูชันของคุณทำให้ฉันมีข้อผิดพลาด: TypeError: dest.on is not a functionมีความคิดอย่างไร
FireBrand

คืออะไรdest.on? แสดงตัวอย่างได้ไหม @FireBrand
tsuz

9
สิ่งนี้บอกว่าคำตอบที่ยอมรับนั้นไม่สมบูรณ์ แต่ใช้ไม่ได้กับการไปยัง s3.upload ตามที่ระบุไว้ในโพสต์ที่อัปเดตของ @ Womp มันจะมีประโยชน์มากหากคำตอบนี้ได้รับการอัปเดตเพื่อรับเอาท์พุทแบบ piped ของอย่างอื่น!
MattW

6

ไม่มีคำตอบใดที่เหมาะกับฉันเพราะฉันต้องการ:

  • บีบเข้าไป s3.upload()
  • บีบผลลัพธ์ของs3.upload()ไปยังสตรีมอื่น

คำตอบที่ยอมรับจะไม่ทำอย่างหลัง คนอื่น ๆ อาศัย api สัญญาซึ่งยุ่งยากในการทำงานเมื่อทำงานกับท่อสตรีม

นี่คือการแก้ไขคำตอบที่ยอมรับของฉัน

const s3 = new S3();

function writeToS3({Key, Bucket}) {
  const Body = new stream.PassThrough();

  s3.upload({
    Body,
    Key,
    Bucket: process.env.adpBucket
  })
   .on('httpUploadProgress', progress => {
       console.log('progress', progress);
   })
   .send((err, data) => {
     if (err) {
       Body.destroy(err);
     } else {
       console.log(`File uploaded and available at ${data.Location}`);
       Body.destroy();
     }
  });

  return Body;
}

const pipeline = myReadableStream.pipe(writeToS3({Key, Bucket});

pipeline.on('close', () => {
  // upload finished, do something else
})
pipeline.on('error', () => {
  // upload wasn't successful. Handle it
})


มันดูดี แต่ในด้านของฉันฉันได้รับข้อผิดพลาดนี้stackoverflow.com/questions/62330721/…
Arco Voltaico

5

โซลูชัน Type Script:
ตัวอย่างนี้ใช้:

import * as AWS from "aws-sdk";
import * as fsExtra from "fs-extra";
import * as zlib from "zlib";
import * as stream from "stream";

และฟังก์ชัน async:

public async saveFile(filePath: string, s3Bucket: AWS.S3, key: string, bucketName: string): Promise<boolean> { 

         const uploadStream = (S3: AWS.S3, Bucket: string, Key: string) => {
            const passT = new stream.PassThrough();
            return {
              writeStream: passT,
              promise: S3.upload({ Bucket, Key, Body: passT }).promise(),
            };
          };
        const { writeStream, promise } = uploadStream(s3Bucket, bucketName, key);
        fsExtra.createReadStream(filePath).pipe(writeStream);     //  NOTE: Addition You can compress to zip by  .pipe(zlib.createGzip()).pipe(writeStream)
        let output = true;
        await promise.catch((reason)=> { output = false; console.log(reason);});
        return output;
}

เรียกวิธีนี้ว่า:

let result = await saveFileToS3(testFilePath, someS3Bucket, someKey, someBucketName);

4

สิ่งที่ควรทราบในคำตอบที่ได้รับการยอมรับมากที่สุดข้างต้นคือ: คุณต้องส่งคืนรหัสผ่านในฟังก์ชันหากคุณใช้ไปป์เช่น

fs.createReadStream(<filePath>).pipe(anyUploadFunction())

function anyUploadFunction () { 
 let pass = new stream.PassThrough();
 return pass // <- Returning this pass is important for the stream to understand where it needs to write to.
}

มิฉะนั้นมันจะเงียบไปข้างหน้าโดยไม่มีข้อผิดพลาดหรือจะทำให้เกิดข้อผิดพลาดTypeError: dest.on is not a functionขึ้นอยู่กับว่าคุณเขียนฟังก์ชันอย่างไร


3

หากช่วยให้ใครก็ตามที่ฉันสามารถสตรีมจากไคลเอนต์ไปยัง s3 ได้สำเร็จ:

https://gist.github.com/mattlockyer/532291b6194f6d9ca40cb82564db9d2a

โค้ดเซิร์ฟเวอร์จะถือว่าreqเป็นอ็อบเจ็กต์สตรีมในกรณีของฉันมันถูกส่งมาจากไคลเอนต์พร้อมข้อมูลไฟล์ที่ตั้งอยู่ในส่วนหัว

const fileUploadStream = (req, res) => {
  //get "body" args from header
  const { id, fn } = JSON.parse(req.get('body'));
  const Key = id + '/' + fn; //upload to s3 folder "id" with filename === fn
  const params = {
    Key,
    Bucket: bucketName, //set somewhere
    Body: req, //req is a stream
  };
  s3.upload(params, (err, data) => {
    if (err) {
      res.send('Error Uploading Data: ' + JSON.stringify(err) + '\n' + JSON.stringify(err.stack));
    } else {
      res.send(Key);
    }
  });
};

ใช่มันทำลายการประชุม แต่ถ้าคุณดูส่วนสำคัญมันสะอาดกว่าสิ่งอื่นใดที่ฉันพบโดยใช้ Multer, busboy ฯลฯ ...

+1 สำหรับแนวทางปฏิบัติและขอบคุณ @SalehenRahman สำหรับความช่วยเหลือของเขา


Multer, busboy จัดการการอัปโหลดข้อมูลหลายส่วน / แบบฟอร์ม req เป็นสตรีมทำงานเมื่อไคลเอนต์ส่งบัฟเฟอร์เป็น body จาก XMLHttpRequest
André Werlang

เพื่อชี้แจงการอัปโหลดกำลังดำเนินการจากส่วนหลังไม่ใช่ไคลเอนต์ใช่ไหม
numX

ใช่มัน "ท่อ" สตรีมที่แบ็กเอนด์ แต่มันมาจากส่วนหน้า
mattdlockyer

3

สำหรับผู้ที่บ่นว่าเมื่อพวกเขาใช้ฟังก์ชันอัปโหลด s3 api และไฟล์ศูนย์ไบต์จะลงเอยที่ s3 (@ Radar155 และ @gabo) - ฉันก็มีปัญหานี้เช่นกัน

สร้างสตรีม PassThrough ที่สองและเพียงแค่ไพพ์ข้อมูลทั้งหมดจากที่หนึ่งไปยังวินาทีและส่งต่อการอ้างอิงจากวินาทีนั้นไปยัง s3 คุณสามารถทำได้หลายวิธี - อาจเป็นวิธีที่สกปรกคือการฟังเหตุการณ์ "ข้อมูล" ในสตรีมแรกจากนั้นเขียนข้อมูลเดียวกันนั้นไปยังสตรีมที่สองซึ่งในทำนองเดียวกันสำหรับเหตุการณ์ "สิ้นสุด" - เพียงแค่โทร ฟังก์ชันสิ้นสุดในสตรีมที่สอง ฉันไม่รู้ว่านี่เป็นข้อผิดพลาดใน aws api เวอร์ชันของโหนดหรือปัญหาอื่น ๆ - แต่มันช่วยแก้ปัญหาให้ฉันได้

นี่คือลักษณะ:

var PassThroughStream = require('stream').PassThrough;
var srcStream = new PassThroughStream();

var rstream = fs.createReadStream('Learning/stocktest.json');
var sameStream = rstream.pipe(srcStream);
// interesting note: (srcStream == sameStream) at this point
var destStream = new PassThroughStream();
// call your s3.upload function here - passing in the destStream as the Body parameter
srcStream.on('data', function (chunk) {
    destStream.write(chunk);
});

srcStream.on('end', function () {
    dataStream.end();
});

สิ่งนี้ใช้ได้ผลกับฉันเช่นกัน ฟังก์ชั่นการอัปโหลด S3 ทำเพียงแค่ "ตาย" แบบเงียบ ๆ เมื่อใดก็ตามที่มีการใช้การอัปโหลดหลายส่วน แต่เมื่อใช้โซลูชันของคุณก็ใช้ได้ดี (!) ขอบคุณ! :)
jhdrn

คุณช่วยให้ข้อมูลว่าทำไมจึงต้องสตรีมครั้งที่สอง
noob7

2

ทำตามคำตอบอื่น ๆ และใช้ AWS SDK ล่าสุดสำหรับ Node.js มีวิธีแก้ปัญหาที่สะอาดและง่ายกว่ามากเนื่องจากฟังก์ชั่น s3 upload () ยอมรับสตรีมโดยใช้ await syntax และสัญญาของ S3:

var model = await s3Client.upload({
    Bucket : bucket,
    Key : key,
    ContentType : yourContentType,
    Body : fs.createReadStream(path-to-file)
}).promise();

สิ่งนี้ใช้ได้กับกรณีการใช้งานเฉพาะของ "การอ่านไฟล์ขนาดใหญ่มาก" ที่ผู้เขียนกล่าวถึง แต่คำตอบอื่น ๆ ยังใช้ได้หากคุณใช้สตรีมนอกบริบทของไฟล์ (เช่นพยายามเขียนสตรีมเคอร์เซอร์ mongo ไปที่ s3 โดยที่คุณยังต้องใช้สตรีม PassThrough + ไปป์)
Ken Colton

0

ฉันใช้ KnexJS และมีปัญหาในการใช้ API การสตรีม ในที่สุดฉันก็แก้ไขได้แล้วหวังว่าสิ่งต่อไปนี้จะช่วยใครบางคนได้

const knexStream = knex.select('*').from('my_table').stream();
const passThroughStream = new stream.PassThrough();

knexStream.on('data', (chunk) => passThroughStream.write(JSON.stringify(chunk) + '\n'));
knexStream.on('end', () => passThroughStream.end());

const uploadResult = await s3
  .upload({
    Bucket: 'my-bucket',
    Key: 'stream-test.txt',
    Body: passThroughStream
  })
  .promise();

-3

หากคุณทราบขนาดของสตรีมคุณสามารถใช้minio-jsเพื่ออัปโหลดสตรีมได้ดังนี้:

  s3Client.putObject('my-bucketname', 'my-objectname.ogg', stream, size, 'audio/ogg', function(e) {
    if (e) {
      return console.log(e)
    }
    console.log("Successfully uploaded the stream")
  })
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.