20

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?

bento
  • 4,846
  • 8
  • 41
  • 59
  • I think there is a way to stop observing certain directories...which would keep meteor from updating when contents in that specific directory changes. – Pastor Bones Nov 03 '12 at 07:21
  • THanks Pastor. I've decided to adopt client-side technologies for creating and saving files. window.saveAs() (and other workarounds) are available for creating files. – bento Nov 04 '12 at 16:59

4 Answers4

25

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
Matyas
  • 13,473
  • 3
  • 60
  • 73
  • so if I have many users that have their own files, can will I run into issues with this dir structure? \Users\user_name\created_file.txt – Aaron Dec 22 '14 at 16:57
  • I want the users to be able to d/l a pdf once I create it, but not be able to d/l any file from that dir. Also, I don't want anyone else to be able to d/l that file except the right user. Basically I won't the files accessible to everyone, just the user, and just the file for that user. – Aaron Dec 22 '14 at 17:11
  • This is something you'll have to verify/implement yourself. Basically you'll have to: 1. Identify the user that is initiating the request. 2. Check his permissions to the requested resource. 3. Deliver it if all information is ok and return HTTP 403 Forbidden otherwise. (This is how I would tackle the issue) – Matyas Dec 23 '14 at 11:49
  • @Matyas, thanks. I'm having trouble getting this pdf I have in my server/.files/users/some_user_id/test.pdf to render. The action function never executes when I go to localhost:3000/serve-file/.files/users/some_user_id/test.pdf. I'm using the latest IR. Router.route('serve-file', {where: 'server', path: 'don't understand this', action: function () { var filePath=process.env.PWD + '/.files/users/' + this.userId + '/' + this.params[1]; var data = fs.readFileSync(filePath); this.response.writeHead(200, { 'Content-Type': 'image'}); this.response.write(data); this.response.end(); – Aaron Dec 26 '14 at 12:58
  • @Aaron Because the path you've provided (`'don't understand this'`) is not a valid path pattern. You are basically not mapping your handler to a path – Matyas Dec 26 '14 at 22:36
  • Thanks again @Matyas. My path to the file is /server/.files/users/test.pdf – Aaron Dec 26 '14 at 22:39
  • I'd suggest using fs.realpathSync('.') instead of process.env.PWD. Former works also in production, latter only on local. – mhlavacka Mar 28 '16 at 20:05
16

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']));
Jacott
  • 695
  • 6
  • 7
  • As of now, this should be the right answer. You also might add that `/my-uploaded-content` is the url-path and `process.env['APP_DYN_CONTENT_DIR']` is a root path directory – nooitaf Sep 18 '13 at 11:05
  • How can I manage caching with this solution? I'd like to have a unlimited caching + a hash added to the name of the image. – mquandalle Oct 26 '13 at 14:46
5

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.

Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
dustin.b
  • 1,275
  • 14
  • 33
1

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.

Sean
  • 4,365
  • 1
  • 27
  • 31