diff --git a/.gitignore b/.gitignore index 4d8f39b..994aaf8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ *.pyc *.eggs/ *.egg-info +.idea mezzanine-git/ diff --git a/filebrowser_safe/base.py b/filebrowser_safe/base.py index 6955f91..d24936a 100644 --- a/filebrowser_safe/base.py +++ b/filebrowser_safe/base.py @@ -47,7 +47,7 @@ def __len__(self): @cached_property def filetype(self): - if self.is_folder: + if not default_storage.type_checks_slow and self.is_folder: return 'Folder' return get_file_type(self.filename) diff --git a/filebrowser_safe/locale/cs/LC_MESSAGES/django.po b/filebrowser_safe/locale/cs/LC_MESSAGES/django.po index ca8b3fc..fb66196 100644 --- a/filebrowser_safe/locale/cs/LC_MESSAGES/django.po +++ b/filebrowser_safe/locale/cs/LC_MESSAGES/django.po @@ -322,7 +322,7 @@ msgstr[1] "%(counter) výsledky" #: templates/filebrowser/include/toolbar.html:9 #, python-format msgid "%(full_result_count)s total" -msgstr "%(full_result_count) celkem" +msgstr "%(full_result_count)s celkem" #: templates/filebrowser/include/search.html:5 msgid "Clear Restrictions" diff --git a/filebrowser_safe/storage.py b/filebrowser_safe/storage.py index 8a312c2..87873b2 100644 --- a/filebrowser_safe/storage.py +++ b/filebrowser_safe/storage.py @@ -2,8 +2,10 @@ # coding: utf-8 # PYTHON IMPORTS +import itertools import os import shutil +import posixpath # DJANGO IMPORTS from django.core.files.move import file_move_safe @@ -14,6 +16,7 @@ class StorageMixin(object): """ Adds some useful methods to the Storage class. """ + type_checks_slow = False def isdir(self, name): """ @@ -112,15 +115,32 @@ def makedirs(self, name): def rmtree(self, name): name = self._normalize_name(self._clean_name(name)) - dirlist = self.listdir(self._encode_name(name)) - for item in dirlist: - item.delete() + directories, files = self.listdir(self._encode_name(name)) + + for key in files: + self.delete('/'.join([name, key])) + + for dirname in directories: + self.rmtree('/'.join([name, dirname])) class GoogleStorageMixin(StorageMixin): + type_checks_slow = True # indicate that isfile/isdir should be avoided, + # for performance reasons, as appropriate + def isfile(self, name): - return self.exists(name) + # Because GCS does (semi-arbitrarily) create empty blobs for + # "folders," it's not enough to check whether the path exists; + # and, there's not (yet) any good way to differentiate these from + # proper files. + # + # It is POSSIBLE that an actual file name endswith / ... + # HOWEVER, it is unlikely, (and kind of evil). + # + # (Until then), just exclude paths out-of-hand if they're empty + # (i.e. the bucket root) OR if they end in /: + return bool(name) and not name.endswith('/') and self.exists(name) def isdir(self, name): # That's some inefficient implementation... @@ -132,16 +152,25 @@ def isdir(self, name): if self.isfile(name): return False - name = self._normalize_name(self._clean_name(name)) - dirlist = self.bucket.list(self._encode_name(name)) + # rather than ``listdir()``, which retrieves all results, retrieve + # blob iterator directly, and return as soon as ANY retrieved + name = self._normalize_name(clean_name(name)) + # For bucket.list_blobs and logic below name needs to end in / + if not name.endswith('/'): + name += '/' - # Check whether the iterator is empty - for item in dirlist: + iterator = self.bucket.list_blobs(prefix=self._encode_name(name), delimiter='/') + dirlist = itertools.chain(iterator, iterator.prefixes) + + # Check for contents + try: + next(dirlist) + except StopIteration: + return False + else: return True - return False def move(self, old_file_name, new_file_name, allow_overwrite=False): - if self.exists(new_file_name): if allow_overwrite: self.delete(new_file_name) @@ -163,6 +192,32 @@ def makedirs(self, name): def rmtree(self, name): name = self._normalize_name(self._clean_name(name)) - dirlist = self.bucket.list(self._encode_name(name)) + dirlist = self.listdir(self._encode_name(name)) for item in dirlist: item.delete() + + def _clean_name(self, name): + """ + Cleans the name so that Windows style paths work + """ + return clean_name(name) + + +def clean_name(name): + """ + Cleans the name so that Windows style paths work + """ + # Normalize Windows style paths + clean_name = posixpath.normpath(name).replace('\\', '/') + + # os.path.normpath() can strip trailing slashes so we implement + # a workaround here. + if name.endswith('/') and not clean_name.endswith('/'): + # Add a trailing slash as it was stripped. + clean_name = clean_name + '/' + + # Given an empty string, os.path.normpath() will return ., which we don't want + if clean_name == '.': + clean_name = '' + + return clean_name diff --git a/filebrowser_safe/views.py b/filebrowser_safe/views.py index 69bf1d1..b59b231 100644 --- a/filebrowser_safe/views.py +++ b/filebrowser_safe/views.py @@ -98,7 +98,9 @@ def browse(request): raise ImproperlyConfigured(_("Error finding Upload-Folder. Maybe it does not exist?")) redirect_url = reverse("fb_browse") + query_helper(query, "", "dir") return HttpResponseRedirect(redirect_url) - abs_path = os.path.join(get_directory(), path) + + base_dir = get_directory() + abs_path = os.path.join(base_dir, path) # INITIAL VARIABLES results_var = {'results_total': 0, 'results_current': 0, 'delete_total': 0, 'images_total': 0, 'select_total': 0} @@ -121,7 +123,7 @@ def browse(request): # CREATE FILEOBJECT url_path = "/".join([s.strip("/") for s in - [get_directory(), path.replace("\\", "/"), file] if s.strip("/")]) + [base_dir, path.replace("\\", "/"), file] if s.strip("/")]) fileobject = FileObject(url_path)