Python FastAPI Tutorial (Part 12): File Uploads - Image Processing, Validation, and Storage
34:30

Python FastAPI Tutorial (Part 12): File Uploads - Image Processing, Validation, and Storage

Corey Schafer 27.02.2026 6 321 просмотров 240 лайков

Machine-readable: Markdown · JSON API · Site index

Поделиться Telegram VK Бот
Транскрипт Скачать .md
Анализ с AI
Описание видео
In this video, we'll be learning how to handle file uploads in FastAPI using UploadFile. We'll allow users to upload profile pictures by building an image processing utility with Pillow, adding proper validation for file type and size, and saving processed images to disk. We'll also cover important concepts like using run_in_threadpool to handle CPU-bound work in async endpoints, generating secure filenames with UUID, and sending files from the frontend using FormData. By the end of this tutorial, users will be able to upload, preview, and display profile pictures across the application. Let's get started... The code from this video can be found here: https://github.com/CoreyMSchafer/FastAPI-12-File-Uploads Full FastAPI Course: https://www.youtube.com/playlist?list=PL-osiE80TeTsak-c-QsVeg0YYG_0TeyXI Pathlib Tutorial - https://youtu.be/yxa-DJuuTBI ✅ Support My Channel Through Patreon: https://www.patreon.com/coreyms ✅ Become a Channel Member: https://www.youtube.com/channel/UCCezIgC97PvUuR4_gbFUs5g/join ✅ One-Time Contribution Through PayPal: https://goo.gl/649HFY ✅ Cryptocurrency Donations: Bitcoin Wallet - 3MPH8oY2EAgbLVy7RBMinwcBntggi7qeG3 Ethereum Wallet - 0x151649418616068fB46C3598083817101d3bCD33 Litecoin Wallet - MPvEBY5fxGkmPQgocfJbxP6EmTo5UUXMot ✅ Corey's Public Amazon Wishlist http://a.co/inIyro1 ✅ Equipment I Use and Books I Recommend: https://www.amazon.com/shop/coreyschafer ▶️ You Can Find Me On: My Website - http://coreyms.com/ My Second Channel - https://www.youtube.com/c/coreymschafer Facebook - https://www.facebook.com/CoreyMSchafer Twitter - https://twitter.com/CoreyMSchafer Instagram - https://www.instagram.com/coreymschafer/ #Python #FastAPI

Оглавление (7 сегментов)

Segment 1 (00:00 - 05:00)

