Skip to content
This repository has been archived by the owner on Aug 30, 2021. It is now read-only.

Save profile images to Amazon S3 #1857

Merged
merged 4 commits into from
Sep 19, 2017
Merged

Save profile images to Amazon S3 #1857

merged 4 commits into from
Sep 19, 2017

Conversation

Ghalleb
Copy link
Contributor

@Ghalleb Ghalleb commented Aug 27, 2017

This PR aimed to save profile images to a S3 Bucket.

I discovered that deploying to Heroku leaded to profile images not being saved.

This 3 environment parameters need to be filled:
S3_PROFILE_BUCKET: the name of the bucket where the images will be saved
S3_ACCESS_KEY_ID: Your S3 access key
S3_SECRET_ACCESS_KEY: Your S3 access key password

@Ghalleb Ghalleb mentioned this pull request Aug 27, 2017
@mleanos
Copy link
Member

mleanos commented Aug 27, 2017

Even though I used S3 with a few of my MEAN.js project, I think these changes might be too opinionated for this framework. There are many other storage services that can be used, and we can't include logic for each in the boilerplate implementation.

This could be a useful feature branch that can be an example of how to handle S3 storage with MEAN.js though. The implementation looks correct though, but I haven't tested.

@Ghalleb
Copy link
Contributor Author

Ghalleb commented Aug 28, 2017

I did this because otherwise deploying the boilerplate to heroku would not really work. The profile images are not saved.
I also saw in the issues that other people had the same problem.
So it could be a good idea to add it to the master branch. If you don't specify the S3_PROFILE_BUCKET variable it will work as usual. In fact it's just a simple change in multer storage config.
I chose S3 because I know it but I cam add others if it's needed.
I demonstrate that mean.js can work in a multi node architecture and scale without problem.

@Ghalleb
Copy link
Contributor Author

Ghalleb commented Aug 28, 2017

If i can go further, when I first started with mean.js, with version 0.4.2, I found usefull that I could deploy to Cloud Foundry (Bluemix, Pivotal Web Services, ...) so easily. So I could focus on the code, not on devopts.
That's why I added the deploy to heroku button and the ability to use S3 (Heroku use AWS too...). With that you can deploy a production class app without losing time. And focus on important things.

@simison
Copy link
Member

simison commented Aug 28, 2017

I agree with @mleanos 's sentiment about adding S3 being too opinionated, but I think the need for easy deployment to get started outweights that. Just for the sake of staying pragmatic.

