generating and serving static files with Meteor

2019-01-10 18:13发布

问题:

I'm looking to create static text files based upon the content of a supplied object, which can then be downloaded by the user. Here's what I was planning on doing:

  1. When the user hits 'export' the application calls a Meteor.method() which, in turn, parses and writes the file to the public directory using typical Node methods.

  2. Once the file is created, in the callback from Meteor.method() I provide a link to the generated file. For example, 'public/userId/file.txt'. The user can then choose to download the file at that link.

  3. I then use Meteor's Connect modele (which it uses internally) to route any requests to the above URL to the file itself. I could do some permissions checking based on the userId and the logged in state of the user.

The problem: When static files are generated in public, the web page automatically reloads each time. I thought that it might make more sense to use something like Express to generate a REST endpoint, which could deal with creating the files. But then I'm not sure how to deal with permissions if I don't have access to the Meteor session data.

Any ideas on the best strategy here?

回答1:

The symlink hack will no longer work in Meteor (from 0.6.5). Instead I suggest creating a package with similar code to the following:

packge.js

Package.describe({
  summary: "Application file server."
});

Npm.depends({
  connect: "2.7.10"
});

Package.on_use(function(api) {
  api.use(['webapp', 'routepolicy'], 'server');

  api.add_files([
    'app-file-server.js',
  ], 'server'); 
});

app-file-server.js

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

RoutePolicy.declare('/my-uploaded-content', 'network');

// Listen to incoming http requests
WebApp.connectHandlers
  .use('/my-uploaded-content', connect.static(process.env['APP_DYN_CONTENT_DIR']));


回答2:

In version 0.6.6.3 0.7.x - 1.3.x you can do the following:

To write

var fs = Npm.require('fs');
var filePath = process.env.PWD + '/.uploads_dir_on_server/' + fileName;
fs.writeFileSync(filePath, data, 'binary');

To serve

In vanilla meteor app

var fs = Npm.require('fs');
WebApp.connectHandlers.use(function(req, res, next) {
    var re = /^\/uploads_url_prefix\/(.*)$/.exec(req.url);
    if (re !== null) {   // Only handle URLs that start with /uploads_url_prefix/*
        var filePath = process.env.PWD + '/.uploads_dir_on_server/' + re[1];
        var data = fs.readFileSync(filePath);
        res.writeHead(200, {
                'Content-Type': 'image'
            });
        res.write(data);
        res.end();
    } else {  // Other urls will have default behaviors
        next();
    }
});

When using iron:router

This should be a server side route (ex: defined in a file in /server/ folder)

Edit (2016-May-9)

var fs = Npm.require('fs');
Router.route('uploads', {
       name: 'uploads',
       path: /^\/uploads_url_prefix\/(.*)$/,
       where: 'server',
       action: function() {
           var filePath = process.env.PWD + '/.uploads_dir_on_server/' + this.params[0];
           var data = fs.readFileSync(filePath);
           this.response.writeHead(200, {
               'Content-Type': 'image'
           });
           this.response.write(data);
           this.response.end();
       }
    });

Outdated format:

Router.map(function() {
    this.route('serverFile', {
        ...// same as object above
    }
});

Notes

  • process.env.PWD will give you the project root
  • if you plan to put files inside your project

    • don't use the public or private meteor folders
    • use dot folders (eg. hidden folders ex: .uploads)

    Not respecting these two will cause local meteor to restart on every upload, unless you run your meteor app with: meteor run --production

  • I've used this approach for a simple image upload & serve (based on dario's version)
  • Should you wish for more complex file management please consider CollectionFS


回答3:

I was stuck at the exact same problem, where i need the users to upload files in contrast to your server generated files. I solved it sort of by creating an "uploads" folder as sibling to the "client public server" on the same folder level. and then i created a simbolic link to the '.meteor/local/build/static' folder like

ln -s ../../../../uploads .meteor/local/build/static/ 

but with nodejs filesystem api at server start time

Meteor.startup(function () {
    var fs = Npm.require('fs');

    fs.symlinkSync('../../../../uploads', '.meteor/local/build/static/uploads'); 
};

in your case you may have a folder like "generatedFiles" instead of my "uploads" folder you need to do this every time the server starts up cuz these folders are generated every time the server starts up e.g. a file changes in your implementation.



回答4:

Another option is to use a server side route to generate the content and send it to the user's browser for download. For example, the following will look up a user by ID and return it as JSON. The end user is prompted to save the response to a file with the name specified in the Content-Disposition header. Other headers, such as Expires, could be added to the response as well. If the user does not exist, a 404 is returned.

Router.route("userJson", {
    where: "server",

    path: "/user-json/:userId",

    action: function() {
        var user = Meteor.users.findOne({ _id: this.params.userId });

        if (!user) {
            this.response.writeHead(404);
            this.response.end("User not found");
            return;
        }

        this.response.writeHead(200, {
            "Content-Type": "application/json",
            "Content-Disposition": "attachment; filename=user-" + user._id + ".json"
        });
        this.response.end(JSON.stringify(user));
    }
});

This method has one big downside, however. Server side routes do not provide an easy way to get the currently logged in user. See this issue on GitHub.