Hey there. How's it going everybody? In this video, we're going to be learning how to handle file uploads and fast API. Specifically, we'll be allowing users to upload profile pictures in our application. So, at this point, we've basically built a complete application. Users can register, log in, create post, edit post, delete post, and we have an account page where users can manage their profile. But if you remember on this account page, we have a profile picture section that says coming soon. So you can see that the profile picture section is just disabled. It says that it's coming in a future tutorial. So that's what we're going to be implementing here. So by the end of this video, we'll be able to upload a real image using form data. We're going to process that image with Pillow. So we'll resize it, convert it to a consistent format, and a few other things for storing it. We'll save it to the disk with a unique file name and we'll update the user so that the profile picture shows on the account page and alongside their post. So with that said, let's go ahead and get started. So first we need to install Pillow. So Pillow is the standard Python imaging library. Pillow allows us to do things like resize images, convert formats, and a bunch of other processing tasks. And that's exactly what we need. So we'll install that. I'm here in my terminal within my fast API blog project here. So if you are using pip, then you can do a pip install pillow. But as I've mentioned throughout the series, I am using UV. So I will do a UV add pillow. So I will go ahead and run that. And that is installed. Now one thing that I should mention is that for file uploads to work in fast API, you also need Python multiart. But if you installed fast API with the standard extras like we did earlier in this series, then Python multiart is already included. So we don't need to install anything extra for that. All right, so let's go ahead and start writing some code here. So we're going to create a new file for our image processing utilities. So this keeps our router file clean and makes the image processing code reusable. So I'm going to create a new file over here and let's just call this image utils and I am in the root of my project here. So image utils. py. And now let's start with the imports. So, first I'm going to import uu ID. And then from IO, we are going to import uh bytes. io. And I'll go over all of these here in just a sec to uh explain exactly what we're using them all for. Uh but from pathlib, we will import path. And then from pil, which is pillow, we are going to import image. And we are going to also import image ops. Okay. So now we have uyu ID for generating unique file names. We have bytes io for working with image bytes in memory from uh we have path from path lib for file operations. And then from pillow we import image uh for the main image functionality and image ops for some convenient image operations. So now let's define a constant where we will uh be storing our profile pictures for now. So I'll say profile picks directory and we will set this equal to a path and if you remember where we wanted these located was uh for/media slprofile pickics. Okay, so I'm using pathlib here for file handling which is a bit more modern in Python. If you're not familiar with pathlib then I do have a video on that as well and I'll leave a link to that video in the description section below. But basically, Pathlib gives us a nice objectoriented way to work with file paths instead of just using string manipulation. Now, if you remember from earlier in the series, we have a user model that has an image path property and that returns either uh media profile pics with their profile picture file name if they have an image file set. But if they don't, then it just returns the static profile pics default. jpeg JPEG if they don't. So in main. py, we're already mounting this for/media directory here as static files. So anything that we save into this media profile pix directory will be accessible by the browser. Now before we write this next function, a quick reminder from our synchronous asynchronous tutorial that we did with fast API. So our fast API app is currently async but image processing with pillow is CPUbound work. So if we do CPUbound work directly in an async endpoint then it blocks the event loop and nothing else can be processed. So we need to be mindful of that. So the solution that we're going to use is to write this as a regular synchronous

Segment 2 (05:00 - 10:00)

function and then call it using run in thread pool which offloads it to a separate thread. And I'll show you exactly how this works when we build the endpoint. So now let's go ahead and create our main function that we're going to be using for processing profile images. Now this is a bit of a longer function. So I'm going to grab these from my snippets, but we'll go over everything after I paste these in. And I forgot to open my snippets here. So let me open these up here. And let me grab this first function here and paste this in here. So let's walk through what this function does. So this is all common image processing stuff. So I'll keep the explanation brief since our focus is really on the fast API side. So first we open the image from the bytes that we received. We use a context manager. So pillow cleans up our resources when we are done. If the image isn't a valid image then pillow will raise an unidentified image error which will catch in our endpoint. We're using the variable name original here for the image that we open and then image for the version that we're working on after transformations. Now, just a couple of short small things here. This XI if transpose here fixes orientation issues. Photos taken on phones often have metadata that says rotate me 90°. And if we don't handle that, profile pictures can appear sideways. This image. fit fit resizes and crops the image to exactly 300 by 300 pixels while maintaining the aspect ratio. This LANCOS method here gives us a high quality resampling. Then we convert it to RGB if needed because some formats like PNG support transparency but JPEGs don't. So we need to handle that before saving it. Uh then we are generating a unique file name here using UU ID4. This is important for security. We completely ignore whatever file name the user uploaded. We generate our own random file name which prevents file name collisions and some other security issues. And we make sure that our directory exists with this make dur method here. Uh parents equal to true to create parent directories if needed. And exists okay equals to true so that it doesn't error if it already exists. And finally, we save as a JPEG with a quality of 85 and optimize equal to true, which is a good balance of quality and file size. And the function returns just the file name, which is what we'll store in our database. So now let's add a helper function for deleting profile images. Uh we'll need this when users upload a new picture to replace their old one or when they delete their account. And I will grab this from my snippets as well. So this one is very short. Let me grab this and paste it in here. So this one is very straightforward. If the file name is none, then we just return early. Otherwise, we build the full path and delete the file if it exists. So we are using this uh path. unlink for deletion, which is the path lib way of deleting a file. All right. So that's our image utils uh python file there. So let me save that. Now before we create our endpoint, let's add a configuration setting for the maximum file size. Now this is a good practice because it centralizes our configuration and makes it easy to change later. So if I open up our config py here, if you remember from earlier tutorials, this is where we are using paidantic settings to have all of our configurations. Uh I will add this after our existing settings here. Let's just call this uh max upload size and we will specify that this is in bytes is going to be an integer and let's put a default here. I will do 5 * 10 20 24 and time 1024. So this is just a default of 5 megabytes here. So that's what 5 * uh 1,024 * 1,024 is. So this should be plenty for profile pictures and it help protects our server from huge file uploads. So I'll go ahead and save that configuration. And now let's create the actual upload endpoint. So we're going to do this in our users router. So let me look in our routers here and we will open up our users router. And first let's update our imports here at the top. So, we're going to need a few things here. So, I need to add upload file to our fast API endpoints or imports here. So, that is going to be upload file. Now, my editor is set up to autosort imports here. So, whenever I save, you may see these jump around. Uh

