Question

I have a database used by two different systems and as a result I actually need the FileField value to have a leading slash, like so:

/dirs/filename.ext

In Django, however, FileField values can't have a leading slash since it breaks how they interact with MEDIA_ROOT.

So my suspicion is that I either have to create a custom storage class or somehow customize FileField so that the leading slash is stripped out when read and restored when saved.


In case anyone is wondering why I am doing this: I am mirroring the files on a separate non-Django server.

On the Django server, the files are relative to the media root. So supposing the media root was /path/to/myapp/media, a file with the path dirs/filename.ext would exist at /path/to/myapp/media/dirs/filename.ext.

Meanwhile, when mirrored on the other server, they are stored relative to the webroot. So the path is equivalent to the absolute URL of the file (e.g. the file dirs/filename.ext is stored in /path/to/example.com/dirs/filename.ext and accessed as http://example.com/dirs/filename.ext).

Both servers are using the same database.

I realize that one solution is prepending a slash everywhere the field is used on the other server, but that is across a number of different source files, whereas in Django thanks to the record model I should be able to make a change just in the models.py file and it will work across the entire Django site.


So far I have tried creating a custom version of FileField and it correctly prepends the / on lookup and saving but I cannot get it to remove the leading slash when used within the Django app.


Example

Imagine a record called Tool with a PDF file for its manual. On the Django server, it would be displayed in a template thusly:

<h1>{{ tool.name }}</h1>
<p>{{ tool.description }}</p>
<p><a href="{{ MEDIA_URL }}{{ tool.file.url }}">Link to Manual</a></p>

Meanwhile on the other server it's more like (this is CF code):

<h1>#GetTool.tool_name#</h1>
<p>#GetTool.tool_description#</p>
<p><a href="#GetTool.tool_file#">Link to Manual</a></p>

In the second server example, it needs to be an absolute URL.

So, to be clear:

  • the second server is not a Django project
  • it would be much more time-consuming to change the code on the second server rather than the first
  • therefore the value in the FileField needs to be an absolute URL in order to be compatible with Django, but needs to be saved with a leading slash in order to be compatible with the second server.
Was it helpful?

Solution 4

Finally figured out how to do this. The trick was also subclassing FieldFile in addition to FileField:

class LeadingSlashFieldFile(files.FieldFile):
    def __init__(self, instance, field, name):
        name = re.sub(r'^/', '', name)
        super(LeadingSlashFieldFile, self).__init__(instance, field, name)

class LeadingSlashFileField(models.FileField):
    attr_class = LeadingSlashFieldFile

    def get_prep_lookup(self, lookup_type, value):
        if hasattr(value, 'name'):
            value = value.name
        if value[0] <> '/':
            value = "/" + value
        return super(LeadingSlashFileField, self).get_prep_lookup(lookup_type, value)

    def get_prep_value(self, value):
        value = super(LeadingSlashFileField, self).get_prep_value(value)
        if value is None:
            return None
        value = unicode(value)
        if value[0] <> '/':
            value = "/" + value
        return value

This appears to work.

OTHER TIPS

You can try creating custom field. Something like:

class MyFileField(models.Field):
    attr_class = MyFieldFile

    def get_directory_name(self):
        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))

    def get_filename(self, filename):
        return os.path.normpath(self.storage.get_valid_name(os.path.basename(filename)))

    def generate_filename(self, instance, filename):
        return os.path.join(self.get_directory_name(), self.get_filename(filename))

    def get_prep_lookup(self, lookup_type, value):
        if hasattr(value, 'name'):
            value = value.name
        return super(FileField, self).get_prep_lookup(lookup_type, value)

    def get_prep_value(self, value):
        "Returns field's value prepared for saving into a database."
        # Need to convert File objects provided via a form to unicode for database insertion
        if value is None:
            return None
        return unicode(value)

This is default django-1.4 code. And you should modify it to add slash where needed

You should also subclass django.db.models.fields.files.FieldFile (you can see MyFieldFile in example) because when you access field value you actualy get this class. And this class have save method that modifies path on save. So remove slash in that class.

Methods of this two classws call methods of storage class so another option is to create alternative storage

from django.core.files.storage import FileSystemStorage

class MyStorage(FileSystemStorage):
    def path(self, name):
        try:
            path = safe_join(self.location, name)
        except ValueError:
            raise SuspiciousOperation("Attempted access to '%s' denied." % name)
        return os.path.normpath(path)

    def _save(self, name, content):
        full_path = self.path(name)
        # many lines of code
        return name  # this is used as filename in database

Configure default storage in django settings:

DEFAULT_FILE_STORAGE = 'yourapp.import.path.MyStorage'

If I understand you right then why can't you just do a method that appends or removes the slash for the field instead of doing a special field?

It's a bit hackish but I guess it works.

class MyModelWithAppendingSlash(models.Model):
    my_file = FileField(options_go_here)

    def my_file_appended(self):
        return path = '/' + self.my_file.filename

    def my_file_slash_removed(self):
        return my_file.filename[1:]

Then when you reference the model in your django app do myModelWithAppendingSlashInstance.my_file_appended() or .my_file_slash_removed()

I snipped a lot of the code and you have to modify it but you get the gist of what I'm trying to achieve here. This might be totally wrong for you but this is how I interpret your question.

Without a line of code, you could fix this using a VIEW in the database. Just create a view that concatenates '/' (or 'http://example.com/' if you want the url to be really absolute) and the path.

CREATE VIEW CF_appname_tool AS
SELECT [other attributes], '/' + file as file
FROM appname_tool;

I needed to know that you use SQL server, because the string concatenation operator differs per DBMS.

I suspect changing the CF code to select from CF_appname_tool is too much hassle. You can move your data from appname_tool to some_other_name and set the db_table option on your Django Tool model:

class Tool(model.Model):
    class Meta:
        db_table = 'some_other_name'

and create the appname_tool VIEW as a selection from some_other_name:

CREATE VIEW appname_tool AS
SELECT [other attributes], '/' + file as file
FROM some_other_name;

You could listen to the post_syncdb signal if you want to create the VIEW programmatically in production.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top