-
Notifications
You must be signed in to change notification settings - Fork 2k
Conversation
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. |
I did this because otherwise deploying the boilerplate to heroku would not really work. The profile images are not saved. |
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. |
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:
@lirantal thoughts? |
Oh hey and thanks for coding & activism @Ghalleb ! It's awesome. :-) |
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. |
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. |
There was a problem hiding this 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.
config/env/default.js
Outdated
@@ -45,8 +45,13 @@ module.exports = { | |||
illegalUsernames: ['meanjs', 'administrator', 'password', 'admin', 'user', | |||
'unknown', 'anonymous', 'null', 'undefined', 'api' | |||
], | |||
s3Config: { |
There was a problem hiding this comment.
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: ''
}
}
config/env/default.js
Outdated
uploads: { | ||
profile: { | ||
s3Bucket: process.env.S3_PROFILE_BUCKET, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will...
There was a problem hiding this comment.
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
@mleanos |
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:
|
Fix file deletion
I added your fix for file deletion and also added S3 file deletion. |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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 |
@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. |
@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 :) |
Are you sure there is no messages? |
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!! |
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