So either we can:

  • just go with this PR,
  • add note+tips on how to store files remotely (I think it's a common need also outside Heroku use)
  • or merge this PR and add note/instructions/list for other possible cloud services

@lirantal thoughts?

@simison
Copy link
Member

simison commented Aug 28, 2017

Oh hey and thanks for coding & activism @Ghalleb ! It's awesome. :-)

@lirantal
Copy link
Member

I agree that we don't want to steer off with too many examples, but on the other hand MEAN.JS serves as a foundation for learning and flexible project boilerplate to start from, as such and with the AWS-franzy I don't think it's far fetched to include this, especially when it's anyway only enabled with proper config.

So I'm ok with this.

@mleanos
Copy link
Member

mleanos commented Aug 30, 2017

I'm fine with these changes too then. I just didn't want to jump down a rabbit hole by convoluting the code-base with too many non-boilerplate use cases. However, AWS is very common & since these changes only take effect when the S3 configuration is present, then it's ok. The implementation is clean enough as well, since it's only a few changes (mostly on the backend).

I'll do a thorough code-review and test this locally. Thanks @Ghalleb. I appreciate the willingness to jump right in here and take the initiative.

Copy link
Member

@mleanos mleanos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ghalleb I've tested this, and mostly everything is working great. And I like the clean implementation.

I've just made some suggestions on some of the configuration changes. And I had one issue, with the new profile image path changes. I've left a comment about that.

@@ -45,8 +45,13 @@ module.exports = {
illegalUsernames: ['meanjs', 'administrator', 'password', 'admin', 'user',
'unknown', 'anonymous', 'null', 'undefined', 'api'
],
s3Config: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Config" might be redundant. One way that I've structured my configs is like the following:

aws: {
  s3: {
    accessKeyId: '',
    secretAccessKey: ''
  }
}

uploads: {
profile: {
s3Bucket: process.env.S3_PROFILE_BUCKET,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better this in the s3 config above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you're relying on this config setting when deciding if to use S3 as the storage. You could use a setting like storageType ("s3", "local", etc.), or useS3 (Boolean). The former would be more extendable.

var multerConfig;


if (config.uploads.profile.s3Bucket) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could check config.uploads.profile.storage === 's3' See above comment about this config name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And to be sure the S3 settings are there

config.uploads.profile.storage === 's3' && config.aws.s3

user.profileImageURL = config.uploads.profile.image.dest + req.file.filename;
user.profileImageURL = config.uploads.profile.s3Bucket ?
req.file.location :
'/' + req.file.path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is preventing the deleteOldFile from finding the existing file every time. I understand why this change was made. Otherwise, the front-end code wouldn't be able to find the location of the image. We might need to find a better way to handle this.

I'm on Windows, so it could be only happening in my environment. @Ghalleb Can you confirm if this is happening in your environment as well (if you're not on Windows, of course :) )

After adding a profile image, my user.profileImageUrl is /modules\users\client\img\profile\uploads\12db59fbdd2193b3b8f99867e1640175.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fix this issue, you can update this line https://github.com/meanjs/mean/blob/master/modules/users/server/controllers/users/users.profile.server.controller.js#L112 to resolve the path for the unlink method.

fs.unlink(path.resolve('.' + existingImageUrl), function (unlinkError) {

But it seems a little hacky to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what to do for this one.
On my environment (Ubuntu), everything is working fine.

For all the other changes, I will make them soon.

Thanks for the review.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you verify that the existing file is getting deleted, and you're not getting an error logged to the console?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, I have an error in the server logs:

Removing profile image failed because file did not exist.
info: POST /api/users/picture 200 24.550 ms - 414

@Ghalleb
Copy link
Contributor Author

Ghalleb commented Sep 4, 2017

@mleanos
I made the changes you requested exept for the profile image path.
Should I go with your hacky trick or do you have a better solution in mind?

@mleanos
Copy link
Member

mleanos commented Sep 4, 2017

I haven't thought of any better solution to the profile image oath issue. See if you could come up with something.

As a first step in solving the issue, how about adding a test for this case? I'm orettybsure there isn't a test for it.

Test case:

  1. Add profile image
  2. Add new profile image
  3. Make sure the first profile image was removed

Fix file deletion
@Ghalleb
Copy link
Contributor Author

Ghalleb commented Sep 5, 2017

I added your fix for file deletion and also added S3 file deletion.

Copy link
Member

@mleanos mleanos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the changes based on my suggestions. They look much better.

I left some additional feedback. I'd like to avoid removing the use of the "/" on the front-end when we're using local storage for the profile image. See my comments about that.

if (unlinkError) {
if (config.uploads.storage === 's3' && config.aws.s3) {
try {
var { region, bucket, key } = amazonS3URI(existingImageUrl);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be troublesome, if switching from "local" to "s3" storage. The first time a user updates their profile picture after switching the storage from one to another, this will create inconsistency with the data/files. This is probably more of an edge case, or migration consideration. Perhaps not a big deal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that as well. Normally you have to configure your app once!
An edge case for me.

@@ -109,24 +135,54 @@ exports.changeProfilePicture = function (req, res) {
function deleteOldImage() {
return new Promise(function (resolve, reject) {
if (existingImageUrl !== User.schema.path('profileImageURL').defaultValue) {
fs.unlink(existingImageUrl, function (unlinkError) {
if (unlinkError) {
if (config.uploads.storage === 's3' && config.aws.s3) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this check is being used in multiple places, you could move it into a local variable at the top level of this request handler.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like: var useLocalStorage = config.uploads.storage === 'local';

Then here you can just if (!useLocalStorage)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to let place for another storage provider...
But if it will only be local or S3, I'll do it.

@@ -3,7 +3,7 @@
<form class="signin form-horizontal">
<fieldset>
<div class="form-group text-center">
<img ngf-src="vm.fileSelected ? picFile : '/' + vm.user.profileImageURL" alt="{{vm.user.displayName}}" class="img-thumbnail user-profile-picture" ngf-drop>
<img ngf-src="vm.fileSelected ? picFile : vm.user.profileImageURL" alt="{{vm.user.displayName}}" class="img-thumbnail user-profile-picture" ngf-drop>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add a config setting to the Shared config that would allow you to check whether the profile should be retrieved using local storage.

Then you wouldn't need remove the "/", thus solving path issues on the backend.

shared: {
    profileImageIsExternal: process.env.UPLOADS_STORAGE ? true : false
    ...
}

});
});
} else {
fs.unlink(path.resolve('.' + existingImageUrl), function (unlinkError) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really a fan of this solution. See my comment about adding a setting to the Shared config. This might not be needed if we can keep the "/" on the front-end when we're using local storage.

@@ -38,7 +38,9 @@
},
"dependencies": {
"acl": "~0.4.10",
"amazon-s3-uri": "0.0.3",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This library is rather new, and doesn't have many stars. However, the implementation looks fine and isn't too complicated. We'll probably have to keep an eye on this library though.

Copy link

@Isabeljason Isabeljason Sep 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even we follow the library updates there are few minor changes that need to be update to it. As i am a new bee to MEAN.js need support from others to get into and complicated issues like path, switching, controllers, pre & post updates need to be checked very carefully.
Update me on this if any wrong.


console.error(unlinkError);
aws.config.update({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? This is already happening up top.

user.profileImageURL = config.uploads.profile.image.dest + req.file.filename;
user.profileImageURL = config.uploads.storage === 's3' && config.aws.s3 ?
req.file.location :
'/' + req.file.path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be changed back to what it was before: config.uploads.profile.image.dest + req.file.filename, if we can still use the "/" on the front-end when using local storage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not fan of the '/' in the front end. I prefer having the final URL in the database. Otherwise if we switch from local to s3, all old URL would be bad formed. And that add more logic to the front where it shouldn't be.

@Ghalleb
Copy link
Contributor Author

Ghalleb commented Sep 6, 2017

I made some changes. But not the '/' in the front-end as I prefer nor addind logic on the fron-end and storing the final url in the database is better in my opinion

@simison simison mentioned this pull request Sep 6, 2017
@Ghalleb Ghalleb mentioned this pull request Sep 8, 2017
@mleanos
Copy link
Member

mleanos commented Sep 10, 2017

@Ghalleb Thank you for your updates based on my feedback!

LGTM now.. I'll merge in the next couple of days, after I test locally once more.

@mleanos mleanos merged commit f146cbc into meanjs:master Sep 19, 2017
@mleanos
Copy link
Member

mleanos commented Sep 19, 2017

@Ghalleb Thank you for contribution. I apologize it took so long to merge

I didn't catch that your commits didn't have messages, before now. Take a look at our contribution guidelines for next time. Looking forward to seeing more contributions from you :)

@Ghalleb
Copy link
Contributor Author

Ghalleb commented Sep 20, 2017

Are you sure there is no messages?
I see them in my repo:
https://github.com/Ghalleb/mean/commits/s3-profile-image

@nlaycock
Copy link

I just wanted to say a massive thank you for all the work on S3. I have just implemented it myself using your approach and it has saved me a HUGE amount of time. Thank you soooooooo much!!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants