Repository: django-cms/django-filer

Issue:
Endless recursion when copying folder into itself
When copying a Folder Structure into itself or a child an endless recursion occurs:
E.g.

```
/a
/a/b
```

When copying `/a` into `/b` one would expect this result:

```
/a
/a/b
/a/b/a (copy of /a)
/a/b/a/b (copy of /a/b)
```

The copy action has a bug however and will continue creating copies of `/a/b/a` and `/a/b/a/b` until the recursion limit of the python interpreter is reached.

```
/a
/a/b
/a/b/a (copy of /a)
/a/b/a/b (copy of /a/b)
/a/b/a/b/a (copy of /a/b/a)
/a/b/a/b/a/b (copy of /a/b/a/b)
(...)
```

Possible solutions:
1. Disable copying into itself. I made a q'n'd hack for this in my own repository. It is not 100% correct, but ensures that my users don't end up with deeply nested folder structures and an internal server error: https://github.com/bikeshedder/django-filer/commit/9b093f68e6621bcdb7df0b1e08b3add521d987d1
2. Make sure copies are not copied again. I guess there are several ways to tackle this. I think the easiest way would be to get an entire listing first and copy those. Right now the order of operations is "copy, list, copy, list,..." which causes this behavior of listing files and folder which have just been copied.

To be true, I currently don't see why anyone would want to copy a folder structure into itself. It should be ok to limit the target directory to any folder not the source or a child of the source, but I might be missing an important use case?



Discussion:
IMO solution 1 is totally correct


Resolving patch:
diff --git a/filer/admin/folderadmin.py b/filer/admin/folderadmin.py
--- a/filer/admin/folderadmin.py
+++ b/filer/admin/folderadmin.py
@@ -18,10 +18,10 @@
 from django.shortcuts import render_to_response, get_object_or_404
 from django.template import RequestContext
 try:
-    from django.utils.encoding import force_str
+    from django.utils.encoding import force_text
 except ImportError:
     # Django < 1.5
-    from django.utils.encoding import force_unicode as force_str
+    from django.utils.encoding import force_unicode as force_text
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext as _
@@ -636,7 +636,7 @@ def delete_files_or_folders(self, request, files_queryset, folders_queryset):
             if n:
                 # delete all explicitly selected files
                 for f in files_queryset:
-                    self.log_deletion(request, f, force_str(f))
+                    self.log_deletion(request, f, force_text(f))
                     f.delete()
                 # delete all files in all selected folders and their children
                 # This would happen automatically by ways of the delete cascade, but then the individual .delete()
@@ -646,11 +646,11 @@ def delete_files_or_folders(self, request, files_queryset, folders_queryset):
                     folder_ids.add(folder.id)
                     folder_ids.update(folder.get_descendants().values_list('id', flat=True))
                 for f in File.objects.filter(folder__in=folder_ids):
-                    self.log_deletion(request, f, force_str(f))
+                    self.log_deletion(request, f, force_text(f))
                     f.delete()
                 # delete all folders
                 for f in folders_queryset:
-                    self.log_deletion(request, f, force_str(f))
+                    self.log_deletion(request, f, force_text(f))
                     f.delete()
                 self.message_user(request, _("Successfully deleted %(count)d files and/or folders.") % {
                     "count": n,
@@ -710,7 +710,7 @@ def _format_callback(self, obj, user, admin_site, perms_needed):
             # Don't display link to edit, because it either has no
             # admin or is edited inline.
             return '%s: %s' % (capfirst(opts.verbose_name),
-                                force_str(obj))
+                                force_text(obj))
 
     def _check_copy_perms(self, request, files_queryset, folders_queryset):
         try:
@@ -762,7 +762,7 @@ def _list_all_destination_folders_recursive(self, request, folders_queryset, cur
 
             # We do not allow copying/moving back to the folder itself
             enabled = (allow_self or fo != current_folder) and fo.has_add_children_permission(request)
-            yield (fo, (mark_safe(("&nbsp;&nbsp;" * level) + force_str(fo)), enabled))
+            yield (fo, (mark_safe(("&nbsp;&nbsp;" * level) + force_text(fo)), enabled))
             for c in self._list_all_destination_folders_recursive(request, folders_queryset, current_folder, fo.children.all(), allow_self, level + 1):
                 yield c
 
@@ -993,7 +993,7 @@ def copy_files_and_folders(self, request, files_queryset, folders_queryset):
         current_folder = self._get_current_action_folder(request, files_queryset, folders_queryset)
         perms_needed = self._check_copy_perms(request, files_queryset, folders_queryset)
         to_copy = self._list_all_to_copy_or_move(request, files_queryset, folders_queryset)
-        folders = self._list_all_destination_folders(request, folders_queryset, current_folder, True)
+        folders = self._list_all_destination_folders(request, folders_queryset, current_folder, False)
 
         if request.method == 'POST' and request.POST.get('post'):
             if perms_needed:
diff --git a/filer/admin/patched/admin_utils.py b/filer/admin/patched/admin_utils.py
--- a/filer/admin/patched/admin_utils.py
+++ b/filer/admin/patched/admin_utils.py
@@ -8,15 +8,17 @@
 
 At all locations where something has been changed, there are inline comments in the code.
 """
+from __future__ import unicode_literals
+
 from django.contrib.admin.util import NestedObjects, quote
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.text import capfirst
 try:
-    from django.utils.encoding import force_str
+    from django.utils.encoding import force_text
 except ImportError:
     # Django < 1.5
-    from django.utils.encoding import force_unicode as force_str
+    from django.utils.encoding import force_unicode as force_text
 from django.core.urlresolvers import reverse
 
 
@@ -58,7 +60,7 @@ def format_callback(obj):
             # Don't display link to edit, because it either has no
             # admin or is edited inline.
             return '%s: %s' % (capfirst(opts.verbose_name),
-                               force_str(obj))
+                               force_text(obj))
 
     to_delete = collector.nested(format_callback)
 
diff --git a/filer/utils/generate_filename.py b/filer/utils/generate_filename.py
--- a/filer/utils/generate_filename.py
+++ b/filer/utils/generate_filename.py
@@ -1,25 +1,24 @@
 from __future__ import unicode_literals
 
 try:
-    from django.utils.encoding import force_str
+    from django.utils.encoding import force_text
 except ImportError:
     # Django < 1.5
-    from django.utils.encoding import force_unicode as force_str
+    from django.utils.encoding import force_unicode as force_text
 from django.utils.timezone import now
 from filer.utils.files import get_valid_filename
-from django.utils.encoding import smart_str
 import os
 
 
 def by_date(instance, filename):
-    datepart = force_str(now().strftime(smart_str("%Y/%m/%d")))
+    datepart = force_text(now().strftime("%Y/%m/%d"))
     return os.path.join(datepart, get_valid_filename(filename))
 
 def randomized(instance, filename):
     import uuid
     uuid_str = str(uuid.uuid4())
-    random_path = "%s/%s/%s" % (uuid_str[0:2], uuid_str[2:4], uuid_str)
-    return os.path.join(random_path, get_valid_filename(filename))
+    return os.path.join(uuid_str[0:2], uuid_str[2:4], uuid_str,
+            get_valid_filename(filename))
 
 
 class prefixed_factory(object):


Test patch: