Câu hỏi Meteor: tải tệp từ máy khách lên bộ sưu tập Mongo vs hệ thống tệp vs GridFS


Meteor là tuyệt vời nhưng nó thiếu hỗ trợ cho việc tải lên tệp truyền thống. Có một số tùy chọn để xử lý việc tải lên tệp:

Từ khách hàng, dữ liệu có thể được gửi bằng:

  • Meteor.call ('saveFile', data) hoặc collection.insert ({tệp: dữ liệu})
  • Biểu mẫu 'POST' hoặc HTTP.call ('POST')

Trong máy chủ, tệp có thể được lưu vào:

  • một bộ sưu tập tập tin mongodb bởi collection.insert ({file: data})
  • hệ thống tệp trong / path / to / dir
  • mongodb GridFS

Những ưu và nhược điểm của những phương pháp này là gì và cách tốt nhất để thực hiện chúng? Tôi biết rằng cũng có các tùy chọn khác như lưu vào trang web của bên thứ ba và nhận được url.


35
2018-01-14 00:50


gốc




Các câu trả lời:


Bạn có thể tải tệp lên khá đơn giản bằng Meteor mà không cần sử dụng bất kỳ gói nào khác hoặc bên thứ ba

 Tùy chọn 1: DDP, lưu tệp vào bộ sưu tập mongo

/*** client.js ***/

// asign a change event into input tag
'change input' : function(event,template){ 
    var file = event.target.files[0]; //assuming 1 file only
    if (!file) return;

    var reader = new FileReader(); //create a reader according to HTML5 File API

    reader.onload = function(event){          
      var buffer = new Uint8Array(reader.result) // convert to binary
      Meteor.call('saveFile', buffer);
    }

    reader.readAsArrayBuffer(file); //read the file as arraybuffer
}

/*** server.js ***/ 

Files = new Mongo.Collection('files');

Meteor.methods({
    'saveFile': function(buffer){
        Files.insert({data:buffer})         
    }   
});

Explantion

Đầu tiên, tệp được lấy từ đầu vào bằng cách sử dụng API tệp HTML5. Một trình đọc được tạo bằng FileReader mới. Tệp được đọc dưới dạng readAsArrayBuffer. Bộ đệm mảng này, nếu bạn console.log, trả về {} và DDP không thể gửi nó qua dây, vì vậy nó phải được chuyển thành Uint8Array.

Khi bạn đặt nó trong Meteor.call, Meteor sẽ tự động chạy EJSON.stringify (Uint8Array) và gửi nó với DDP. Bạn có thể kiểm tra dữ liệu trong lưu lượng truy cập websocket của bảng điều khiển chrome, bạn sẽ thấy một chuỗi giống base64

Về phía máy chủ, Meteor gọi EJSON.parse () và chuyển nó trở lại bộ đệm

Ưu điểm

  1. Đơn giản, không có cách hacky, không có gói thêm
  2. Dính vào dữ liệu trên nguyên tắc dây

Nhược điểm

  1. Băng thông nhiều hơn: chuỗi base64 kết quả lớn hơn ~ 33% so với tệp gốc
  2. Giới hạn kích thước tệp: không thể gửi tệp lớn (giới hạn ~ 16 MB?)
  3. Không có bộ nhớ đệm
  4. Chưa có gzip hoặc nén
  5. Mất nhiều bộ nhớ nếu bạn xuất bản tệp

 Tùy chọn 2: XHR, đăng từ máy khách lên hệ thống tệp

/*** client.js ***/

// asign a change event into input tag
'change input' : function(event,template){ 
    var file = event.target.files[0]; 
    if (!file) return;      

    var xhr = new XMLHttpRequest(); 
    xhr.open('POST', '/uploadSomeWhere', true);
    xhr.onload = function(event){...}

    xhr.send(file); 
}

/*** server.js ***/ 

var fs = Npm.require('fs');

//using interal webapp or iron:router
WebApp.connectHandlers.use('/uploadSomeWhere',function(req,res){
    //var start = Date.now()        
    var file = fs.createWriteStream('/path/to/dir/filename'); 

    file.on('error',function(error){...});
    file.on('finish',function(){
        res.writeHead(...) 
        res.end(); //end the respone 
        //console.log('Finish uploading, time taken: ' + Date.now() - start);
    });

    req.pipe(file); //pipe the request to the file
});

Giải trình 

Tệp trong máy khách được lấy, một đối tượng XHR được tạo và tệp được gửi qua 'POST' tới máy chủ.

Trên máy chủ, dữ liệu được đưa vào hệ thống tệp cơ bản. Bạn cũng có thể xác định tên tệp, thực hiện vệ sinh hoặc kiểm tra xem nó có tồn tại trước khi lưu hay không.

Ưu điểm

  1. Tận dụng lợi thế của XHR 2 để bạn có thể gửi arraybuffer, không cần FileReader () mới so với tùy chọn 1
  2. Arraybuffer ít cồng kềnh so với chuỗi base64
  3. Không giới hạn kích thước, tôi đã gửi một tệp ~ 200 MB trong localhost mà không gặp vấn đề gì
  4. Hệ thống tệp nhanh hơn mongodb (phần lớn sau này trong điểm chuẩn dưới đây)
  5. Cachable và gzip

