Understanding django's file upload

62 阅读2分钟

Setup

First of all we write a upload endpoint:

path('upload/books/', handle_upload_books)

@api_view(['POST'])
def handle_upload_books(request):
    books = request.data.get('books')
    files = request.FILES.get('files')

Let us check files:

image.png

MemoryFileUploadHandler is django's default uploadhandler.

But How to save it on server disk? stackoverflow had given me a easy demo.

image.png

It works but save the file into our app folder. We need to debug the save method to find out what happened.

image.png

image.png

Now we know how it generate save path.

image.png

After some complex file operation it will give us a new filename which we need to save into databse. (This is my second time to debug, so it will concat a unique key after filename)

image.png

Before save this new filename into databse. We also want to support mulitple files upload.This files are MultiValueDict so we need use getlist to get files.

image.png

@api_view(['POST'])
def handle_upload_books(request):
    books = request.data.get('books')
    files = request.FILES.getlist('files')
    for file in files:
        file_dir = default_storage.save(file.name, file)

    return Response(status=status.HTTP_200_OK)

Insert databse for uploaded Books

Now we need to create some book instance then insert them to databse.

Add a filename field into Book model.

class Book(TimeStampedModel, models.Model):
    title = models.CharField(max_length=100)
    author = models.TextField(null=True, blank=True)
    description = models.TextField(null=True, blank=True)
    imageUrl = models.TextField(null=True, blank=True)
+   filename = models.TextField(null=True, blank=True)
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)

Don't forget to migrate~

(venv) (base) PS F:\projects\ulibrary-django-server> python .\manage.py makemigrations
Migrations for 'ulibrary':
  ulibrary\migrations\0007_book_filename.py
    - Add field filename to book           
(venv) (base) PS F:\projects\ulibrary-django-server> python .\manage.py migrate       
Operations to perform:
  Apply all migrations: admin, auth, authtoken, contenttypes, sessions, ulibrary
Running migrations:
  Applying ulibrary.0007_book_filename... OK

Then create instances:

image.png

Very ugly but it works.

A better way to save file (FileField)

First modify our Book model:

class Book(TimeStampedModel, models.Model):
    title = models.CharField(max_length=100)
    author = models.TextField(null=True, blank=True)
    description = models.TextField(null=True, blank=True)
    imageUrl = models.TextField(null=True, blank=True)
-   filename = models.TextField(null=True, blank=True)
+   filename = models.FileField(null=True, blank=True)
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)

We can use admin to test it:

image.png

Success! But how to apply this into our /upload/book/ endpoint to support multiple create?

image.png

Raise a error:

 "filename": [
        "The submitted data was not a file. Check the encoding type on the form."
    ]

I don't know why but looks like my file is allowNull:

image.png

I also tried this:

image.png

Same error! 😢

image.png

**TemporaryFile object has no attribute size! I stuck in here some hours. **

Lucky, I findout the solution:

image.png

Temp file is already saved on our disk so we need to read this file first. Now it works! (But the code are still ugly 🤣)

When we debug into TemporaryFileUploadHandler we can find out it really create a file temporary(not)

image.png

Django Settings

We want temp files and uploaded files are both in our project folder so we can check and delete them easily.

+ FILE_UPLOAD_TEMP_DIR = BASE_DIR.joinpath("temp")
+ MEDIA_ROOT = BASE_DIR.joinpath("upload")

Test Case

def test_upload_book(self):
    # set header to mock login
    token, created = Token.objects.get_or_create(user=self.defaultUser)
    client = APIClient()
    client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
    test_filename = 'The_Sense_of_Style.epub'

    test_file_dir = Path(__file__).resolve().parent.joinpath(test_filename)

    file_obj = DjangoFile(open(test_file_dir, mode='rb'), name=test_filename)
    data = {
        "books": json.dumps([{"title": "upload_book_1"}]),
        "files": [
            file_obj
        ]
    }
    response = client.post('/upload/books/', data=data)
    self.assertEqual(response.status_code, status.HTTP_200_OK)

    # remove created file
    test_file_path = Path(__file__).resolve().parent.parent.parent.joinpath('upload', test_filename)
    if test_file_path.exists():
        test_file_path.unlink()

    created_book = Book.objects.filter(title='upload_book_1')
    self.assertGreater(created_book.count(), 0)
    self.assertTrue(bool(created_book.first().filename))

Thanks for reading!