00:00
(upbeat music)
00:02
So it's pretty common for applications to allow users
00:06
to upload a photo representation of themselves
00:08
into our application that we refer to as an avatar.
00:11
So today we're gonna cover how we can go about
00:13
adding a field for that into our user profile here.
00:15
So let's go ahead and hide our browser away
00:17
and jump into our profiles edit page.
00:19
Now with the way that we set up our form inputs,
00:21
we can specify a type and a valid input type is file.
00:25
So if we wanted to add in a file input,
00:27
we could do @form input, specify the type here as input,
00:31
give it a label of something like avatar
00:34
and a name of avatar.
00:36
And now on our actual user model here,
00:38
if we jump into here, scroll down a little bit,
00:40
we have the storage column for this
00:43
referred to as our avatar URL.
00:44
Rather than taking our avatar image as a base 64
00:47
and storing it directly inside of a database,
00:48
what we're gonna do is take that image, move it somewhere,
00:51
and then store a reference of where actually
00:53
we've moved it to inside of our database
00:56
as our avatar URL here.
00:57
So in terms of a default value,
00:59
if a user already has an uploaded avatar,
01:01
we're not gonna have an actual file
01:03
to be able to append in here as the value,
01:05
but we can still have that represented inside of our form
01:08
as a hidden input.
01:09
So we could do input type hidden name avatar URL value equals
01:14
and then we could set the default value
01:15
to our auth user.avatarURL.
01:19
That way, if they do have an avatar URL,
01:21
it will still allow us to discern
01:22
whether or not they wanna keep it
01:24
or remove it altogether as we send up data with this form.
01:26
And my bad, I've wrote input here as the type
01:28
that should be file.
01:29
Okay, so if we take a look at this real quick,
01:31
inside of our browser,
01:32
we should now see an avatar field
01:34
that when clicked allows our user
01:35
to jump into their file system
01:37
and select an image to upload.
01:39
Cool, let's hide that back away.
01:41
Let's next jump into our profile validator
01:43
and we're gonna add in two properties.
01:44
First, we're gonna add in our avatar.
01:46
This is the representation of the actual image
01:48
that we're uploading.
01:49
So we'll do vine dot and there is a file type here
01:52
and it accepts in a number of validation options.
01:55
Some of the more prominent ones
01:56
that you might want to validate against
01:57
would be extension names.
01:59
So these would be actual file extensions
02:00
that we want to allow users to upload.
02:02
So that might be something like JPEG, PNG, JPEG,
02:05
and any other file types applicable
02:07
that you would want to accept.
02:09
Essentially, this allows us to say
02:11
that we don't want users to be able to upload a PDF
02:13
as their avatar image.
02:14
And then we can also add in a size
02:17
to limit the file size that the user can upload with.
02:19
So if we wanted to limit our upload
02:21
to something like five megabytes,
02:22
we could do five MB there to discern that here
02:25
with our avatar upload.
02:26
And then of course,
02:27
we also want to make this field optional altogether.
02:30
We're not gonna require users to have an avatar.
02:32
It's gonna be completely optional.
02:33
And then we also have our avatar URL.
02:35
This is what we use to mostly discern
02:37
whether or not a user wants to remove their avatar.
02:39
Since our actual avatar field
02:41
is only ever gonna have a value
02:43
if they want to upload an avatar,
02:44
we can use the avatar URL to persist
02:47
whether or not they want to keep
02:48
a previously uploaded avatar image
02:50
as their current avatar.
02:51
Or if our avatar URL were to come up empty,
02:53
then we would know that they are intent
02:55
on removing their avatar image.
02:57
Okay, so file.
02:58
And this is just gonna be a string
02:59
because this is a string representation
03:01
of where we've stored their avatar.
03:03
And it too is gonna be optional.
03:05
Cool, so let's give that a save.
03:06
And then we'll jump into our profiles controller.
03:08
We now have from our validator an avatar
03:11
and an avatar URL that we can extract out.
03:13
Now, a thing to note before we get started
03:14
is our avatar comes through
03:16
since we have it as a file on our validator
03:19
as a multi-part file.
03:20
So this is gonna have additional information on it
03:22
like the file name that the user's uploaded the file with.
03:25
And it's gonna have options
03:26
that we can take with that file,
03:28
like being able to move it somewhere else.
03:30
When we upload a file via a form like this,
03:31
the body parser is gonna move this
03:33
into a temporary folder called TMP
03:35
inside of our application.
03:36
So that allows us to then move that file wherever we want.
03:40
That could be uploading it to a service like S3,
03:42
Google Cloud Storage, Delogean Spaces, and the like,
03:45
or somewhere inside of our application structure
03:47
that we want to persist those images.
03:49
The way we can do this is if we scroll down here,
03:52
let's do this just underneath where we're querying
03:53
our profile.
03:54
If we have an avatar that's come up,
03:56
we can await, reach for our avatar.
03:59
And here's a quick autocomplete list
04:01
of all of the properties and methods
04:02
that the multi-part file has that we can make use of.
04:05
So you can see it comes with a file size,
04:07
subtype, temporary path location,
04:09
whether or not it's been validated.
04:11
We scroll up a little bit further.
04:12
There's the file path, file name, field name,
04:14
extension names, whether it came up with any errors
04:16
and the like.
04:17
But what we care about right now is a method called move.
04:20
So if we call this, it's going to accept in
04:22
as its first argument, a location that we actually want
04:24
to move the avatar to.
04:26
Remember by default, this is going to upload
04:28
to a TMP directory in the root of our application.
04:30
So let's say instead we want to move it to a folder
04:33
inside of our application called storage,
04:35
and then a sub folder inside of storage
04:36
specifically for our avatars.
04:38
We can use the app module.
04:39
So if we type out app here and hit tab to auto import this
04:42
from AdonisJS core services app,
04:44
we can call a method called make path
04:46
that allows us to make and or get a reference
04:49
to a path inside of our application structure.
04:51
So if we wanted to move this avatar
04:53
into a folder called storage, we could do storage.
04:55
And if we wanted a sub folder called avatar,
04:57
we could do slash avatars.
04:59
And now if storage slash avatars already exists
05:02
inside of the root of our application,
05:04
then this will just return back a reference
05:05
to that location.
05:06
Otherwise it will create a folder called storage
05:09
and a sub folder called avatars if needed.
05:11
Additionally, with this move method,
05:12
we also have some other options.
05:14
So we can rename the file and specify whether or not
05:16
we want to allow it to overwrite a file
05:18
already inside that location.
05:20
We'll leave both of these off for now.
05:22
Cool, so this will take care of storing our avatar
05:24
where we want it inside of our application.
05:26
Now what we need to do is store that location
05:28
as our avatar URL path on our user.
05:31
So we can do auth user avatar URL equals.
05:34
And now this app dot make path is going to be an absolute
05:36
path in our file system to this location.
05:39
And that's not what we want to store as our avatar URL,
05:42
but rather we just care about the location
05:44
where we're gonna be able to access this going forward.
05:45
So we might save this as something like avatars slash,
05:49
and then the avatar file name.
05:51
And the file name is also going to include
05:53
the file extension, so we can leave that off.
05:55
And then that will get persisted with our usual merge,
05:58
save call right down here.
05:59
So we don't need to call save here too.
06:01
Lastly, if a user is not uploading an avatar,
06:04
then they may be trying to remove their avatar.
06:06
So we can do else if not avatar URL,
06:10
then we just want to clear that on our user.
06:11
So auth user dot avatar URL equals,
06:15
and we can just null that out.
06:16
Cool, so we should have most everything set up.
06:18
There is an additional thing that we want to do,
06:20
but I want to show how things behave
06:22
before we make that change.
06:23
So let's jump back into our browser
06:25
and let's attempt to upload an avatar here.
06:26
So let's select an image.
06:27
I have an image right here.
06:28
We'll just go ahead and open it up.
06:30
Okay, you can see it's selected right here as the file name.
06:32
If we go and just hit update avatar,
06:34
you'll see that we're gonna get back an invalid validation
06:37
for our file type, the avatar must be a file.
06:40
Now, the reason that DonetsJS isn't able to recognize
06:42
our avatar as an actual file
06:44
is because we haven't specified on our form
06:46
that we want to allow this to happen
06:48
via an anti-multi-part form data.
06:50
So if we had our browser back away,
06:52
go back into our edit page,
06:53
scroll up to where we have our form.
06:55
What we want to do is add an attribute called ENC type
06:58
and set this to the multi-part form data value.
07:01
This is gonna allow our form to upload in multiple parts
07:04
so that our server can actually digest
07:07
all the bits of the image
07:08
and actually recognize it as an image.
07:10
So with that applied, if we give this a save
07:12
and jump back into our browser
07:13
and give this a try once more.
07:14
So let's go ahead and select our avatar image here,
07:16
hit open and update our profile.
07:18
You'll see that we get redirected right back here
07:20
with no errors applied on our avatar field anymore.
07:23
Furthermore, if we hide this away real quick
07:24
and we add a quick inspect of our auth user avatar URL,
07:29
jump back into our browser,
07:31
you can see that we have now avatars/av.jpg,
07:34
exactly as we are.
07:35
So next, what we want to do is display that avatar
07:37
above our avatar field and allow the user to hit X on it
07:40
to clear it out,
07:41
which would then clear out our avatar URL field.
07:43
Before we do that though,
07:44
we need to give ourselves a way to access our storage path.
07:48
If we had stored the image inside of our public directory,
07:50
it would have been immediately accessible
07:52
just via slash whatever the file path is.
07:54
But we've stored this inside of storage
07:56
because public is prone to getting cleared out
07:58
and updated every time that we deploy out our application
08:02
inside of a production-based environment.
08:03
But since storage is not publicly accessible,
08:06
like our public directory is,
08:08
we need to jump into our routes file.
08:10
And I'm gonna put this up at the top of all of our routes.
08:12
So scroll up to where we have our home route,
08:14
we'll put it just underneath there.
08:16
So we'll do router.get.
08:17
And since we uploaded our image itself
08:19
inside of storage/avatars,
08:21
we'll assume the storage part
08:23
and just specify that we want to reach for avatar images
08:25
by doing /avatars here,
08:27
and then we can do /filename.
08:29
So that anytime that we attempt to reference
08:30
just the storage path that we've saved our avatar URL as,
08:33
it'll come up exactly like this.
08:35
We'll have avatars/filename.
08:37
Actually, let's also go ahead and prefix the uploaded image.
08:39
So let's jump back into our profiles controller here
08:41
and prefix the avatars URL here with a slash two.
08:44
Okay, and we also forgot to take a look
08:45
at what actually uploaded.
08:47
So if we scroll down here a little bit,
08:48
we're gonna see a storage directory
08:49
now inside of our application.
08:51
If we click on this, there's our avatars
08:53
and there's our av.jpg right there.
08:55
You also see this temp directory.
08:56
This is where this image was temporarily stored
08:59
as we were uploading it with our multi-part form data.
09:02
Whenever we called avatar.move,
09:03
that's where it moved it from that temp directory
09:05
into our storage avatars folder.
09:08
Now we'll need to retest our upload here.
09:10
So I'm gonna go ahead and right click on the image,
09:12
delete it out and move to trash.
09:13
This will just allow us to persist that extra slash on there
09:16
by resubmitting our form.
09:18
Okay, let's jump back into our routes now
09:20
and let's make a new controller for our avatars.
09:22
So hit command + shift + P, make controller,
09:25
and we'll call this avatar.
09:26
Hit enter there.
09:28
It's not a resource, so we'll hit no there.
09:30
And there we go.
09:31
And we'll go ahead and add a show method onto there.
09:33
We'll circle back to here momentarily.
09:34
Let's go ahead and finish defining our route.
09:36
Okay, so we have our avatars controller
09:39
and we have our show method inside of there
09:41
is avatars show.
09:43
Now let's jump back into our avatars controller
09:45
and we'll want the response
09:47
as well as our route parameters.
09:49
For this, all that we need to do is download the image
09:51
and return it back as the response.
09:53
So we can return response, download,
09:55
and then we can use our app module again.
09:57
Hit tab.import that, make path,
09:59
reach for storage/avatars.
10:02
And we can specify a comma here
10:04
and add in our params.filename
10:06
to add that into our make path.
10:08
Let's give that a save.
10:09
Jump back into our edit page.
10:10
And now we should be able to replace this inspect here
10:13
with an actual image.
10:14
So let's do div class relative
10:16
and then plot the image inside of there
10:18
with a source, auth, user, avatar URL.
10:21
We'll do class width full
10:23
since we have this form section here pretty small,
10:26
and that should suffice.
10:27
We'll also want to wrap this in an if.
10:28
So if auth user does have an avatar URL, just like so,
10:33
and our if, and let's go ahead and see what we get.
10:35
So let's jump back to our browser.
10:36
And all right, we deleted it.
10:37
So let's go back into browse,
10:38
hit our image, open, update, and there we go.
10:41
So now we see our image
10:42
because we've re-uploaded it and fixed the path.
10:45
I don't quite like seeing myself that big,
10:46
so I'm gonna go ahead and do width one third
10:49
and mix auto on that relative div there.
10:51
And now what we want to do is add in a button.
10:53
So we'll do button type, button class,
10:56
and then we'll absolutely position this
10:58
to the top zero, right zero,
11:00
add a padding of maybe two or three, rounded full,
11:04
and we'll just do and times there to add a little X.
11:06
So let's start by breaking our button down here
11:08
and let's add an on click attribute to it.
11:11
Whenever we click, what we want to do
11:12
is both clear out the value of our avatar URL
11:16
as well as remove the parent element to this button,
11:19
which would be the div containing our image preview.
11:21
The easiest way to get a direct reference
11:22
to our avatar URL field is through the form.
11:24
So what I'm gonna do is scroll on up to our form,
11:26
add an ID, we'll just call this something like edit form,
11:29
and then we can scroll back down to our button.
11:31
And this will allow us to do document.forms,
11:33
reach for our edit form,
11:35
which allows us to reach directly
11:36
for our avatar URL field and its value
11:39
so that we can set it back to an empty string.
11:41
Let's go ahead and wrap that in parentheses.
11:43
Then once we've cleared out our avatar value,
11:44
what we want to do is an or,
11:46
since this won't necessarily come back truthy,
11:48
and reach for this,
11:49
which is our actual button element, .parent element,
11:52
which is now our div surrounding our button, .remove
11:56
to actually remove the div
11:58
and all of its subcontents from the document.
12:00
Just so that we can see this all evidently happen,
12:02
let's go and switch our avatar URL
12:04
from type hidden to type text,
12:06
just so that we can see it directly inside of our document.
12:08
Jump back into our browser.
12:09
There we go.
12:10
I'm gonna give this a hard refresh
12:11
to make sure nothing's going on there.
12:12
Here's our little X that will allow us
12:14
to clear everything out.
12:14
If we go ahead and give that a click,
12:16
there goes our avatar URL value,
12:18
as well as our avatar preview.
12:20
And now if we go ahead and update our profile,
12:21
our avatar is gone,
12:23
and we should also probably remove the file
12:25
from our file system and set this
12:27
with a default value of an empty string.
12:28
So first let's set this to a default value of an empty string
12:31
if we don't have an avatar URL, just like so.
12:33
And go ahead and switch this back to a type of hidden
12:35
since we've seen it in action.
12:37
And let's jump back into our profiles controller
12:38
where we have our else if,
12:40
and we're clearing out our avatar URL.
12:41
If you take a look at our storage avatars,
12:43
we still have our image in here,
12:45
despite it no longer being bound to our auth user.
12:47
Well, what I wanna do is bring the FS module
12:49
back into our application,
12:50
so we can import a method called unlink
12:53
from FS and FS promises.
12:55
Give that a save real quick.
12:56
I wanted a node prefix, there we go.
12:58
Okay, if we scroll back down now,
12:59
we can do a wait, call unlink,
13:02
which accepts in a path,
13:03
which allows us to delete out our file as noted right here.
13:07
So what we wanna do is provide a path to that file,
13:09
and we'll wanna do this
13:10
before we remove our avatar URL value.
13:12
So we'll do app make path to get an absolute path
13:15
to where we're storing our avatars,
13:17
prefix this with storage,
13:19
and then provide in our auth user avatar URL
13:22
as the second portion to that.
13:24
Now it's not quite happy
13:25
because avatar URL here could be null.
13:27
So to our else if,
13:29
we'll also want to check and end auth user avatar URL
13:33
to verify that the user no longer wants an avatar URL,
13:36
but did have an avatar URL to start with,
13:38
fixing the nullable reference here on our unlink call.
13:41
Let's give that a save.
13:42
I'm gonna go ahead and clear out our AV
13:43
just so that we can walk through the flow one more time.
13:45
Delete that out, move it to the trash, there we go.
13:47
Since we've already destroyed the reference
13:49
to our avatar URL on our user,
13:50
let's go ahead and jump back into our browser,
13:52
browse for our file, let's give it a select,
13:54
open it up, update, there we go.
13:55
If we jump back into our text editor,
13:57
there it is once more.
13:58
If we go ahead and hit on our X,
13:59
update our profile once more.
14:01
Now if we jump back into our text editor,
14:03
there we go, our storage avatars
14:05
no longer contains our image because it was removed.
14:08
Awesome, so there are a couple of things
14:10
that you're gonna wanna keep in mind here
14:11
before we round things out.
14:12
First and foremost,
14:13
we do have our storage directly inside of our application.
14:15
So depending on how your deployment
14:17
for your application works,
14:18
you may need to literally move that storage
14:20
from the old location to the new location
14:22
every time that you deploy out,
14:23
which could be time consuming and resource intensive.
14:27
Instead, what you may wanna do is dot dot slash
14:29
on your storage, which would give you a fixed location
14:31
outside of your application route
14:32
that you could store your files.
14:34
Another thing that you're gonna wanna keep in mind
14:35
as you're working with files
14:36
and especially your directory system
14:38
is ensuring that you're not giving users access
14:40
to be able to search through your file system.
14:43
So here we have a route specifically to /avatars
14:45
with a file name route parameter
14:47
that specifically reaches into our storage avatars location.
14:50
This could potentially open you up to the possibility
14:52
of somebody reaching inside of your application
14:54
going to something like say localhost3333/avatars
14:58
to reach a valid avatar URL slash.
15:01
A user could attempt to do dot dot slash
15:03
to go back a directory,
15:04
which would then be our storage dot dot slash again
15:07
to go into the root of our application
15:08
and then reach for something that we have inside of there,
15:11
say something like readme.d.
15:13
With how we've written things in AdonisJS itself,
15:15
we're protected against this,
15:16
but it is something to keep in mind.
15:18
So like if we were to hit enter here,
15:20
those dot dot slashes and everything before it
15:22
are gonna get stripped out
15:23
and we're not gonna be able to find a route for this,
15:25
but just keep that possibility in mind,
15:27
be sure to check against it as well.
Join The Discussion! (5 Comments)
Please sign in or sign up for free to join in on the dicussion.
sp-dev
Can you please tell me how to upload image in s3 in adonis js 6 please i am unable to do this because v5 methods are not working Thanks
Please sign in or sign up for free to reply
tomgobich
Answered on YouTube, but will answer here as well:
Hey there! The updated version of Drive is coming soon to AdonisJS 6, and it's called FlyDrive! As of now, the package is ready, they're just working to add the tight integration directly into AdonisJS 6. So, you can configure it and use it in AdonisJS 6 now, then swap to the direct implementation once it's released if you wish. Alternatively, you can always directly use the S3 SDK provided directly from Amazon. You can find the documentation for it here: https://flydrive.dev/docs/introduction
We do plan on doing lessons on FlyDrive in the future!
Please sign in or sign up for free to reply
guy-aloni
Thanks Tom for the tutorial!
I am trying to implement upload action that will work in localhost with file-system, and in production with s3. Can you please tell how to do it with minimum code? I guess that using
DRIVE_DISK
in.env
file should be enough, but I do not manage to have the same code working for both s3 and local file system.Please sign in or sign up for free to reply
tomgobich
Thanks for watching, Guy!
For that, I'd recommend using Drive (FlyDrive). Its goal is to provide a unified API regardless if you're working with the local disk, S3, GCS, etc. We, unfortunately, don't have any lessons on FlyDrive of yet but the documentation is pretty great! With it, the only thing you should need to change between local and production is that
DRIVE_DISK
environment variable and the APIs between local and S3 will be the same (bar any advanced situations).Drive is a wrapper around FlyDrive specifically integrating FlyDrive with AdonisJS. Both, written and maintained by the AdonisJS core team.
Please sign in or sign up for free to reply
guy-aloni
Thanks. I tried it but got problems with relative and absolute path, and could not manage to have the same code working with both
fs
ands3
depending on the environment argumentDRIVE_DISK
. I understand that basically the same code should work for both, I will give it another try :-)Please sign in or sign up for free to reply