Nhược điểm

  1. XHR 2 không khả dụng trong các trình duyệt cũ hơn, ví dụ: dưới IE10, nhưng tất nhiên bạn có thể thực hiện một bài đăng truyền thống <form> Tôi chỉ sử dụng xhr = new XMLHttpRequest (), chứ không phải HTTP.call ('POST') vì HTTP.call hiện tại trong Meteor vẫn chưa thể gửi arraybuffer (chỉ cho tôi nếu tôi sai).
  2. / path / to / dir / phải ở bên ngoài thiên thạch, nếu không viết một tập tin trong / công kích hoạt một tải lại

Tùy chọn 3: XHR, lưu vào GridFS

/*** client.js ***/

//same as option 2


/*** version A: server.js ***/  

var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
var GridStore = MongoInternals.NpmModule.GridStore;

WebApp.connectHandlers.use('/uploadSomeWhere',function(req,res){
    //var start = Date.now()        
    var file = new GridStore(db,'filename','w');

    file.open(function(error,gs){
        file.stream(true); //true will close the file automatically once piping finishes

        file.on('error',function(e){...});
        file.on('end',function(){
            res.end(); //send end respone
            //console.log('Finish uploading, time taken: ' + Date.now() - start);
        });

        req.pipe(file);
    });     
});

/*** version B: server.js ***/  

var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
var GridStore = Npm.require('mongodb').GridStore; //also need to add Npm.depends({mongodb:'2.0.13'}) in package.js

WebApp.connectHandlers.use('/uploadSomeWhere',function(req,res){
    //var start = Date.now()        
    var file = new GridStore(db,'filename','w').stream(true); //start the stream 

    file.on('error',function(e){...});
    file.on('end',function(){
        res.end(); //send end respone
        //console.log('Finish uploading, time taken: ' + Date.now() - start);
    });
    req.pipe(file);
});     

Giải trình 

Kịch bản máy khách giống như trong tùy chọn 2.

Theo Meteor 1.0.x mongo_driver.js dòng cuối cùng, một đối tượng toàn cầu được gọi là MongoInternals được trưng ra, bạn có thể gọi defaultRemoteCollectionDriver () để trả về đối tượng db cơ sở dữ liệu hiện tại được yêu cầu cho GridStore. Trong phiên bản A, GridStore cũng được hiển thị bởi MongoInternals. Mongo được sử dụng bởi meteor hiện tại là v1.4.x

Sau đó, bên trong một route, bạn có thể tạo một đối tượng write mới bằng cách gọi var file = new GridStore (...) (API). Sau đó, bạn mở tệp và tạo luồng.

Tôi cũng bao gồm một phiên bản B. Trong phiên bản này, GridStore được gọi là sử dụng một ổ đĩa mongodb mới thông qua Npm.require ('mongodb'), mongo này là v2.0.13 mới nhất như của bài viết này. Cái mới API không yêu cầu bạn mở tệp, bạn có thể gọi trực tiếp luồng (true) và bắt đầu đường ống

Ưu điểm

  1. Giống như trong tùy chọn 2, được gửi bằng cách sử dụng bộ đệm mảng, chi phí thấp hơn so với chuỗi base64 trong tùy chọn 1
  2. Không cần phải lo lắng về việc vệ sinh tên tập tin
  3. Tách từ hệ thống tệp, không cần phải ghi vào thư mục tạm thời, db có thể được sao lưu, đại diện, phân đoạn, v.v ...
  4. Không cần phải thực hiện bất kỳ gói nào khác
  5. Có thể lưu và có thể được nén
  6. Lưu trữ kích thước lớn hơn nhiều so với bộ sưu tập mongo bình thường
  7. Sử dụng đường ống để giảm tình trạng quá tải bộ nhớ

Nhược điểm

  1. Lưới điện Mongo không ổn định. Tôi bao gồm phiên bản A (mongo 1.x) và B (mongo 2.x). Trong phiên bản A, khi đường ống các tệp lớn> 10 MB, tôi nhận được rất nhiều lỗi, bao gồm tệp bị hỏng, đường ống chưa hoàn thành. Vấn đề này được giải quyết trong phiên bản B sử dụng mongo 2.x, hy vọng sao băng sẽ nâng cấp lên mongodb 2.x sớm
  2. Sự nhầm lẫn API. Trong phiên bản A, bạn cần mở tệp trước khi có thể phát trực tiếp, nhưng trong phiên bản B, bạn có thể phát trực tuyến mà không cần gọi mở. Tài liệu API cũng không rõ ràng và luồng không phải là 100% cú pháp có thể trao đổi với Npm.require ('fs'). Trong fs, bạn gọi file.on ('finish') nhưng trong GridFS bạn gọi file.on ('end') khi viết xong / kết thúc.
  3. GridFS không cung cấp viết atomicity, vì vậy nếu có nhiều đồng thời ghi vào cùng một tập tin, kết quả cuối cùng có thể rất khác nhau
  4. Tốc độ. Mongo GridFS chậm hơn nhiều so với hệ thống tệp.

