Refresh after CollectionFS Upload

Hi everyone,

Small issue that Google and StackOverflow can’t seem to help with. I’m using CollectionFS for all my image uploads, more specifically using GridFS for now. I may switch to S3 / FileSystem later if they prove to be better.

Anyway, the problem I’m having is that when the upload is complete, the new image isn’t replacing the old image unless I refresh the page. Is there a way to either a) Have the new image automatically appear without refresh or b) force a refresh on upload?

I’d prefer the former to the latter if possible! Some reference code:

Upload -

Template.editProfile.events({
      'change #avatarUp': function(event, template) {
	   FS.Utility.eachFile(event, function(file) {
           Avatars.insert(file, function (err, fileObj) {
                if (err){
                // handle error
                } else {
                // handle success depending what you need to do
                var userId = Meteor.userId();
                var avatarURL = {
                    "profile.avatar": "/cfs/files/avatars/" + fileObj._id
                };
                Meteor.users.update(userId, {$set: avatarURL});
                }
            });
	   });
    }
});

Show avatar -

<img src="{{currentUser.profile.avatar}}" />

A thanks must go out to @serkandurusoy who got me this far, but I didn’t want to specifically badger him further :smile:

Cheers!

1 Like

Do you have autopublis? If not, are you publishing the Avatars collection and subscribing to it?

What happens if you place Meteor.publish(null, function() {return Avatars.find()}) in a server js file?

Hello again! Sorry for the late reply.

Yes I still have autopublish running. I’m still in development so it made sense to me to have autopublish working. Should I remove it?

Adding that code into /server/publications/avatars.js made no difference. Even if I have no subscription in place, it still shows the avatar since I’m using {{currentUser.profile.avatar}}.

I think the problem is that although the link to the avatar is automatically updating, it’s updating too early which means there’s nothing to see, since the image isn’t fully uploaded. The only problem is I don’t know a good, suitable fix for this!

Would adding some kind of pause before updating the profile.avatar url work? Just a thought.

Literally, just after I typed that response I again fixed it.

Here is my upload code now:

Template.editProfile.events({
    'change #avatarUp': function(event, template) {
        event.preventDefault();
	   FS.Utility.eachFile(event, function(file) {
           Avatars.insert(file, function (err, fileObj) {
                if (err){
                    toastr.error("Upload failed... please try again.");
                } else {
                    toastr.success('Upload succeeded!'), setTimeout(function() {
                        var userId = Meteor.userId();
                        var avatarURL = {
                            "profile.avatar": "/cfs/files/avatars/" + fileObj._id
                        };
                        Meteor.users.update(userId, {$set: avatarURL});
                    }, 500);
                }
            });
	   });
    }
});

I added the setTimeout value to the avatar.profile update function and it’s worked perfectly. One question for clarification though, does setTimeout prevent any javascript being executed? Are there any built-in CollectionFS functions that may be better?

Hm this is rather interesting. Could you try the timeout with the duration as “0” instead of “500”? I’m just wondering if this is an issue that can be rather mode officially remedied with Tracker itself

Setting it to “0” just goes back to the same problem. Looks like it’s updating the .profile.avatar link before the image is uploaded, so it’s not finding the image. Again, a refresh fixes it.

“500” seems like the best number so far in testing, though this is on a local machine and not a server, so maybe 750 or 1000 might be a better number in production.

Sorry for the late reply too by the way, I didn’t realise I had another response!

Whenever I see timeouts being used as a workaround for an async response I get nervous. You’re already unsure - 500? 750? 1000? Where do you stop? You likely end up penalising everyone to cater for the slowest response you’ve found to date.

I’m with @serkandurusoy on this - there has to be a better way and if the “on completion” callback is not working as expeced then it may be possible to use Tracker.

1 Like

I think you should store _id reference to CFS in profile and display it using CFS helpers.
Cause currently you are probably not accounting for whole storing procedure and just waiting for insert finish?
But I am not using CFS so this is just some random post :smiley:

1 Like

Honestly never heard of Tracker thus far. Any links or examples I could use?

I’m aware my edit above isn’t really a good solution, but CFS don’t have the best documentation and it was the best workaround I could find for now that worked.

Any help will always be appreciated.

I tried this:

   'change #avatarUp': function(event, template) {
        event.preventDefault();
	   FS.Utility.eachFile(event, function(file) {
           Avatars.insert(file, function (err, fileObj) {
                if (err){
                    toastr.error("Upload failed... please try again.");
                } else {
                    toastr.success('Upload succeeded!');
                    var userId = Meteor.userId();
                    var avatarURL = {
                        "profile.avatar": "/cfs/files/avatars/" + fileObj._id
                    };
                    if(fileObj.isUploaded) {
                        Meteor.users.update(userId, {$set: avatarURL});
                    }
                };
            });
	   });
    }

But it didn’t work unfortunately. The documentation I used to try this (fileObj.isUploaded) was from an old post. Worth a try but didn’t work :frowning:

I’ll look into Tracker shortly.

Tracker is the underlying package for things like Session, ReactiveVars and ReactiveDicts (but can be used in its own right). Back in the good ol’ days it was called Deps (you still find Deps documentation hanging around in unexpected places from time-to-time).

However, the more I look at your code, the more confused I am that any timeout would work, which I guess is what prompted @serkandurusoy’s suggestion to drop it to zero.

btw, just noticed a typo in the code (a , which should be a ;) just before setTimeout.