Segment 3 (10:00 - 15:00)

don't worry about that. So, now I need to add the import for catching invalid images from pillow. So, I will just add this here. I'll say from pil we want to import and that is going to be unidentified image error and we can see that got put there and now I also need uh run in threadpool from starlet so that is going to be from starlet concurrency we are going to import run in threadpool okay I'll save that and finally I need to import our image utility functions that we just created so from image utils we want to import. Uh we have that delete profile image function and we also have that process profile image function. So I'll save that and it put those down there. And we already have our settings imported here from a previous tutorial. So the file size limit that we just added will be available through that. All right. So now let's scroll down here and add our new endpoint. So, we're creating a separate endpoint specifically for profile pictures uh because file uploads use multi-art form data uh while our existing patch endpoint uses JSON. So, keeping these separate is cleaner. So, I'm just going to add it down here at the bottom. Now, again, this is a bit of a longer endpoint. So I'm going to grab these from my snippets here, but I will go over everything so that we are sure that we understand exactly what's going on here. So I will paste this in at the bottom. And now let's take a look at this. Okay, so we can see that we're using router. patch. Now we're using patch because we're updating an existing resource which is the user's profile. Now the signature here takes the user ID from the path. It also gets the current user from our authentication dependency. And we also have the database session here. And then we have this file parameter here of the type upload file. Now this is a special uh fast API type for handling file uploads. When you upload a file, the browser sends it using a content type called multi-art form data instead of JSON. And Fast API handles all of that for us through this upload file object here. Now upload file gives us a lot of useful attributes and methods. So for example I could say file. file name that gives us the original file name from the client. Uh filecontent type gives us the uh mime type. Now we can't fully trust that content type that we get from the client and we'll talk more about that here in just a second. Uh we could also use files size to give us the file size and uh file read reads the file contents in the bytes. And there are others as well but those are the most common ones. So let's move on to the function code here. Now we are checking authorization here. So that's the same pattern that we saw in earlier tutorials. So only uh the user can update their own profile picture. Next we read the I think I may have scrolled past it here. Yep. uh we read the file content with await file read and then we check the size using the length of that content. So we use lin content rather than files size because files size isn't always reliable until after you've read the file. Uh if the file is too large, we return a 400 bad request here. Now here is where we do the actual image processing and this ties back to our asynchronous tutorial. So image processing with pillow is CPUbound work and doing CPUbound work directly in an async endpoint would block the event loop. Now normally you'd use a regular synchronous function for CPUbound work and fast API runs it in a thread pool automatically but we need this endpoint to be asynchronous so that we can await our database calls. So the solution here is to use run and threadpull from Starlet. So we wrap our CPUbound process uh profile image function in this run and thread pool function here which offloads it to a thread pool while keeping our endpoint asynchronous. So we get the best of both worlds here. Now if the file is not an image then pillow will raise this unidentified image error which we can uh catch here and convert to a 400 bad request error. And now we have an order of operations down here at the bottom. And this is important. So the image has already been saved by the process profile image function here. So first we store the old file name. So we can see that we're doing that here. And then we update the database with the new file

Segment 4 (15:00 - 20:00)

