|
| 1 | +import logging |
| 2 | +from pathlib import Path |
| 3 | + |
| 4 | +from django.core.files.storage import FileSystemStorage |
| 5 | +from storages.utils import safe_join, get_available_overwrite_name |
| 6 | + |
| 7 | + |
| 8 | +log = logging.getLogger(__name__) |
| 9 | + |
| 10 | + |
| 11 | +class BuildMediaStorageMixin: |
| 12 | + |
| 13 | + """ |
| 14 | + A mixin for Storage classes needed to write build artifacts |
| 15 | +
|
| 16 | + This adds and modifies some functionality to Django's File Storage API. |
| 17 | + By default, classes mixing this in will now overwrite files by default instead |
| 18 | + of finding an available name. |
| 19 | + This mixin also adds convenience methods to copy and delete entire directories. |
| 20 | +
|
| 21 | + See: https://docs.djangoproject.com/en/1.11/ref/files/storage |
| 22 | + """ |
| 23 | + |
| 24 | + @staticmethod |
| 25 | + def _dirpath(path): |
| 26 | + """It may just be Azure, but for listdir to work correctly, the path must end in '/'""" |
| 27 | + path = str(path) |
| 28 | + if not path.endswith('/'): |
| 29 | + path += '/' |
| 30 | + |
| 31 | + return path |
| 32 | + |
| 33 | + def get_available_name(self, name, max_length=None): |
| 34 | + """ |
| 35 | + Overrides Django's storage implementation to always return the passed name (overwrite) |
| 36 | +
|
| 37 | + By default, Django will not overwrite files even if the same name is specified. |
| 38 | + This changes that functionality so that the default is to use the same name and overwrite |
| 39 | + rather than modify the path to not clobber files. |
| 40 | + """ |
| 41 | + return get_available_overwrite_name(name, max_length=max_length) |
| 42 | + |
| 43 | + def delete_directory(self, path): |
| 44 | + """ |
| 45 | + Delete all files under a certain path from storage |
| 46 | +
|
| 47 | + Many storage backends (S3, Azure storage) don't care about "directories". |
| 48 | + The directory effectively doesn't exist if there are no files in it. |
| 49 | + However, in these backends, there is no "rmdir" operation so you have to recursively |
| 50 | + delete all files. |
| 51 | +
|
| 52 | + :param path: the path to the directory to remove |
| 53 | + """ |
| 54 | + log.debug('Deleting directory %s from media storage', path) |
| 55 | + folders, files = self.listdir(self._dirpath(path)) |
| 56 | + for folder_name in folders: |
| 57 | + if folder_name: |
| 58 | + # Recursively delete the subdirectory |
| 59 | + self.delete_directory(safe_join(path, folder_name)) |
| 60 | + for filename in files: |
| 61 | + if filename: |
| 62 | + self.delete(safe_join(path, filename)) |
| 63 | + |
| 64 | + def copy_directory(self, source, destination): |
| 65 | + """ |
| 66 | + Copy a directory recursively to storage |
| 67 | +
|
| 68 | + :param source: the source path on the local disk |
| 69 | + :param destination: the destination path in storage |
| 70 | + """ |
| 71 | + log.debug('Copying source directory %s to media storage at %s', source, destination) |
| 72 | + source = Path(source) |
| 73 | + for filepath in source.iterdir(): |
| 74 | + sub_destination = safe_join(destination, filepath.name) |
| 75 | + if filepath.is_dir(): |
| 76 | + # Recursively copy the subdirectory |
| 77 | + self.copy_directory(filepath, sub_destination) |
| 78 | + elif filepath.is_file(): |
| 79 | + with filepath.open('rb') as fd: |
| 80 | + self.save(sub_destination, fd) |
| 81 | + |
| 82 | + |
| 83 | +class BuildMediaFileSystemStorage(BuildMediaStorageMixin, FileSystemStorage): |
| 84 | + |
| 85 | + """Storage subclass that writes build artifacts under MEDIA_ROOT""" |
| 86 | + |
| 87 | + def get_available_name(self, name, max_length=None): |
| 88 | + """ |
| 89 | + A hack to overwrite by default with the FileSystemStorage |
| 90 | +
|
| 91 | + After upgrading to Django 2.2, this method can be removed |
| 92 | + because subclasses can set OS_OPEN_FLAGS such that FileSystemStorage._save |
| 93 | + will properly overwrite files. |
| 94 | + See: https://github.com/django/django/pull/8476 |
| 95 | + """ |
| 96 | + name = super().get_available_name(name, max_length=max_length) |
| 97 | + if self.exists(name): |
| 98 | + self.delete(name) |
| 99 | + return name |
| 100 | + |
| 101 | + def listdir(self, path): |
| 102 | + """Return empty lists for nonexistent directories (as cloud storages do)""" |
| 103 | + if not self.exists(path): |
| 104 | + return [], [] |
| 105 | + return super().listdir(path) |
0 commit comments