Anyhoo, the docs for currentUser don’t state it’s reactive, just that it calls Meteor.user(), so setting up a reactive helper would seem to be the best way to achieve what you want. Fortunately, Meteor.userId() is reactive, so this should work (untested):

Template.editProfile.helpers({
  avatarURL: function() {
    return Meteor.users.findOne(Meteor.userId()).profile.avatar;
  };
});

and use <img src="{{avatarURL}}" /> in your template.

Hi @robfallows thanks for your reply.

Just tried what you suggested and it’s doing the same thing. It’s changing the URL, but it’s changing it before the file is uploaded so it’s not displaying, hence I tried if(fileObj.isUploaded) earlier. Unfortunately, CFS doesn’t have the best documentation (in my opinion) as to what to do once the file is uploaded. There’s tons of stuff for the upload process itself though.

Again, any help would be smashing! It’s seems to be one of those things that in theory should be easy to fix, but actually it’s probably going to be one of those really annoying and fiddly things to sort out…

I dont know which documentation you are looking at, but CFS have quite few helpers to show images in documentation.
But constructing your own link and pushing it into profile is not best approach I think.
Just use reference to CFS file in profile and use CFS helper to show that image which ID is stored for example in Meteor.user().profile.avatar (that one is reactive and returns object without need for additional find)

Thanks for your reply @shock it is much appreciate!

This example from the CFS docs:

{{#each images}}
  URL: {{this.url}}
  <img src="{{this.url store='thumbnail'}}" alt="thumbnail">
{{/each}}

It does work (once I set everything to my own variables etc) but it shows ALL images, not the single avatar I’m after.

If I do this:

Template.editProfile.helpers({
   avatarURL: function() {
        return Meteor.users.findOne(Meteor.userId()).profile.avatar;
    },
    images: function () {
        return Avatars.find();
    }
});

{{#each images}}
    <a href="#" data-toggle="modal" data-target="#newAvatar"><img class="avatar" src="{{avatarURL}}" /></a>
{{/each}}

Then it displays only one avatar, but lot’s of times! So I’m nearly there I think. I’m a bit out of ideas now though :frowning:

Ok quick update, changed the code block in the template to:

{{#if images}}
    <a href="#" data-toggle="modal" data-target="#newAvatar"><img class="avatar" src="{{avatarURL}}" />
{{/if}}

But now when I update the image, it’s back to square-one. It’s not updating the link to the image after the upload, it’s updating the link immediately. That’s the problem.

However, I do feel this way of displaying the avatar is a better way of doing it :smile:

I would probably go with something like this
untested, I use coffeescript, so high probability I forgot ; in vanilla

'change #avatarUp': function(event, template) {
        event.preventDefault();
	   FS.Utility.eachFile(event, function(file) {
           Avatars.insert(file, function (err, fileObj) {
                if (err){
                    toastr.error("Upload failed... please try again.");
                } else {
                    toastr.success('Upload succeeded!');
                    Meteor.users.update({_id: Meteor.userId() }, {$set: { 'profile.avatar': fileObj._id }});                   
                };
            });
	   });
    }


Template.editProfile.helpers({
    avatarImage: function () {
        return Avatars.findOne({_id: Meteor.user().profile.avatar)});
    }
});


{{#with avatarImage}}
    <a href="#" data-toggle="modal" data-target="#newAvatar"><img class="avatar" src="{{this.url}}" />
{{/if}}
2 Likes

That works an absolute treat. You’re a star!

If I could give you a hundred likes I would!

Quick question, I’m new to Meteor so can you explain what the _id is referencing here?

Thanks again!

http://docs.meteor.com/#/full/selectors

1 Like

Wow, this thread really took off while I was gone :smile:

Reading through the whole ordeal, I can say a couple of things:

  1. The final code works fine because instead of storing a url, you are storing a reference to the file object, whose instance you get reactively in your template helper as a data context so tracker does actually do its job

  2. Apparently, getting a file reference may not mean an acknowledgement of the complete file data having recevied and stored on the server yet. This is interesting, but I guess what actually has led to the isUploaded helper

  3. Tracker.afterFlush() is what you may want to wrap your helpers with (if you had contined storing the url instead of a refernce) to make sure it is rerun after all other reactive computations have finished running. Another option would be to use Meteor.defer() to wap your user update function to defer execution of the url update. Either way would be a replacement for the timeout, and istead of setting a fixed wait time, it looks for the actual operation that needs finishing.

  4. Although not directly related to this case, publishing nested fields is tricky. In your case you are using autopublis so the whole document is published. An edge case would be for users whose avatar fields have not yet been set. In that case, setting that on the server would not cause it to propagate to the client because meteor’s ddp server tracks document changes on topmost fields.

Anyhow, I’m glad the problem is solved and this thread sure is good reference for future readers so thanks for sharing your problem and the path to its solution.

2 Likes

Indeed it did!

Thanks that’s more what I was looking for in terms of understanding the way this works.

That is incredibly useful for a lot of things, I didn’t know either technique existed. It seems I’m still brushing the surface of what Meteor can do (which is both amazing and scary at the same time…)

To be honest I thought this may be the case anyway since right now I’m in development mode and not really using publish / subscribe yet.

And finally, yes this has been a very useful thread and I’m sure it’ll be useful for many other CFS users.

Thanks to you all for your replies and helping me come to a stable and suitable solution!

2 Likes