name. And then we commit. And only after a successful commit do we delete that old profile image. So why do we do it in this order here? Uh because if we deleted the old file first and then this database commit failed for some reason then we would lose the user's profile picture entirely. But by saving the new file first and then uh deleting the old file last. We protect against that. So in the worst case uh scenario we might end up with an orphan file on disk but that's better than losing someone's profile picture entirely. And then finally here at the bottom we are just returning this updated user which is the current user and that is going to include the new image path that our front end will use. Now one thing worth noting here uh do you remember how I mentioned that we can't fully trust the content type from upload file? Well, that's why we are validating here with Pillow instead. We are actually uh trying to open the file as an image and if Pillow can't identify it, then we reject it. And that's a much more reliable check than trusting what the client tells us. Okay, so now let's add an endpoint for deleting profile pictures. So users should be able to remove their profile picture and go back to the default if they want to. So I will also grab this from my snippets here. So delete profile picture endpoint here. This one is a lot shorter. So let me grab this and we will go over this as well. So this follows the same pattern here. So we check our authorization first and then we check that there is actually uh a profile picture to delete here. If not then we return a helpful error. And then we update the database to set the image file to none, which will cause our model to return the default image path. And after the commit succeeds, we delete the actual file here. And then again, we are returning that updated user, which is just going to be that current user. Okay. So, there's one more thing that we need to update before we can test this out. So, when a user deletes their entire account, we should also delete their profile picture uh file as well. Otherwise, we'd leave orphan files on disk. So, let me find the delete user endpoint here. So, that is delete user. So, that is this endpoint here. So, we just need to add two things here. So, right before the database commit here, I am going to uh set the old file name here. So, I'll say old file name is equal to and that is going to be user. image file. And then after that commit then we will say if we had an old file name then we want to delete the profile image that is the function from our image utils that we had there and we would will delete that old file name there. So it's that same safe order of operations. we uh don't delete it here because we're waiting for that database commit first and then after that commit that is when we are deleting that image. Now we also have an important security fix to make here. If I look in schemas py. So let me open up our schemas file here and then go to our users. If we look at our user update schema here, we can see that it currently includes this image file as a field that can be updated. Now, this was fine before when we didn't have any upload functionality and wanted to test this manually, but now it's a security issue. So for example, if a user can set their image file uh to any string through the patch endpoint, then they could set it to another user's file name and then delete it through the delete picture endpoint. So we need to lock this down here. So we just need to remove image file from uh user update entirely here. So let me remove that because now that we have those endpoints uh profile pictures should only be changed through the upload or the delete endpoints and those will handle the validation. Now since I changed that schema that also means that I need to go back to our users router here and remove the handling of image file from the update user endpoint. So that is update user. Let me find that and this is going to be very simple here. All I have to do is find where we have the image file here. And I'm just going to remove this because uh we no longer have that in our schema. So there's nothing to update there. So I will just remove that there. And there we go. So now this update user endpoint only handles username and email. Profile

Segment 5 (20:00 - 25:00)

