Meteor-Files

repository·master·Indexed 22 days ago

https://github.com/veliovgroup/meteor-files

A comprehensive file management package for Meteor.js that uses MongoDB Collections for metadata. It supports AWS S3, Google Cloud Storage, Dropbox, Google Drive, and GridFS backends. Key features include HTTP/DDP transports, pause/resume functionality, parallel multi-stream async uploads, and custom download routes with RFC 2616 compliance. The API is centered around FilesCollection, FileCursor, and FilesCursor for managing file uploads, retrieval, and removal.

Tokens
46.5K
Snippets
112
Records
157
Agent score
76%

What's inside ostrio-meteor-files

  1. Overview of Meteor-Files features

    master

    Meteor-Files is a library for Meteor.js designed for robust file upload and management. Key capabilities include:

    • Advanced Uploads: Supports HTTP or DDP transports, optimized RAM usage for large files, pause/resume functionality, auto-pause on connection loss, and parallel multi-stream async uploads. It also supports non-Latin file names.
    • Storage Flexibility: Integrates with third-party storage like AWS S3, Dropbox, Google Cloud Storage, and Google Drive. It also supports GridFS (both via GridFSBucket and the legacy gridfs-stream).
    • File Serving: Provides custom download routes with support for progressive (chunked) downloads, correct mime-type and Content-Range headers, and proper 206 and 416 HTTP responses following RFC 2616.
    • File System Integration: Automatically writes files to the file system and a special Collection. Users can control path, collection name, schema, chunk size, and naming functions.
    • Cordova Support: Works in Cordova apps using FileReader for uploading and reading files.
    • Post-processing: Supports file subversions like thumbnails, audio/video format conversions, and revisions.
  2. Use Lifecycle Hooks for Authorization

    master

    You can use lifecycle hooks within the FilesCollection configuration to implement fine-grained authorization logic.

    onBeforeUpload

    Used to validate a file before it is written to the filesystem. If the function returns false or a string, the upload is rejected. This is useful for checking file size, extension, or user roles.

    onBeforeRemove

    Used to prevent unauthorized file deletions. If the function returns false, the removal is aborted.

    onAfterUpload

    Triggered immediately after a file is written to the filesystem. This is an isomorphic event. It is highly recommended to use this hook on the server to verify the actual file content (e.g., using mmmagic to check the real MIME type) to prevent extension/MIME-type spoofing.

    // Example: Authorization via onBeforeUpload
    const imagesCollection = new FilesCollection({
      collectionName: 'images',
      allowClientCode: true,
      async onBeforeUpload() {
        if (this.userId) {
          const user = await this.userAsync();
          if (user.profile.role === 'admin') {
            return true;
          }
        }
        return 'Not enough rights to upload a file!';
      }
    });
    
    // Example: Security via onAfterUpload (Server-side MIME check)
    const imagesCollection = new FilesCollection({
      collectionName: 'images',
      onAfterUpload(file) {
        if (Meteor.isServer) {
          const { Magic, MAGIC_MIME_TYPE } = require('mmmagic');
          const magic = new Magic(MAGIC_MIME_TYPE);
          magic.detectFile(file.path, Meteor.bindEnvironment((err, mimeType) => {
            if (err || !mimeType.includes('image')) {
              this.remove(file._id);
            }
          }));
        }
      }
    });
  3. Pipe data during upload

    master

    If you call insert() with the second argument set to false (disabling autoStart), you can use the .pipe() method to transform file data before it is uploaded.

    Each .pipe(fn) call passes the current chunk (as a Base64 string/DataURL) to the function fn. The function must return a Base64 string. This is useful for client-side encryption or compression.

    Note: If you modify the data, you may need to manually update the file name, extension, or mime-type if the transformation changes them.

    import { imagesCollection } from '/imports/collections/images.js';
    
    const encrypt = function encrypt(data) {
      // data is a Base64 string
      return encryptAndReturnAsBase64(data);
    };
    
    const zip = function zip(data) {
      return zipAndReturnAsBase64(data);
    };
    
    // Usage: pipe through multiple functions
    imagesCollection.insert({
      file: fileElement,
      chunkSize: 'dynamic'
    }, false) // false = do not auto-start
      .pipe(encrypt)
      .pipe(zip)
      .start();
  4. Configure default file storage paths

    master

    By default, if config.storagePath is not specified in the Constructor options, files are stored in assets/app/uploads relative to the running script.

    Important Storage Behaviors:

    • Development: Files are stored in yourDevAppDir/.meteor/local/build/programs/server. Warning: All files will be deleted when the application rebuilds or you run meteor reset. To ensure persistence during development, use an absolute path outside your project folder (e.g., /data).
    • Production: Files are stored in yourProdAppDir/programs/server. If using MeteorUp (MUP), you must add Docker volumes to your mup.json configuration.
  5. Understand the FileCursor class

    master

    The FileCursor class represents an individual file record. It is the object returned when you use FilesCollection#findOne() or when iterating through results using FilesCursor#each().

    Because FileCursor wraps the file document, all original properties of the document are accessible directly by name on the FileCursor instance (e.g., fileCursor.name or fileCursor.size).

    import { FilesCollection } from 'meteor/ostrio:files';
    
    const imagesCollection = new FilesCollection();
    const cursor = await imagesCollection.findOneAsync(); // Returns a FileCursor instance
    console.log(cursor.name); // Access properties directly
  6. Compare upload Transports: DDP, HTTP, and RTC Data Channel

    master

    The ostrio-meteor-files package supports different transport mechanisms for uploading files, each with different performance characteristics and use cases:

    DDP (WebSockets)

    Uses the Meteor Distributed Data Protocol via SockJS. It is the default way Meteor communicates with the server.

    • Pros: Persistent connection; standard Meteor communication.
    • Cons: Synchronous; high overhead due to EJSON; blocks other DDP requests (methods, subscriptions) during transfer.

    HTTP (TCP/IP)

    Standard web protocol. Recommended to use with HTTP/2, SSL/TLS, and SSL session caching for optimal performance.

    • Pros: Asynchronous and simultaneous requests; no data-processing/encoding overhead (sends data as-is); high speed.
    • Cons: Originally designed for text-based data (hypertext) rather than binary files.

    RTC Data Channel (UDP)

    Note: This is currently only available in the webrtc-data-channel branch and is in testing mode.

    • Pros: Single socket; direct client-to-server tunneled connection; pure binary support; native mobile support; uses UDP.
    • Cons: No support in mobile browsers; chunk size is limited to 64KB.
  7. Implement GCS upload and download interception

    master

    To fully offload storage to GCS, implement the following hooks in your FilesCollection configuration:

    1. Uploading to GCS (onAfterUpload)

    Use the onAfterUpload callback to upload the local file to your bucket. It is recommended to store the GCS path in the file version's metadata (e.g., versions[version].meta.pipePath) so it can be retrieved later. After a successful upload, use this.unlink() to remove the original file from the local filesystem.

    2. Streaming from GCS (interceptDownload)

    Use the interceptDownload hook to check if a file has a pipePath (indicating it is stored in GCS). If it does, create a readable stream from the GCS bucket using bucket.file(path).createReadStream() and pass it to this.serve(). Return true to signal that the download was handled.

    3. Deleting from GCS

    To ensure files are removed from GCS when deleted from the collection, intercept the remove method of your FilesCollection to call bucket.file(path).delete() for each version's pipePath before calling the original removal method.

    Collections.files = new FilesCollection({
      onAfterUpload(fileRef) {
        _.each(fileRef.versions, (vRef, version) => {
          const filePath = 'files/' + (Random.id()) + '-' + version + '.' + fileRef.extension;
          const options = { destination: filePath, resumable: true };
    
          bucket.upload(fileRef.path, options, (error, file) => {
            if (!error) {
              // Update metadata with the GCS path
              this.collection.update(
                { _id: fileRef._id },
                { $set: { [`versions.${version}.meta.pipePath`]: filePath } },
                () => this.unlink(fileRef, version)
              );
            }
          });
        });
      },
      interceptDownload(http, fileRef, version) {
        const path = fileRef.versions[version]?.meta?.pipePath;
        if (path) {
          const remoteReadStream = getReadableStream(http, path, fileRef.versions[version]);
          this.serve(http, fileRef, fileRef.versions[version], version, remoteReadStream);
          return true;
        }
        return false;
      }
    });
  8. Core API Abstractions: FilesCollection, FileCursor, and FilesCursor

    master

    The Meteor-Files API is built around three primary classes:

    1. FilesCollection: The main entry point used to initialize a file collection. It can be configured with schemas (e.g., SimpleSchema), access rules (allow/deny), and lifecycle hooks (onBeforeUpload, onBeforeRemove).
    2. FileCursor: Returned by .findOne(). It represents a single file document and provides methods to interact with it:
      • link(): Returns a downloadable URL.
      • get(property): Returns the document as a plain object.
      • removeAsync(): Removes the document (returns a Promise<number>).
      • fetchAsync(): Resolves to the document as a plain object in an Array.
      • with(): Returns a reactive version of the cursor.
    3. FilesCursor: Returned by .find(). It represents a set of matching documents and provides methods for bulk operations:
      • fetchAsync(): Returns all matching documents as an Array.
      • countAsync(): Returns the number of matching documents.
      • removeAsync(): Removes all matching documents.
      • forEachAsync(callback, context): Iterates over matching documents.
      • eachAsync(): Resolves to an Array of FileCursor instances for each document.
  9. Understand the relationship between FilesCollection and Mongo.Collection

    master

    A FilesCollection instance contains a direct reference to its underlying Mongo.Collection. Conversely, the Mongo.Collection maintains a back-reference to the FilesCollection that created it via the .filesCollection property. This allows you to navigate from the file management layer down to the database layer and back up to the file management layer.

    const imagesCollection = new FilesCollection({collectionName: 'images'});
    
    // get the underlying Mongo.Collection
    const collection = imagesCollection.collection;
    
    // get the 'parent' FilesCollection of this collection instance
    const parent = collection.filesCollection;
    
    // returns true
    console.log(parent === imagesCollection);
  10. Use the public option for web-server file serving

    master

    Setting config.public to true allows your web-server (like Nginx or Apache) to serve uploaded files directly, which is more efficient than serving them through the Meteor process.

    When using public: true, you must follow these requirements:

    1. downloadRoute: Must be explicitly provided and point to the root of your web/proxy-server (e.g., '/uploads/').
    2. storagePath: Must be an absolute path to the public directory of your web/proxy-server (e.g., '/var/www/myapp/public/uploads/').
    3. Integrity: integrityCheck is not guaranteed when using this mode.
    4. Exclusivity: You cannot use protected: true if public: true is set.
  11. Upload a file using `insert()`

    master

    To upload a file, call insert() on your FilesCollection instance. You can pass an options object to configure the upload behavior.

    Basic Usage

    imagesCollection.insert({
      file: fileElement, // The File object from an input
      chunkSize: 'dynamic'
    });

    Uploading Base64 strings

    If you are uploading a Base64 string instead of a File object, you must set isBase64: true and provide a fileName.

    As DataURL:

    imagesCollection.insert({
      file: 'data:image/png;base64,ivbor...', 
      isBase64: true, 
      fileName: 'pic.png'
    });

    As plain Base64:

    imagesCollection.insert({
      file: 'base64str...', 
      isBase64: true, 
      fileName: 'pic.png', 
      type: 'image/png'
    });
    import { FilesCollection } from 'meteor/ostrio:files';
    const imagesCollection = new FilesCollection({collectionName: 'images'});
    
    // Standard file upload
    imagesCollection.insert({
      file: e.currentTarget.files[0],
      chunkSize: 'dynamic'
    });
    
    // Base64 upload
    imagesCollection.insert({
      file: 'base64str...', 
      isBase64: true, 
      fileName: 'pic.png', 
      type: 'image/png'
    });
  12. Install image processing libraries for thumbnail generation

    master

    To generate thumbnails after file uploads, you must install a CLI image processing tool and its corresponding NPM wrapper.

    1. Install CLI Tools

    Install either GraphicsMagick (recommended for performance) or ImageMagick on your host machine.

    macOS (Homebrew):

    brew install graphicsmagick
    # or
    brew install imagemagick

    2. Install NPM Packages

    Install the ostrio:files Meteor package, along with gm (GraphicsMagick wrapper) and fs-extra.

    meteor add ostrio:files
    meteor npm install --save gm fs-extra

    3. Configure ImageMagick usage (Optional)

    If you are using ImageMagick instead of GraphicsMagick, you must explicitly tell the gm package to use the ImageMagick subclass:

    const gm = require('gm');
    const im = gm.subClass({ imageMagick: true });
    brew install graphicsmagick
    # or for ImageMagick:
    # brew install imagemagick
    
    meteor add ostrio:files
    meteor npm install --save gm fs-extra