Playing Next Lesson In
seconds

Transcript

  1. So whenever we were doing our avatar upload, we created one route specifically for rendering out our avatar images. But if we jump over to our movie poster now,

  2. we need to add the ability to render this image as well. Now, we could of course replicate what we did for our avatars,

  3. which was have a specific route for it that reads from the storage, or we can create a single route that will take care of both of these and any future use cases that may arise.

  4. For this, we're going to want to switch the actual path of the image. So our avatars and the file name of the avatar,

  5. our posters and the file name of the posters to a wildcard route parameter. This will allow us to insert anything as route parameters, including multiple route parameters as well,

  6. and they'll all be ingested as this star character. Now, of course, if we left this as is, we would no longer be able to have any of our subsequent get routes down below.

  7. So we need some form of a prefix specifically for this directory, and for that, storage is probably the most applicable since we're putting all of our files inside of a storage directory.

  8. So let's also refactor our avatars controller into a storage controller. So we'll scroll over to here, rename this,

  9. go down, rename, replace avatars with now storage. We go ahead and hit "Yes" to have that auto-update the imports. Jump into our storage controller,

  10. let's also rename this from avatars controller to storage controller. Jump back into our routes. I'm going to scroll up, make sure that that actually changed. So the import path looks right.

  11. Let's now switch this to storage controller, scroll down, update this here. So I'm going to highlight that and Command Shift F to search for it.

  12. It looks like that's the only place that that name is being used. So let's go and switch this to storage.showto. Now, this change has immediately invalidated all of the images that we've saved manually so far,

  13. which is just our one profile image and that one cover image that we uploaded in the last lesson. So that's no biggie. We'll take care of that here in a little bit. First though, let's go ahead and update our storage controller

  14. because this is currently relying on a route parameter called filename, and it's specifically looking inside of our avatars directory. Let's temporarily comment out our download.

  15. Instead, let's just return back params, and let's take a look at params star to see exactly what we're getting here. Let's jump back into our browser real quick.

  16. I'm going to open up another tab here, go to localhost3333/storage/,

  17. and let's try avatars/myfilename.png. Hit "Enter" there, and you can see that we get two parts.

  18. So this is coming through as an array value. Take a look at the raw data, you can see exactly that. So we have the first index being avatars,

  19. and the second index being myfilename.png, split by that slash. If you're curious about why it's rendering out this way, this is Firefox's JSON preview.

  20. So that's why you're seeing it like this here. So this will look different if you're looking at it inside the different browser. Cool. So with that, what we should be able to do now is hide this away,

  21. and discern that we can get the const file path as params star.join/.

  22. So let's go ahead and return file path real quick just to make sure that that looks as intended. Jump back into our Firefox, refresh.

  23. Now, we just get back a single string of avatars/myfilename. It's no longer an array. Cool. Now, for this next portion, I'm following along with the actual AdonisJS documentation,

  24. so you can do this as well. We jump into file uploads. It's underneath basics file uploads here. There's a section for serving files. An alternative approach to what we've done is you can also read

  25. directly from request params for star, and join that together with your separator as well. You'll see that they also protect themselves against path traversal.

  26. Here they have a path traversal regex string. That's essentially going to make sure that nobody's going to try to traverse into your actual application files,

  27. which if you were to allow, would potentially lead you to vulnerabilities and potential secret leaks.

  28. So for example, somebody could try to do ./ here to go back out of the storage directory which would then be our project route and do

  29. something like .emv to try to read from and get your EMV file, which holds all of your project secrets and environment variables. Now, if we were to actually try this,

  30. that's going to get stripped out and it's just going to switch to .emv. The router has some built-in protection there. But it's always good to check against this, especially if you're allowing dynamic route parameters.

  31. So I'm going to go ahead and give this path traversal regex here a copy, hide our browser back away, and I'm going to paste it right up here outside of our actual controller just as they had it.

  32. We can also normalize our file path using Node.js. So we can do const normalized path equals, type out normalize,

  33. and you'll start to see that we get some auto-populated options. What we want is the one from path. This is really Node.js path. So we can go ahead and import that.

  34. We can call that as a function and pass in our file path that we've joined together from our route parameters. If we hover over this to see what it's doing, you can see that it's normalizing the string path,

  35. reducing dot dot and dot parts, getting rid of multiple slashes with just a single one we're found and using backslashes on Windows. Once we get that normalized path,

  36. what we'll want to do is check it against our path traversal regex. So we'll do if, type out path,

  37. traversal regex dot and test that regex against our normalized path. If that test passes,

  38. so if it found a match for this regex, then it is a request that contains path traversal attempts. So we'll want to return response,

  39. and we can say that this was a bad request. Just as in the documentation, we'll say that this is a malformed path. Otherwise, if the test does not pass,

  40. then it's not attempting to do path traversal. So we can bring back into the equation our download. We're no longer specifically going into our avatars. That's going to be a part of our file path now,

  41. so we can get rid of that. But we do still want to look in storage since that is not a part of our wildcard route parameter.

  42. So we'll want to make sure that we append that in, and then replace our params file name with now our normalized path.

  43. That should get us an absolute URL or our response download to our files. As we stated before we started this though, we've invalidated our files because we've now

  44. prefixed this route name with storage. So we can either go through and let's jump down to say our profiles edit,

  45. and prefix the display here with /storage. So for example, with that prefix in place, if we were to jump back into our browser now,

  46. see I think this one contains our avatar preview, and give it a hard refresh, holding Command Shift R, it's going to make sure that it's not cached or anything. We still see our image,

  47. but if I jump back into our text editor and get rid of that storage prefix, jump back into our browser, you can see it's already gone. You can do a hard refresh again just to make sure,

  48. but yeah, it's no longer working. So you have those two options to you. You can either prefix those paths with storage, or as I said,

  49. we can jump up into our profiles controller here, and prefix the avatar URL with that storage like so, and we'll also want to jump into our movie service,

  50. and do the same thing here. So right here we'll want to do /storage/posters and then our file. Sorry, circling back to here for a second. One additional thing that we need to do,

  51. since storage is now directly on the saved URL inside of our database, we also need to update our unlinks. So if we jump back into our profiles controller real quick,

  52. we have storage being prefixed onto our avatar URL, which means that we no longer need it inside of the make path for our unlink to remove it out of our storage directory.

  53. So we can replace storage with just a single dot, to reference that it should be relative to whatever the avatar URL stored value is.

  54. And we also need to do that within our admin, movies controller as well, where we have that unlink here too,

  55. because our store poster is now appending that same /storage on as a prefix. So we want to replace the unlink make path with just the dot there,

  56. to use the relative path from our poster URL. Big thanks to Gribble for pointing that out to me down below in the comments. With that in place, we should now be able to jump back into our browser.

  57. We will need to clear that image out, make sure that worked okay. We'll go ahead and give it a save. Browse, add that image back in, open it up, upload it. And there we go.

  58. So now we can see the previews working again, give it a hard refresh just to verify. And sure enough, can right click it, go down to inspect, and you can see it's reading from storage avatars av.jpg.

  59. Awesome. Next, let's jump over to our movie poster. We'll want to clear this one here out as well. We can update it real quick, jump back into the edit, browse for a poster,

  60. select it, open it up, go down here, update. And now if we jump back into here, look at that, we can see our image. Awesome. We can right click it, inspect,

  61. and it's reading from storage posters, and then it has that unique name, .gif. Awesome. So everything there seems to be working A-okay. And now we have one single path

  62. that we can read any storage files from for our application.

