I have a Django model with an ImageField() field. Now I'd like to rename the filename of the image (based on a unique CharField of the same model) before it gets saved to the filesystem. Additionally, if an image with the same filename already exists, the existing file should be renamed and the newly uploaded file should keep its filename.
I am not quite sure what's the correct or preferred way to do it. Should I override ModelAdmin.save_model(), do it in the Model.save() method or write an own custom file storage?
Can anyone give me some hits how I can accomplish this? Any tips or sample code are greatly appreciated.
You can combine two mechanisms here: passing an upload_to argument in the field's definition and a custom FileSystemStorage backend.
Here is a dummy model with an upload_to callable:
def upload_bar(instance, filename):
# Do something with the filename
return new_filename
class Foo(models.Model):
bar = models.ImageField(upload_to=upload_bar)
...
And a custom dummy FileSystemStorage backend:
from django.core.files.storage import FileSystemStorage
class OverwriteStorage(FileSystemStorage):
def _save(self, name, content):
if self.exists(name):
# Rename it
return super(OverwriteStorage, self)._save(name, content)
def get_available_name(self, name):
return name
However I would be very cautious in meddling with existing files (ie changing existing files' names). Note that Django does not remove the file from the filesystem even when the object is deleted.
Here's the solution to get unique filenames like 18f6ad9f-5cec-4420-abfd-278bed78ee4a.png
models.py
import os
import uuid
from django.conf import settings
from django.db import models
def make_upload_path(instance, filename):
file_root, file_ext = os.path.splitext(filename)
dir_name = '{module}/{model}'.format(module=instance._meta.app_label, model=instance._meta.module_name)
file_root = unicode(uuid.uuid4())
name = os.path.join(settings.MEDIA_ROOT, dir_name, file_root + file_ext.lower())
# Delete existing file to overwrite it later
if instance.pk:
while os.path.exists(name):
os.remove(name)
return os.path.join(dir_name, file_root + file_ext.lower())
class YourModel(models.Model):
title = models.CharField(max_length=100)
image = models.ImageField(blank=True, upload_to=make_upload_path)
you can access self.image.name from Model.save()
http://lightbird.net/dbe/photo.html
Related
I'm restricting the upload button to allow only csv files.
I need help please to append _hello at the end of each file uploaded by the user, but before the extension. (e.g. user_file_name.csv becomes automatically user_file_name_hello.csv)
Optional: I'd like the original file to be first renamed automatically, then saved to my uploads directory.
models.py
from django.db import models
# validation method to check if file is csv
from django.core.exceptions import ValidationError
def validate_file_extension(value):
if not value.name.endswith('.csv'):
raise ValidationError(u'Only CSV files allowed.')
# Create your models here.
class user_file(models.Model):
user_file_csv = models.FileField(upload_to='documents/user_files/', validators=[validate_file_extension])
forms.py
from django import forms
from .models import user_file
from django.forms import FileInput
class user_file_form(forms.ModelForm):
class Meta:
model = user_file
widgets = {'user_file_csv': FileInput(attrs={'accept': 'text/csv'})}
fields = ('user_file_csv',)
Thank you!
Maybe you need something like this:
class FileUploadUtil:
#staticmethod
def my_files_path(instance, filename):
name, file_extention = os.path.splitext(filename)
name = 'prefix-{}-{}-sufix.{}'.format(name, instance.id, file_extention)
return "my_files/{}".format(name)
class MyModel(models.Model):
# Other fields
# ...
my_file = models.FileField(max_length=300, upload_to=FileUploadUtil.my_files_path)
Optional: I'd like the original file to be first renamed automatically, then saved to my uploads directory.
You can override save() method. Check here
Django document
Maybe You need decorator.
from pathlib import Path
def rename_helper(path: str, append_text: str):
stem, suffix = Path(path).stem, Path(path).suffix
return f"{stem}{append_text}{suffix}"
def rename_previous_image(func):
""" return wrapper object """
def wrapper(*args, **kwargs):
self = args[0]
model = type(self)
previous_obj = model.objects.filter(pk=self.pk)
if previous_obj.exists():
old_name_with_path = Path(str(previous_obj[0].user_file_csv))
Path.rename(old_name_with_path , rename_helper(path=old_name_with_path , append_text="_hello"))
return func(*args, **kwargs)
return wrapper
And, You can decorate your model save() method.
class MyModel(models.Model):
# Other fields
# ...
my_file = models.FileField(max_length=300, upload_to=FileUploadUtil.my_files_path)
#rename_previous_image
def save(self, **kwargs):
super(user_file, self).save(**kwargs) # You must add This row.
besides,
recommend rename your user_file class
like UserFile
Check This PEP 8
Have a good day.
I find myself in an odd situation only when deployed (debug == false):
My model throws a path traversal attempt exception. I want to create a directory for every file uploaded and save the file within the directory (some.zip) used in example. In my dev environment I have no problems and everything works just fine.
models.py:
class Template(models.Model):
def get_folder(self, filename):
filename_PATH = Path(filename)
template_dir = filename_PATH.stem
return Path(settings.TEMPLATES_FOLDER).joinpath(template_dir, filename)
name = models.CharField("template", max_length=32, unique=True)
file = models.FileField("templatefile", upload_to=get_folder, null=True, max_length=260, storage=OverwriteStorage())
class OverwriteStorage(FileSystemStorage): #this is actually above
def get_available_name(self, name, max_length=None):
self.delete(name)
return name
forms.py:
class TemplateAdminForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
super().__init__(*args, **kwargs)
class Meta:
model = Template
fields = ["name", "file", ]
def clean(self):
cleaned_data = super().clean()
upFile = Path(str(cleaned_data["file"]))
if upFile.suffix == ".zip":
path = self.instance.get_folder(cleaned_data["name"])
logging.error(f"{path}")
unpack_zip(path) ## works! the directory is created/filled
else:
raise forms.ValidationError("unknown file type ...")
logging.error("DONE!") # I see this output
return cleaned_data
## signal to see when the error might be happening:
#receiver(post_save, sender = Template)
def testing(sender, **kwargs):
logging.error("we never get here")
settings.py:
TEMPLATES_FOLDER = PATH(MEDIA_ROOT).joinpath("TEMPLATES")
but:
ERROR:django.security.SuspiciousFileOperation:Detected path traversal attempt in '/opt/project/media_root/TEMPLATES/some/some' WARNING:django.request:Bad Request: /admin/appName/template/add/
Edit:
Because of this discussion it might be important, this is happening on django 3.2.8
I get the same error on Django 3.2.6 when opening a file with mode "wb" at an absolute path name, when I'm not using a temporary file which I have read is recommened in order to avoid this problem so I will link this answer in case it helps you deploy it and share my experience.
Here's where it's been advised: answer
One possible solution would be to move that directory under the django project root folder and address it with a relative path. I'd try to use this too in order to understand how you could achieve this:
import os
print("WORKING DIRECTORY: " + os.getcwd())
An article on this topic suggests to use the following code (when dealing with an image file in that case): link
from django.core.files.temp import NamedTemporaryFile
from django.core import files
image_temp_file = NamedTemporaryFile(delete=True)
in_memory_image = open('/path/to/file', 'rb')
# Write the in-memory file to the temporary file
# Read the streamed image in sections
for block in in_memory_image.read(1024 * 8):
# If no more file then stop
if not block:
break # Write image block to temporary file
image_temp_file.write(block)
file_name = 'temp.png' # Choose a unique name for the file
image_temp_file.flush()
temp_file = files.File(image_temp_file, name=file_name)
Lets go through the code:
Create a NamedTemporaryFile instead of TemporaryFile as Django’s ImageField requires file name.
Iterate over your in-memory file and write blocks of data to the NamedTemporaryFile object.
Flush the file to ensure the file is written to the storage.
Change the temporary file to a Django’s File object.
You can assign this file to Django models directly and save it.
>>> from blog.models import Blog
>>> b = Blog.objects.first()
>>> b.image = temp_file
>>> b.save()
I personally solved my SuspiciousFileOperation problems by addressing my directory with "BASE_DIR" from settings.py as the beginning of the path (nothing above that level in the filesystem), using a NamedTemporaryFile and by using the model FileField save() method appropriately like this:
# inside a model class save(self, *args, **kwargs) method
# file_name is the file name alone, no path to the file
self.myfilefield.save(file_name, temporary_file_object, save=False) # and then call the super().save(*args, **kwargs) inside the save() method of your model
I am using django-extensions.AutoSlugField and django.db.models.ImageField.
To customize image name uploaded for django.db.models.ImageField, what I did:
from django.utils.text import slugify
# idea is to make image name the same as the automatically generated slug, however don't work
def update_image_name(instance, filename):
# this debug instance, and instance.slug is empty string
print(instance.__dict__)
# I attempt to use slugify directly and see that it's not the same as the output generated by AutoSlugField
# E.g. If I create display_name "shawn" 2nd time, AutoSlugField will return "shawn-2", but slugify(display_name) return "shawn"
print(slugify(instance.display_name))
return f"images/{instance.slug}.jpg"
class Object(models.Model):
...
display_name = models.TextField()
...
# to customize uploaded image name
image = models.ImageField(blank=True, upload_to=update_image_name)
...
# create slug automatically from display_name
slug = AutoSlugField(blank=True, populate_from=["display_name"]
Based on what I debug, when I call instance inside update_image_name, slug is empty string.
If I understand correctly slug is only created at event save, so when I call ImageField instance, slug is not yet created, therefore empty string.
I think it might have something to do with event post save. However, I am not sure if that's the real reason or how to do that.
How can I get the automatically generated slug as my customized image name?
That's a tricky one because the order the fields are getting saved matters.
The brute-force attack I'm suggesting would be to override the save method of your model and manually call the create_slug method before everything else ensuring the slug is set:
from django.utils.encoding import force_str
[...]
class Object(models.Model):
[...]
def save(self, *args, **kwargs):
self.slug = force_str(self._meta.get_field('slug').create_slug(self, False))
super(Object, self).save(*args, **kwargs)
That's what AutoSlugField does, refer to the code here. self._meta.get_field('slug') get's the slug field definition and then we just call the create_slug method.
Tested under Python 3.7.9 & Django 3.1.5 like this:
from django.core.files.uploadedfile import SimpleUploadedFile
o = Object()
o.display_name = "foo bar"
o.image = SimpleUploadedFile(name='test_image.png', content=open('/path/to/test/image.png', 'rb').read(), content_type='image/png')
o.save()
Then I see update_image_name return images/foo-bar.jpg.
I'm trying to make dynamic upload path to FileField model. So when user uploads a file, Django stores it to my computer /media/(username)/(path_to_a_file)/(filename).
E.g. /media/Michael/Homeworks/Math/Week_1/questions.pdf or /media/Ernie/Fishing/Atlantic_ocean/Good_fishing_spots.txt
VIEWS
#login_required
def add_file(request, **kwargs):
if request.method == 'POST':
form = AddFile(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.parent = Directory.objects.get(directory_path=str(kwargs['directory_path']))
post.file_path = str(kwargs['directory_path'])
post.file_content = request.FILES['file_content'] <-- need to pass dynamic file_path here
post.save()
return redirect('/home/' + str(post.author))
MODELS
class File(models.Model):
parent = models.ForeignKey(Directory, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
file_name = models.CharField(max_length=100)
file_path = models.CharField(max_length=900)
file_content = models.FileField(upload_to='e.g. /username/PATH/PATH/..../')
FORMS
class AddFile(forms.ModelForm):
class Meta:
model = File
fields = ['file_name', 'file_content']
What I have found was this, but after trial and error I have not found the way to do it. So the "upload/..." would be post.file_path, which is dynamic.
def get_upload_to(instance, filename):
return 'upload/%d/%s' % (instance.profile, filename)
class Upload(models.Model):
file = models.FileField(upload_to=get_upload_to)
profile = models.ForeignKey(Profile, blank=True, null=True)
You can use some thing like this(i used it in my project):
import os
def get_upload_path(instance, filename):
return os.path.join(
"user_%d" % instance.owner.id, "car_%s" % instance.slug, filename)
Now:
photo = models.ImageField(upload_to=get_upload_path)
Since the file_path is an attribute on the File model, can you not build the full path something like this:
import os
def create_path(instance, filename):
return os.path.join(
instance.author.username,
instance.file_path,
filename
)
And then reference it from your File model:
class File(models.Model):
...
file_content = models.FileField(upload_to=create_path)
Link to docs
The other answers work flawlessly; however, I want to point out the line in the source code that allows such functionality. You can view the function, generate_filename, here, in Django's source code.
The lines that make the magic happen:
if callable(self.upload_to):
filename = self.upload_to(instance, filename)
When you pass a callable to the upload_to parameter, Django will call the callable to generate the path. Note that Django expects your callable to handle two arguments:
instance
the model that contains the FileField/ImageField
filename
the name of the uploaded file, including the extension (.png, .pdf, ...)
Also note that Python does not force your callable's arguments to be exactly 'instance' and 'filename' because Django passes them as positional parameters. For example, I prefer to rename them:
def get_file_path(obj, fname):
return os.path.join(
'products',
obj.slug,
fname,
)
And then use it like so:
image = models.ImageField(upload_to=get_file_path)
I need to rename the file with the name of the variable.
I Have this:
def content_file_name_Left(instance, filename):
return 'user_{0}/Left/{1}'.format(instance.ID, filename)
...
user_ImageLeft = models.FileField(default='', upload_to=content_file_name_Left)
I want that its save in: user_x/Left/user_ImageLeft.[format]
I have 20 images and I don't want make 20 functions for write manually the name of the variable.
Thanks
Just tested this and the best way seems to be by using a deconstructible class (deconstructible is used to prevent migration errors):
#deconstructible
class PathAndUniqueFilename(object):
def __init__(self, sub_path):
self.path = sub_path
def __call__(self, instance, filename):
self.path = self.path.format(instance.user.id)
return os.path.join(self.path, filename)
and then call this in your model like so:
user_ImageLeft = models.FileField(default='', upload_to=PathAndUniqueFilename('user_{0}/Left/'))
What this does is take the parameters of PathAndUniqueFilename('user_{0}/Left/'), and uses format() in the deconstructible in order to add a custom folder name.