Điểm chuẩn Bạn có thể thấy trong tùy chọn 2 và tùy chọn 3, tôi bao gồm var start = Date.now () và khi viết kết thúc, tôi console.log ra thời gian trong , dưới đây là kết quả. Lõi kép, RAM 4 GB, HDD, ubuntu dựa trên 14.04.

file size   GridFS  FS
100 KB      50      2
1 MB        400     30
10 MB       3500    100
200 MB      80000   1240

Bạn có thể thấy rằng FS nhanh hơn nhiều so với GridFS. Đối với một tập tin 200 MB, nó mất ~ 80 giây bằng cách sử dụng GridFS nhưng chỉ ~ 1 giây trong FS. Tôi chưa thử SSD, kết quả có thể khác. Tuy nhiên, trong thực tế, băng thông có thể quyết định tốc độ truyền tải của tập tin từ máy khách đến máy chủ, đạt tốc độ truyền 200 MB / giây không phải là điển hình. Mặt khác, tốc độ truyền ~ 2 MB / giây (GridFS) là tiêu chuẩn nhiều hơn.

Phần kết luận

Không có nghĩa là toàn diện, nhưng bạn có thể quyết định lựa chọn nào là tốt nhất cho nhu cầu của bạn.

  • DDP là đơn giản nhất và bám vào nguyên tắc Meteor lõi nhưng dữ liệu thì cồng kềnh hơn, không nén trong quá trình truyền, không thể lưu vào bộ đệm. Nhưng tùy chọn này có thể tốt nếu bạn chỉ cần các tệp nhỏ.
  • XHR kết hợp với hệ thống tệp là cách 'truyền thống'. API ổn định, nhanh, 'có thể phát trực tuyến', có thể nén, có thể lưu vào bộ nhớ cache (ETag v.v.), nhưng cần phải nằm trong một thư mục riêng biệt
  • XHR kết hợp với GridFS, bạn sẽ có được lợi ích của bộ đại diện, khả năng mở rộng, không có hệ thống tập tin thư mục chạm, các tệp lớn và nhiều tệp nếu hệ thống tệp hạn chế số, cũng có thể lưu vào bộ nhớ cache. Tuy nhiên, API không ổn định, bạn gặp lỗi trong nhiều lần viết, đó là s..l..o..w ..

Hy vọng rằng sớm, sao băng DDP có thể hỗ trợ gzip, bộ nhớ đệm, vv và GridFS có thể được nhanh hơn...


71
2018-01-14 00:50



Yêu những cách tiếp cận này !! Tôi chỉ có một câu hỏi liên quan đến sắt: bộ định tuyến - khi tôi cố gắng this.request.pipe(file); Tôi nhận được một TypeError: Cannot call method 'pipe' of undefined bạn có biết tại sao điều này có thể xảy ra không? - lol
Tôi đã không thử với Iron: router, xin lỗi. Có một WebApp.rawConnectHandlers mà xảy ra trước khi WebApp.connectHandlers, có thể cung cấp cho một thử? - Green
Không sao đâu - Tôi rất phấn khởi khi bạn đề cập đến nó trong câu trả lời của bạn. WebApp có phải là một gói hay ... Nó được dùng để làm gì? (Tôi đang dần dần nhận được đầu của tôi xung quanh API rộng lớn mà đi kèm với sao băng) :) - lol
WebApp là một trong các gói mặc định github.com/meteor/meteor/tree/devel/packages/webapp  meteor.com/webapp Vốn có, nó sử dụng kết nối js github.com/senchalabs/connect - Green
Ahh vâng! Các Connect API là chìa khóa! Nó cung cấp các máy móc trực tuyến. Và trong sắt: router nó được gọi là (?) Với onBeforeAction: function(req, res, next). Tôi có nên thêm câu trả lời này làm câu trả lời, thêm câu trả lời vào câu trả lời của bạn hoặc để lại câu trả lời này trong các nhận xét không? - lol


Xin chào chỉ để thêm vào Option1 liên quan đến việc xem tập tin. Tôi đã làm điều đó mà không có ejson.

<template name='tryUpload'>
  <p>Choose file to upload</p>
  <input name="upload" class='fileupload' type='file'>
</template>

Template.tryUpload.events({
'change .fileupload':function(event,template){
console.log('change & view');
var f = event.target.files[0];//assuming upload 1 file only
if(!f) return;
var r = new FileReader();
r.onload=function(event){
  var buffer = new Uint8Array(r.result);//convert to binary
  for (var i = 0, strLen = r.length; i < strLen; i++){
    buffer[i] = r.charCodeAt(i);
  }
  var toString = String.fromCharCode.apply(null, buffer );
  console.log(toString);
  //Meteor.call('saveFiles',buffer);
}
r.readAsArrayBuffer(f);};

0
2018-06-26 09:23