Using A Wildcard Route Param to Download Storage Images

@tomgobich
Published by
@tomgobich
In This Lesson

We'll learn how we can utilize a wildcard route parameter to dynamically download images that've been uploaded and stored within our application storage.

Join the Discussion 8 comments

Create a free account to join in on the discussion
  1. @gribbl

    Hello,

    When I try to access uploaded file, for example /storage/avatars/filename.png, without a route or controller, the file displays just fine. The StorageController isn’t called.

    Finally, I get this error when I try to delete a movie’s poster : 'ENOENT: no such file or directory, unlink', I think the error comes from this line: 'await unlink(app.makePath('storage', movie.posterUrl))'. The generated path seems to be incorrect.

    But I don’t understand why it seems to work for you.

    1
    1. Responding to gribbl
      @tomgobich

      Hi Gribbl!

      If you're able to directly access the file via the URL without going through a route definition, that leads me to believe the file is actually within your public directory. Files within this directory are made accessible by the AdonisJS static file server.

      That would also explain the "no such file" error on the attempted deletion, as app.makePath('storage') is going to be looking in the ~/storage directory directly off your application root, not within the public directory.

      Can't say for certain though - just making an educated guess based on the info provided.

      Hope this helps!

      1
      1. Responding to tomgobich
        @gribbl

        I don’t have a public directory, only storage at the root of the project.

        The line router.get('/storage/*', [StorageController, 'show']).as('storage.show') is commented, and I can still access my downloaded files in that directory. 🤔

        And for file deletion, it seems like the makePath method generates the following path: .../storage/storage/posters/filename.png since posterUrl also contains /posters . So, I used app.makePath('.', movie.posterUrl) instead, and it works. 😁

        1
        1. Responding to gribbl
          @tomgobich

          Sorry for the delayed response! Are you able to share the repo url? Neither should be the case, and I can't immediately think of anything that would cause either of those.

          1
          1. Responding to tomgobich
            @gribbl

            No problemo.

            That's strange. I have the exact same code as you. But just to confirm, if I understood correctly (and correct me if I'm wrong), when we save a movie's poster, we use the following lines in the movie service:

             await poster.move(app.makePath('storage/posters'), {
                  name: fileName,
                })
            
            return `/storage/posters/${fileName}`
            Copied!

            So, the file is saved at storage/posters, and in the database, poster_url will have the value /storage/posters/filename.png.

            And when we want to delete the poster, we do this:

            await unlink(app.makePath('storage', movie.posterUrl))
            Copied!

            So, the generated path will contain storage twice. As a result, since no file is located in storage/storage/posters, the deletion will not work. Right? Or did I misunderstand?

            1
            1. Responding to gribbl
              @tomgobich

              I'm so sorry, my head was in the wrong spot! You're absolutely right, the /storage gets added to the saved value in the database so the unlink should be:

              await unlink(app.makePath('.', movie.posterUrl))
              Copied!

              Terribly sorry for the confusion there! I'll make a note to correct this lesson.

              1
              1. Responding to tomgobich
                @gribbl

                No problem at all. Thank you for your response.😊

                1
                1. Responding to gribbl
                  @tomgobich

                  Anytime! 😊

                  0