pictures are handled separately through our new endpoints that we just created. All right. So at this point, this should be everything that we need to get this working on the back end. So let's test what we have so far before we touch the front end. So let me make sure that our server is running here. And it is. So let's go to the browser here. And let me load up our docs here. And first, just so I don't have to uh restart the server, I need to make sure that the uh new endpoints are there. And they are. There's our upload profile picture and delete user picture there. Now, I should already have a user that I created in the last tutorial. If you don't have any users, then you'll need to create a new user that we can log in as. I'll go ahead and log in as my test user here. So, I'm going to click authorize. Now, for the username, we are using emails. So, that was coreymshafergmail. com. For the password, I believe that was test password one. Okay. Okay, so now I should be authorized here. If I check the get current user route, then we can see that I am logged in there. Now let me go to the upload profile picture route here. If I look at our front end really quick, then we can see that my current profile picture is just this default with this silhouette person there. So now let me go to upload profile picture and try it out. Our user here is a user ID of one. And now for the file here, let me choose file. And now I have put this populate images directory in my project where I've added a bunch of different test images here. So first let me just try this user one. png here. And let's execute that. And we can see that we got a 200 response here. And we can see that it's given us the image file and the image path. So that should have worked. So now let me check the file system here. If I go back to our project here and I look at our media directory, then we can see that we have this long uh hex image here. And if I click on that, then we can see that what we uploaded was a PNG, but this is a JPEG here. Now, I don't know how to view the exact pixels in VS Code, but just to my eye, I think that this seems to be a 300 by 300 pixel image. So, I'm going to assume that resizing worked. So, all of our image processing seems to be working correctly. So, now let me upload a second image here and make sure that this old one gets cleaned up because remember uh we are deleting old images as well. So if I go back here, let me try to upload a new profile picture. So I will use this user 2. png here. This is just a picture of my dog. I will execute that. That seems to have worked. So now if I go back here, we can see that now uh that old image with that old uh with the hex there is gone. Now we have this new image here. Uh this one is from a previous tutorial and we can see that uh did not get resized. So that one is a lot bigger. I can actually just delete this one. This didn't go through and actually delete uh images from previous tutorials. Our cleanup is just deleting the current user's profile picture with the new one. But we can see that this worked as well. Uh I don't want to zoom in there. Uh okay. But we can see that now that is the only image in that profile pictures directory. So it does seem to have deleted that old image. So that's working well. So now let's test uh some error cases here. So now let me try uploading a file that's too large. So I have a JPEG here that I've labeled as 6 megabytes. Uh we can see that it's 6. 2 megabytes as the size there. Now we set a limit of 5 megabytes. So it should tell me that this image is too large. So if I execute this then we can see that we got a 400 request or response here and it says that the file is too large. Maximum size is 5 megbytes. And lastly let me try uploading a non-image file here and see what it does. So I have this user 7. txt here. Let me open that and execute. And let's see what we get here. We can see that we get another 400 response and it says invalid image file. Please upload a valid image and then gives us a few different extensions there. Okay, so the backend seems to be working great. So now let's update the front end so users can upload from the account page and not

Segment 6 (25:00 - 30:00)

just the backend using the docs here. So now let me go back to my code here and we will close those down. Now for our front end to support file uploads, we are just going to need to update the account template here. So let me open up the account. html template here. And now I just need to find the place where we say that the uh profile pictures are coming soon. So that is right here. So we're going to replace this entire section with a functional upload form. Now, just like with the rest of the series, I'm not going to focus too much on the HTML since this is more of a fast API tutorial, but let me grab this from my snippets and I will briefly discuss it after we paste this in. So, for the div here, I will grab that and I will replace that. So, let me explain what we have here. So, first there is a preview container here with an image tag. Uh this starts hidden with this dnone class here. But when the user selects a file, we'll show a preview before they upload. The object fit cover class here. Just make sure that the preview looks good, even if the image isn't perfectly square. And then we have this file input here with this accept image here. This tells the browser to only show image files in the file picker. It's not security, it's just convenience for the user. And then we have an upload button here uh that starts as disabled and it'll be enabled once a file is selected. So finally there is a small text note here that just says that what our maximum file size is and what we are supporting. So now let's add the JavaScript to make this work. And I'll add this to the script section. Uh so let me scroll down here to our scripts and we will put it let's see I will put it here after this uh load user data function here. So yeah let's put it around line 159 here. That'll just be with some of the other event listeners there. So from my snippets here, let me grab this section here and I'll discuss this after we paste this in. So let me grab that and we will add that to our scripts there. And let me make sure that the tabs are consistent there. Okay. So this code here uses the file reader API uh to show a preview of the selected image before the user uploads it. And when they select a file, we read it as a data URL and set that as the source of our preview image and show the preview and we also enable the upload button. Uh if they clear the selection, then we hide the preview and disable the button again. So that JavaScript there is mostly just image preview stuff. Now let's add the upload handler here. So from the snippets again, this is going to be the profile picture handler. So and we can see that this is also the last snippet that I have here. And I will paste that in here. Let's save that. And let's take a look here. Now, if you've been following along in the series, then this is just some stuff that we've already seen here. We're making sure that there is a token and redirecting them to the login if there is not. Now, two things worth pointing out here. First, we're using form data here instead of JSON. So, for file uploads, we need form data and we append the file with the key file here to match our endpoint parameter name. And second, notice that we are not setting the content type header. So, we can see that I've left a comment here uh that specifically says that. So when you use form data, the browser automatically sets content type to multi-art form data with the correct boundary string. If we set it ourselves, then it would break. So we still include the authorization header here for authentication. The rest of this is the same error handling pattern that we've been using in the HTML and JavaScript throughout the series. So we can see that if we get a 401 then we go to the login page for a 403 we give an error message. Now on success here then we just clear the user cache uh in case the user data is cached elsewhere in the app. We up update the profile image display. We clear the file input hide the preview

