From 5112692fbdebc483cc11be024bc34a9c4022b97a Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Mon, 22 May 2023 20:21:40 +1200 Subject: [PATCH] Add FileSystemOverwriteStorage --- CHANGELOG.rst | 11 ++++++++++ docs/backends/filesystem.rst | 21 +++++++++++++++++++ storages/backends/filesystem.py | 27 ++++++++++++++++++++++++ tests/test_filesystem.py | 37 +++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 docs/backends/filesystem.rst create mode 100644 storages/backends/filesystem.py create mode 100644 tests/test_filesystem.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 35d9b6fe..f5fa7450 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,17 @@ django-storages CHANGELOG ========================= +UNRELEASED +********** + +Filesystem +---------- + +Add new ``FileSystemOverwriteStorage`` backend - similar to Django's existing filesystem storage, +but overwrites existing files instead of always saving with unique names. (`#1252`_) + +.. _#1252: https://github.com/jschneier/django-storages/pull/1252 + 1.14.2 (2023-10-08) ******************* diff --git a/docs/backends/filesystem.rst b/docs/backends/filesystem.rst new file mode 100644 index 00000000..76b4b625 --- /dev/null +++ b/docs/backends/filesystem.rst @@ -0,0 +1,21 @@ +Filesystem +========== + +django-storages contains an alternative filesystem storage backend. + +Unlike Django's builtin filesystem storage, this one always overwrites files of the same name, and never renames files. + + +Usage +***** + +:: + + from storages.backends.filesystem import FileSystemOverwriteStorage + + storage = FileSystemOverwriteStorage(location='/media/photos') + storage.save("myfile.txt", ContentFile("content 1")) + + # This will overwrite the previous file, *not* create a new file. + storage.save("myfile.txt", ContentFile("content 2")) + diff --git a/storages/backends/filesystem.py b/storages/backends/filesystem.py new file mode 100644 index 00000000..de4c67aa --- /dev/null +++ b/storages/backends/filesystem.py @@ -0,0 +1,27 @@ +import os +import pathlib + +from django.core.exceptions import SuspiciousFileOperation +from django.core.files.storage import FileSystemStorage + + +class FileSystemOverwriteStorage(FileSystemStorage): + """ + Filesystem storage that never renames files. + Files uploaded via this storage class will automatically overwrite any files of the same name. + """ + + # Don't throw errors if the file already exists when saving. + # https://manpages.debian.org/bullseye/manpages-dev/open.2.en.html#O_EXCL + OS_OPEN_FLAGS = FileSystemStorage.OS_OPEN_FLAGS & ~os.O_EXCL + + # Don't check what files already exist; just use the original name. + def get_available_name(self, name, max_length=None): + # Do validate it though (just like FileSystemStorage does) + name = str(name).replace("\\", "/") + dir_name, _ = os.path.split(name) + if ".." in pathlib.PurePath(dir_name).parts: + raise SuspiciousFileOperation( + "Detected path traversal attempt in '%s'" % dir_name + ) + return name diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py new file mode 100644 index 00000000..d021a35e --- /dev/null +++ b/tests/test_filesystem.py @@ -0,0 +1,37 @@ +import tempfile + +import pytest +from django.core.exceptions import SuspiciousFileOperation +from django.core.files.base import ContentFile + +from storages.backends import filesystem + + +@pytest.fixture() +def storage(): + with tempfile.TemporaryDirectory() as tmpdirname: + yield filesystem.FileSystemOverwriteStorage(location=tmpdirname) + + +def test_save_overwrite(storage): + content = ContentFile("content") + name = "testfile.txt" + storage.save(name, content) + + assert storage.exists(name) + assert storage.size(name) == len(content) + + content2 = ContentFile("content2") + storage.save(name, content2) + # No rename was done; the same file was overwritten + assert storage.exists(name) + assert storage.size(name) == len(content2) + + +def test_filename_validate(storage): + content = ContentFile("content") + with pytest.raises(SuspiciousFileOperation): + storage.save("/badfile.txt", content) + + with pytest.raises(SuspiciousFileOperation): + storage.save("foo/../../../badfile.txt", content)