I'm creating a Rest API with Django Rest Framework, and in some call, I need upload one of different file types.
For do this, I am using drf-extra-fields and I have this problem:
When the client convert file to Base64, the filename change and lose the file extension, for suply this, I created in serializer, new CharField called "file_extension":
class MySerializer(serializers.ModelSerializer):
# Some other fields...
file = FileField(required=False)
file_extension = serializers.CharField(required=False)
class Meta:
# model and fields...
I created my custom Base64FileField, and set some allowed types and my method for get file extension:
class FileField(Base64FileField):
ALLOWED_TYPES = ['pdf', 'txt', 'xml']
def get_file_extension(self, filename, decoded_file):
# Not working because the filename not include extension.
return filename.split('.')[-1]
For solve this, I thinked set filename in serializer with file extension (taked for the other field) before get_file_extension is called.
Thanks you!
Use filetype to identify the type of the decoded_file
filetype - PyPI
import filetype
class FileField(Base64FileField):
ALLOWED_TYPES = ['pdf', 'txt', 'xml']
def get_file_extension(self, filename, decoded_file):
kind = filetype.guess(decoded_file)
return kind.extension
Related
what else do I need to add to that "file = models.FileField()"
this is what I have done but am still not getting any results, why that?
class Course(models.Model):
TOPIC_CHOICES = (
("History", "History"),
("Chemistry", "Chemistry"),
("Computer", "Computer")
)
lecturer = models.ForeignKey(Lecturer, on_delete=models.CASCADE)
category = models.CharField(choices=TOPIC_CHOICES, max_length=100)
topic = models.CharField(max_length=250)
file = models.FileField()
date_created = models.DateTimeField(default=datetime.now)
def __str__(self):
return f"{self.lecturer}: {self.topic}"
According to Django documentation, FileField takes two optional arguments.
upload_to: Sets the upload directory. The value of this argument can have several types. It can be String, Path, or a callable function. Here is an example:
upload = models.FileField(upload_to='uploads/')
If you want to define a function for this argument which returns the upload directory, you have to define it based on Django's specification of such function. The function should have the instance and filename arguments. Here is an example:
def user_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return 'user_{0}/{1}'.format(instance.user.id, filename)
class MyModel(models.Model):
upload = models.FileField(upload_to=user_directory_path)
storage: A storage object, or a callable which returns a storage object. This argument is used to specify a storage setting for your file-upload field. This argument enables you to choose the appropriate storage environment at runtime.
from django.conf import settings
from django.db import models
from .storages import MyLocalStorage, MyRemoteStorage
def select_storage():
return MyLocalStorage() if settings.DEBUG else MyRemoteStorage()
class MyModel(models.Model):
my_file = models.FileField(storage=select_storage)
Another use-case of this argument is having different storage environments for different types of files.
from django.conf import settings
from django.db import models
from .storages import LargeFilesStorage
class MyModel(models.Model):
my_file = models.FileField(storage=LargeFilesStorage())
As these arguments are optional, you can instantiate a FileField without them. The default values for these arguments are: upload_to='', storage=None
I am uploading two files using django rest framework, i want to upload these two files into the same folder and create said folder with a random name.
So far I can upload both files to different random folders using the following:
from uuid import uuid4
def path_and_rename(path, dataset):
def wrapper(instance, filename):
main_folder = '{}/{}/'.format(path, uuid4().hex)
name = '{}.csv'.format(dataset)
return os.path.join(main_folder, name)
return wrapper
class Dataset(Model):
trainFile = FileField(null=False, blank=False,
validators=[FileExtensionValidator(['csv'])],
upload_to=path_and_rename('files/', 'train'))
testFile = FileField(null=False, blank=False,
validators=[FileExtensionValidator(['csv'])],
upload_to=path_and_rename('files/', 'test'))
class DatasetSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Dataset
fields = (
'id',
'trainFile',
'testFile',
)
read_only_fields = ('created',)
How could I get both files to be uploaded to the same random folder?
You are calling path_to_rename(), and in turn uuid4() on two separate occasions so you're going to get two random UUIDs. As a sidenote, for better code readability, your "wrapper" should be the outer function.
Try:
def upload_to_wrapper(upload_dir, dataset):
def path_and_rename(instance, filename):
return os.path.join(upload_dir, '{}.csv'.format(dataset))
return path_and_rename
class Dataset(Model):
upload_dir = 'files/{}'.format(uuid4().hex)
trainFile = FileField(null=False, blank=False,
validators=[FileExtensionValidator(['csv'])],
upload_to=upload_to_wrapper(upload_dir, 'train'))
testFile = FileField(null=False, blank=False,
validators=[FileExtensionValidator(['csv'])],
upload_to=upload_to_wrapper(upload_dir, 'test'))
Since the first FileField is required and the fields will be uploaded in order, you could use an alternate upload_to function for all of your fields apart from the first that relies on the initial upload path. Something like:
import posixpath
def dataset_file(dataset):
def wrapper(instance, filename):
main_folder = posixpath.split(instance.trainFile)[0]
name = '{}.csv'.format(dataset)
return posixpath.join(main_folder, name)
return wrapper
```
I'm working on a little Django project and I have a filefield in my models.py.
Everything works fine, but I don't like that Django is editing the filenames after upload. It changes spaces into underscores and it removes (square) brackets and stuff like that.
Is there any way to stop Django from doing this?
I can see that it is to make the website safer, more secure and also just avoid errors. But, I'm the only one who's going to be able to upload files anyway.
Hopefully someone knows if (and how) this is possible :)
Thanks!
edit:
Here's the FileField in the models.py:
file = models.FileField(upload_to=file_path)
Here's my upload_to:
def file_path(instance, filename):
extension = filename.split('.')[-1]
new_filename = '%s - %s.%s' % (str(instance.model.object_number), str(instance.model.object_name), str(extension))
return '/'.join(['files', str(instance.model.object_theme), str(instance.model.object_number), new_filename])
Yes, you can write your own upload_to function, as explained in the documentation: https://docs.djangoproject.com/en/1.9/ref/models/fields/#django.db.models.FileField.upload_to
def user_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return 'user_{0}/{1}'.format(instance.user.id, filename)
class MyModel(models.Model):
upload = models.FileField(upload_to=user_directory_path)
If you want to avoid overwriting files with identical names, you have to add that functionality back. The default storage uses this function django.core.file.storage.get_availble_name()
You can read the source code on github
Edit: It looks as though your upload_to function doesn't return anything. It should return a pathname as a string.
import os
def file_path(instance, filename):
base, extension = os.path.splitext(filename)
return '{model.object_number} - {model.object_name}{extension}'.format(
model=instance.model, extension=extension)
You need to create your own storage class
mystorage.py
import re
from django.utils.encoding import force_text
from django.utils.functional import keep_lazy_text
from django.core.files.storage import FileSystemStorage
#keep_lazy_text
def get_valid_filename(s):
"""
>>> get_valid_filename(" tawanda's portrait in 2019.jpg ")
'tawandas portrait in 2019.jpg'
"""
s = force_text(s).strip()
return re.sub(r'(?u)[^-\w. ]', '', s)
class CleanFileNameStorage(FileSystemStorage):
def get_valid_name(self, name):
"""
Return a filename, based on the provided filename, that's suitable for
use in the target storage system.
"""
return get_valid_filename(name)
If you want to apply this storage to all models you can assign the Django setting DEFAULT_FILE_STORAGE with your custom class e.g
DEFAULT_FILE_STORAGE = 'mystorage.CleanFileNameStorage'
If this is only for a particular model then:
models.py
def myfile_save_to(instance, filename):
return 'my_files/{filename}'.format(filename=filename)
class MyFileModel(models.Model):
my_file = models.FileField(
upload_to=myfile_save_to,
storage=CleanFileNameStorage(),
)
Remember if you specify the storage on a model level you must initialize the class e.g. CleanFileNameStorage() if you don't you will get argument errors
I am trying to create a form where users will be allowed to upload any image file + SWF files. Django's ImageField does not support SWF so I need to override it.
What I want to do is to check if the file is SWF, if True, return it. If it's not SWF, call the original method which will take care of the file validation.
However, I am not sure how to implement that. Here is an example of what I am trying to achieve, but it does not work:
from hexagonit import swfheader
class SwfImageField(forms.ImageField):
def to_python(self, data):
try:
swfheader.parse(data)
return data
except:
return super(SwfImageField, self).to_python(data)
What is actually does is allowing only SWF files at the moment.
An alternative and possibly easiest solution is to use a standard FileField with a custom validator:
def my_validate(value):
ext = os.path.splitext(value.name)[1] # [0] returns path filename
valid = ['.jpg', '.swf']
if ext not in valid:
raise ValidationError("Unsupported file extension.")
class MyForm(forms.Form):
file = forms.FileField(validators=[my_validate])
may be it will be useful:
from django.core.validators import FileExtensionValidator
class MyModel(models.Model):
......
my_field = models.FileField('Name', validators=[FileExtensionValidator(['svg', 'jpg', 'png', '.....'])])
I have a model for which I am making an api in tastypie. I have a field which stores the path to a file which I maintain manually (I am not using FileField since users are not uploading the files). Here is a gist of a model:
class FooModel(models.Model):
path = models.CharField(max_length=255, null=True)
...
def getAbsPath(self):
"""
returns the absolute path to a file stored at location self.path
"""
...
Here is my tastypie config:
class FooModelResource(ModelResource):
file = fields.FileField()
class Meta:
queryset = FooModel.objects.all()
def dehydrate_file(self, bundle):
from django.core.files import File
path = bundle.obj.getAbsPath()
return File(open(path, 'rb'))
In the api in the file field this returns full path to a file. I want tastypie to be able to serve the actual file or at least an url to a file. How do I do that? Any code snippets are appreciated.
Thank you
Decide on a URL scheme how your files will be exposed through the APIs first. You don't really need file or dehydrate_file (unless you want to change the representation of the file for the model itself in Tastypie). Instead just add an additional action on the ModelResource. Example:
class FooModelResource(ModelResource):
file = fields.FileField()
class Meta:
queryset = FooModel.objects.all()
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/(?P<pk>\w[\w/-]*)/download%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('download_detail'), name="api_download_detail"),
]
def download_detail(self, request, **kwargs):
"""
Send a file through TastyPie without loading the whole file into
memory at once. The FileWrapper will turn the file object into an
iterator for chunks of 8KB.
No need to build a bundle here only to return a file, lets look into the DB directly
"""
filename = self._meta.queryset.get(pk=kwargs[pk]).file
wrapper = FileWrapper(file(filename))
response = HttpResponse(wrapper, content_type='text/plain') #or whatever type you want there
response['Content-Length'] = os.path.getsize(filename)
return response
GET .../api/foomodel/3/
Returns:
{
...
'file' : 'localpath/filename.ext',
...
}
GET .../api/foomodel/3/download/
Returns:
...contents of actual file...
Alternatively you could create a non-ORM Sub Resource file in FooModel. You would have to define resource_uri (how to uniquely identify each instance of the resource), and override dispatch_detail to do exactly what download_detail above does.
The only conversion tastypie does on a FileField is to look for an 'url' attribute on what you return, and return that if it exists, else it will return the string-ized object, which as you have noticed is just the filename.
If you want to return the file content as a field, you will need to handle the encoding of the file. You have a few options:
Simplest: Use CharField and use the base64 module to convert the bytes read from the file into a string
More general but functionally equivalent: write a custom tastypie Serializer that knows how to turn File objects into string representations of their contents
Override the get_detail function of your resource to serve just the file using whatever content-type is appropriate, to avoid the JSON/XML serialization overhead.