Segment 7 (30:00 - 34:00)

and show a success message. And in the finally block down here, we reenable the button and reset its text. Now again, if you are following along with the front-end stuff, even though I'm not explaining this as much as I explained the backend fast API stuff, I do have all of this available for download in the description section below. Uh, so now let me go ahead and save this. And now let's test the front end. So let me go to the front end here. So first I need to log in. So I will go to login here and I will log in with my test user that I created before and that is test password login. That is successful. And now let's go to the account page here. And now let's try to update our profile picture within here. So I'll go to choose file and I will grab that user 1. png there. And we can see that it gives us the preview. And now let's upload that. It says profile picture updated successfully. So we can see that it reloaded that. Now let me go ahead and refresh this page to make sure that it is still there. And it is. And lastly, let me go to the homepage here and make sure that this is showing up beside our post. And we can see that it is. So that change was saved to the database and the image shows up with all of our user information here. Okay, awesome. So now we have that file upload capability working on the back end and our front end here is using that backend functionality uh so that we can update our profile picture on the front end here as well. Okay, so let's recap a bit of what we did in this tutorial. So we installed pillow for image processing. We created image processing utilities that resize images uh convert them to a JPEG and a few other things. We used that run in thread pool to handle CPUbound work to work properly and asynchronous endpoints. We built the file upload backend endpoints with proper validation. We updated the front end here with an image preview functionality and proper form data handling. And that basically completes our file upload and profile picture capability for our fast API app that we have here. Now, one quick note on production. Uh if I go back to the code here, we can see that we are saving this on disk here. So, we have these images here uh saved in this media folder. So, one quick note on production. Uh what we've built here works great for development and small-scale applications, but for production at scale, you'd typically want to use object storage like Amazon S3 or Google Cloud Storage instead of storing files locally. You'd also want a CDN for serving static files efficiently. And I'm actually going to have a tutorial on this later in the series so we can see exactly how to do that. Now, in the next tutorial, we're going to be looking at pageionation. So as our application grows, we don't want to return all the posts at once. And this is really more of a back-end topic than a front-end one. So we'll be adding query parameters to our API endpoints so clients can request specific pages of data and we'll handle that logic in our database queries. Then we'll add some basic pageionation controls to the front end to request a certain number of posts at a time and take advantage of that backend functionality. But I think that's going to do it for this video. Hopefully now you have a good idea of how to handle file uploads in fast API. In the next video, we're going to be looking at that pageionation to handle large data sets efficiently. But if anyone has any questions about what we covered in this video, then feel free to ask in the comment section below and I'll do my best to answer those. And if you enjoy these tutorials and would like to support them, then there are several ways you can do that. The easiest way is to simply like the video and give it a thumbs up. Also, it's a huge help to share these videos with anyone who you think would find them useful. And if you have the means, you can contribute through Patreon or YouTube. And there are links to those pages in the description section below. Be sure to subscribe for future videos, and thank you all for watching. [groaning]

Другие видео автора — Corey Schafer

Ctrl+V

Экстракт Знаний в Telegram

Экстракты и дистилляты из лучших YouTube-каналов — сразу после публикации.

Подписаться

Дайджест Экстрактов

Лучшие методички за неделю — каждый понедельник