summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKe Wu <ke.wu@ibeca.me>2012-06-01 12:06:50 -0700
committerKe Wu <ke.wu@ibeca.me>2012-06-06 13:48:33 -0700
commit59e862e4226edadf01b84b6072c3cfecb4241415 (patch)
treee335ac2dc266d884b8d13a2b2814264ce59dc618
parentf6802a9058e74d7a3f4249c9ac5314705ae2a6f6 (diff)
downloadtuskar-ui-59e862e4226edadf01b84b6072c3cfecb4241415.tar.gz
Add Swift pseudo-folder support to Horizon.
Implements blueprint swift-folders. Change-Id: If29ad3cc1fcfb9b7bdb66d915a667f3363d38da0
-rw-r--r--.gitignore1
-rw-r--r--horizon/api/swift.py17
-rw-r--r--horizon/dashboards/nova/containers/forms.py78
-rw-r--r--horizon/dashboards/nova/containers/tables.py55
-rw-r--r--horizon/dashboards/nova/containers/tests.py10
-rw-r--r--horizon/dashboards/nova/containers/urls.py33
-rw-r--r--horizon/dashboards/nova/containers/views.py90
-rw-r--r--horizon/dashboards/nova/templates/nova/containers/create.html4
-rw-r--r--horizon/dashboards/nova/templates/nova/containers/index.html4
-rw-r--r--horizon/dashboards/nova/templates/nova/objects/_copy.html2
-rw-r--r--horizon/dashboards/nova/templates/nova/objects/index.html9
-rw-r--r--horizon/tests/api_tests/swift_tests.py4
12 files changed, 240 insertions, 67 deletions
diff --git a/.gitignore b/.gitignore
index 5453cb62..2d0c4f50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,4 @@ build
dist
AUTHORS
ChangeLog
+tags
diff --git a/horizon/api/swift.py b/horizon/api/swift.py
index 9af3aa9c..05683998 100644
--- a/horizon/api/swift.py
+++ b/horizon/api/swift.py
@@ -87,12 +87,15 @@ def swift_delete_container(request, name):
swift_api(request).delete_container(name)
-def swift_get_objects(request, container_name, prefix=None, marker=None):
+def swift_get_objects(request, container_name, prefix=None, path=None,
+ marker=None):
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
container = swift_api(request).get_container(container_name)
objects = container.get_objects(prefix=prefix,
marker=marker,
- limit=limit + 1)
+ limit=limit + 1,
+ delimiter="/",
+ path=path)
if(len(objects) > limit):
return (objects[0:-1], True)
else:
@@ -122,6 +125,16 @@ def swift_copy_object(request, orig_container_name, orig_object_name,
return orig_obj.copy_to(new_container_name, new_object_name)
+def swift_create_subfolder(request, container_name, folder_name):
+ container = swift_api(request).get_container(container_name)
+ obj = container.create_object(folder_name)
+ obj.headers = {'content-type': 'application/directory',
+ 'content-length': 0}
+ obj.send('')
+ obj.sync_metadata()
+ return obj
+
+
def swift_upload_object(request, container_name, object_name, object_file):
container = swift_api(request).get_container(container_name)
obj = container.create_object(object_name)
diff --git a/horizon/dashboards/nova/containers/forms.py b/horizon/dashboards/nova/containers/forms.py
index 1aeba8c1..e0bfc9b3 100644
--- a/horizon/dashboards/nova/containers/forms.py
+++ b/horizon/dashboards/nova/containers/forms.py
@@ -41,21 +41,47 @@ no_slash_validator = validators.RegexValidator(r'^(?u)[^/]+$',
class CreateContainer(forms.SelfHandlingForm):
- name = forms.CharField(max_length="255",
+ parent = forms.CharField(max_length=255,
+ required=False,
+ widget=forms.HiddenInput)
+ name = forms.CharField(max_length=255,
label=_("Container Name"),
validators=[no_slash_validator])
def handle(self, request, data):
try:
- api.swift_create_container(request, data['name'])
- messages.success(request, _("Container created successfully."))
+ if not data['parent']:
+ # Create a container
+ api.swift_create_container(request, data["name"])
+ messages.success(request, _("Container created successfully."))
+ else:
+ # Create a pseudo-folder
+ container, slash, remainder = data['parent'].partition("/")
+ remainder = remainder.rstrip("/")
+ subfolder_name = "/".join([bit for bit
+ in (remainder, data['name'])
+ if bit])
+ api.swift_create_subfolder(request,
+ container,
+ subfolder_name)
+ messages.success(request, _("Folder created successfully."))
+ url = "horizon:nova:containers:object_index"
+ if remainder:
+ remainder = remainder.rstrip("/")
+ remainder += "/"
+ return shortcuts.redirect(url, container, remainder)
+
except:
exceptions.handle(request, _('Unable to create container.'))
+
return shortcuts.redirect("horizon:nova:containers:index")
class UploadObject(forms.SelfHandlingForm):
- name = forms.CharField(max_length="255",
+ path = forms.CharField(max_length=255,
+ required=False,
+ widget=forms.HiddenInput)
+ name = forms.CharField(max_length=255,
label=_("Object Name"),
validators=[no_slash_validator])
object_file = forms.FileField(label=_("File"))
@@ -63,10 +89,14 @@ class UploadObject(forms.SelfHandlingForm):
def handle(self, request, data):
object_file = self.files['object_file']
+ if data['path']:
+ object_path = "/".join([data['path'].rstrip("/"), data['name']])
+ else:
+ object_path = data['name']
try:
obj = api.swift_upload_object(request,
data['container_name'],
- data['name'],
+ object_path,
object_file)
obj.metadata['orig-filename'] = object_file.name
obj.sync_metadata()
@@ -74,13 +104,14 @@ class UploadObject(forms.SelfHandlingForm):
except:
exceptions.handle(request, _("Unable to upload object."))
return shortcuts.redirect("horizon:nova:containers:object_index",
- data['container_name'])
+ data['container_name'], data['path'])
class CopyObject(forms.SelfHandlingForm):
new_container_name = forms.ChoiceField(label=_("Destination container"),
validators=[no_slash_validator])
- new_object_name = forms.CharField(max_length="255",
+ path = forms.CharField(max_length=255, required=False)
+ new_object_name = forms.CharField(max_length=255,
label=_("Destination object name"),
validators=[no_slash_validator])
orig_container_name = forms.CharField(widget=forms.HiddenInput())
@@ -97,15 +128,38 @@ class CopyObject(forms.SelfHandlingForm):
orig_object = data['orig_object_name']
new_container = data['new_container_name']
new_object = data['new_object_name']
+ new_path = "%s%s" % (data['path'], new_object)
+
+ # Iteratively make sure all the directory markers exist.
+ if data['path']:
+ path_component = ""
+ for bit in data['path'].split("/"):
+ path_component += bit
+ try:
+ api.swift.swift_create_subfolder(request,
+ new_container,
+ path_component)
+ except:
+ redirect = reverse(object_index, args=(orig_container,))
+ exceptions.handle(request,
+ _("Unable to copy object."),
+ redirect=redirect)
+ path_component += "/"
+
+ # Now copy the object itself.
try:
api.swift_copy_object(request,
orig_container,
orig_object,
new_container,
- new_object)
- vals = {"container": new_container, "obj": new_object}
- messages.success(request, _('Object "%(obj)s" copied to container '
- '"%(container)s".') % vals)
+ new_path)
+ dest = "%s/%s" % (new_container, data['path'])
+ vals = {"dest": dest.rstrip("/"),
+ "orig": orig_object.split("/")[-1],
+ "new": new_object}
+ messages.success(request,
+ _('Copied "%(orig)s" to "%(dest)s" as "%(new)s".')
+ % vals)
except exceptions.HorizonException, exc:
messages.error(request, exc)
return shortcuts.redirect(object_index, orig_container)
@@ -114,4 +168,4 @@ class CopyObject(forms.SelfHandlingForm):
exceptions.handle(request,
_("Unable to copy object."),
redirect=redirect)
- return shortcuts.redirect(object_index, new_container)
+ return shortcuts.redirect(object_index, new_container, data['path'])
diff --git a/horizon/dashboards/nova/containers/tables.py b/horizon/dashboards/nova/containers/tables.py
index c41b43db..5b03be89 100644
--- a/horizon/dashboards/nova/containers/tables.py
+++ b/horizon/dashboards/nova/containers/tables.py
@@ -52,7 +52,7 @@ class CreateContainer(tables.LinkAction):
class ListObjects(tables.LinkAction):
name = "list_objects"
- verbose_name = _("List Objects")
+ verbose_name = _("View Container")
url = "horizon:nova:containers:object_index"
classes = ("btn-list",)
@@ -71,7 +71,10 @@ class UploadObject(tables.LinkAction):
else:
# This is a table action and we already have the container name
container_name = self.table.kwargs['container_name']
- return reverse(self.url, args=(container_name,))
+ subfolders = self.table.kwargs.get('subfolder_path', '')
+ args = (http.urlquote(bit) for bit in
+ (container_name, subfolders) if bit)
+ return reverse(self.url, args=args)
def update(self, request, obj):
# This will only be called for the row, so we can remove the button
@@ -129,7 +132,6 @@ class DownloadObject(tables.LinkAction):
classes = ("btn-download",)
def get_link_url(self, obj):
- #assert False, obj.__dict__['_apiresource'].__dict__
return reverse(self.url, args=(http.urlquote(obj.container.name),
http.urlquote(obj.name)))
@@ -147,12 +149,18 @@ class ObjectFilterAction(tables.FilterAction):
return filter(comp, objects)
+def sanitize_name(name):
+ return name.split("/")[-1]
+
+
def get_size(obj):
return filesizeformat(obj.size)
class ObjectsTable(tables.DataTable):
- name = tables.Column("name", verbose_name=_("Object Name"))
+ name = tables.Column("name",
+ verbose_name=_("Object Name"),
+ filters=(sanitize_name,))
size = tables.Column(get_size, verbose_name=_('Size'))
def get_object_id(self, obj):
@@ -163,3 +171,42 @@ class ObjectsTable(tables.DataTable):
verbose_name = _("Objects")
table_actions = (ObjectFilterAction, UploadObject, DeleteObject)
row_actions = (DownloadObject, CopyObject, DeleteObject)
+
+
+def get_link_subfolder(subfolder):
+ return reverse("horizon:nova:containers:object_index",
+ args=(http.urlquote(subfolder.container.name),
+ http.urlquote(subfolder.name + "/")))
+
+
+class CreateSubfolder(CreateContainer):
+ verbose_name = _("Create Folder")
+ url = "horizon:nova:containers:create"
+
+ def get_link_url(self):
+ container = self.table.kwargs['container_name']
+ subfolders = self.table.kwargs['subfolder_path']
+ parent = "/".join((bit for bit in [container, subfolders] if bit))
+ parent = parent.rstrip("/")
+ return reverse(self.url, args=(http.urlquote(parent + "/"),))
+
+
+class DeleteSubfolder(DeleteObject):
+ data_type_singular = _("Folder")
+ data_type_plural = _("Folders")
+
+
+class ContainerSubfoldersTable(tables.DataTable):
+ name = tables.Column("name",
+ link=get_link_subfolder,
+ verbose_name=_("Subfolder Name"),
+ filters=(sanitize_name,))
+
+ def get_object_id(self, obj):
+ return obj.name
+
+ class Meta:
+ name = "subfolders"
+ verbose_name = _("Subfolders")
+ table_actions = (CreateSubfolder, DeleteSubfolder)
+ row_actions = (DeleteSubfolder,)
diff --git a/horizon/dashboards/nova/containers/tests.py b/horizon/dashboards/nova/containers/tests.py
index 5256fe62..e58ba48b 100644
--- a/horizon/dashboards/nova/containers/tests.py
+++ b/horizon/dashboards/nova/containers/tests.py
@@ -102,16 +102,18 @@ class ObjectViewTests(test.TestCase):
ret = (self.objects.list(), False)
api.swift_get_objects(IsA(http.HttpRequest),
self.containers.first().name,
- marker=None).AndReturn(ret)
+ marker=None,
+ path=None).AndReturn(ret)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:nova:containers:object_index',
args=[self.containers.first().name]))
self.assertTemplateUsed(res, 'nova/objects/index.html')
- expected = [obj.name for obj in self.objects.list()]
- self.assertQuerysetEqual(res.context['table'].data,
+ # UTF8 encoding here to ensure there aren't problems with Nose output.
+ expected = [obj.name.encode('utf8') for obj in self.objects.list()]
+ self.assertQuerysetEqual(res.context['objects_table'].data,
expected,
- lambda obj: obj.name)
+ lambda obj: obj.name.encode('utf8'))
def test_upload_index(self):
res = self.client.get(reverse('horizon:nova:containers:object_upload',
diff --git a/horizon/dashboards/nova/containers/urls.py b/horizon/dashboards/nova/containers/urls.py
index 9ba21594..c72f1fc1 100644
--- a/horizon/dashboards/nova/containers/urls.py
+++ b/horizon/dashboards/nova/containers/urls.py
@@ -23,16 +23,29 @@ from django.conf.urls.defaults import patterns, url
from .views import IndexView, CreateView, UploadView, ObjectIndexView, CopyView
-OBJECTS = r'^(?P<container_name>[^/]+)/%s$'
-
-
# Swift containers and objects.
urlpatterns = patterns('horizon.dashboards.nova.containers.views',
url(r'^$', IndexView.as_view(), name='index'),
- url(r'^create/$', CreateView.as_view(), name='create'),
- url(OBJECTS % r'$', ObjectIndexView.as_view(), name='object_index'),
- url(OBJECTS % r'upload$', UploadView.as_view(), name='object_upload'),
- url(OBJECTS % r'(?P<object_name>[^/]+)/copy$',
- CopyView.as_view(), name='object_copy'),
- url(OBJECTS % r'(?P<object_name>[^/]+)/download$',
- 'object_download', name='object_download'))
+
+ url(r'^(?P<container_name>(.+/)+)?create$',
+ CreateView.as_view(),
+ name='create'),
+
+ url(r'^(?P<container_name>[^/]+)/(?P<subfolder_path>(.+/)+)?$',
+ ObjectIndexView.as_view(),
+ name='object_index'),
+
+ url(r'^(?P<container_name>[^/]+)/(?P<subfolder_path>(.+/)+)?upload$',
+ UploadView.as_view(),
+ name='object_upload'),
+
+ url(r'^(?P<container_name>[^/]+)/'
+ r'(?P<subfolder_path>(.+/)+)?'
+ r'(?P<object_name>.+)/copy$',
+ CopyView.as_view(),
+ name='object_copy'),
+
+ url(r'^(?P<container_name>[^/]+)/(?P<object_path>.+)/download$',
+ 'object_download',
+ name='object_download')
+)
diff --git a/horizon/dashboards/nova/containers/views.py b/horizon/dashboards/nova/containers/views.py
index c8720319..eaac1a50 100644
--- a/horizon/dashboards/nova/containers/views.py
+++ b/horizon/dashboards/nova/containers/views.py
@@ -33,7 +33,8 @@ from horizon import exceptions
from horizon import forms
from horizon import tables
from .forms import CreateContainer, UploadObject, CopyObject
-from .tables import ContainersTable, ObjectsTable
+from .tables import ContainersTable, ObjectsTable,\
+ ContainerSubfoldersTable
LOG = logging.getLogger(__name__)
@@ -63,31 +64,68 @@ class CreateView(forms.ModalFormView):
form_class = CreateContainer
template_name = 'nova/containers/create.html'
+ def get_initial(self):
+ initial = super(CreateView, self).get_initial()
+ initial['parent'] = self.kwargs['container_name']
+ return initial
+
-class ObjectIndexView(tables.DataTableView):
- table_class = ObjectsTable
+class ObjectIndexView(tables.MultiTableView):
+ table_classes = (ObjectsTable, ContainerSubfoldersTable)
template_name = 'nova/objects/index.html'
def has_more_data(self, table):
return self._more
- def get_data(self):
- objects = []
- self._more = None
- marker = self.request.GET.get('marker', None)
- container_name = self.kwargs['container_name']
- try:
- objects, self._more = api.swift_get_objects(self.request,
- container_name,
- marker=marker)
- except:
- msg = _('Unable to retrieve object list.')
- exceptions.handle(self.request, msg)
- return objects
+ @property
+ def objects(self):
+ """ Returns a list of objects given the subfolder's path.
+
+ The path is from the kwargs of the request
+ """
+ if not hasattr(self, "_objects"):
+ objects = []
+ self._more = None
+ marker = self.request.GET.get('marker', None)
+ container_name = self.kwargs['container_name']
+ subfolders = self.kwargs['subfolder_path']
+ if subfolders:
+ prefix = subfolders.rstrip("/")
+ else:
+ prefix = None
+ try:
+ objects, self._more = api.swift_get_objects(self.request,
+ container_name,
+ marker=marker,
+ path=prefix)
+ except:
+ objects = []
+ msg = _('Unable to retrieve object list.')
+ exceptions.handle(self.request, msg)
+ self._objects = objects
+ return self._objects
+
+ def get_objects_data(self):
+ """ Returns the objects within the in the current folder.
+
+ These objects are those whose names don't contain '/' after
+ striped the path out
+ """
+ filtered_objects = [item for item in self.objects if
+ item.content_type != "application/directory"]
+ return filtered_objects
+
+ def get_subfolders_data(self):
+ """ Returns a list of subfolders given the current folder path.
+ """
+ filtered_objects = [item for item in self.objects if
+ item.content_type == "application/directory"]
+ return filtered_objects
def get_context_data(self, **kwargs):
context = super(ObjectIndexView, self).get_context_data(**kwargs)
context['container_name'] = self.kwargs["container_name"]
+ context['subfolder_path'] = self.kwargs["subfolder_path"]
return context
@@ -96,7 +134,8 @@ class UploadView(forms.ModalFormView):
template_name = 'nova/objects/upload.html'
def get_initial(self):
- return {"container_name": self.kwargs["container_name"]}
+ return {"container_name": self.kwargs["container_name"],
+ "path": self.kwargs['subfolder_path']}
def get_context_data(self, **kwargs):
context = super(UploadView, self).get_context_data(**kwargs)
@@ -104,25 +143,25 @@ class UploadView(forms.ModalFormView):
return context
-def object_download(request, container_name, object_name):
- obj = api.swift.swift_get_object(request, container_name, object_name)
+def object_download(request, container_name, object_path):
+ obj = api.swift.swift_get_object(request, container_name, object_path)
# Add the original file extension back on if it wasn't preserved in the
# name given to the object.
- filename = object_name
+ filename = object_path.rsplit("/")[-1]
if not os.path.splitext(obj.name)[1]:
name, ext = os.path.splitext(obj.metadata.get('orig-filename', ''))
- filename = "%s%s" % (object_name, ext)
+ filename = "%s%s" % (filename, ext)
try:
object_data = api.swift_get_object_data(request,
container_name,
- object_name)
+ object_path)
except:
redirect = reverse("horizon:nova:containers:index")
exceptions.handle(request,
_("Unable to retrieve object."),
redirect=redirect)
response = http.HttpResponse()
- safe_name = filename.encode('utf-8')
+ safe_name = filename.replace(",", "").encode('utf-8')
response['Content-Disposition'] = 'attachment; filename=%s' % safe_name
response['Content-Type'] = 'application/octet-stream'
for data in object_data:
@@ -147,9 +186,12 @@ class CopyView(forms.ModalFormView):
return kwargs
def get_initial(self):
+ path = self.kwargs["subfolder_path"]
+ orig = "%s%s" % (path or '', self.kwargs["object_name"])
return {"new_container_name": self.kwargs["container_name"],
"orig_container_name": self.kwargs["container_name"],
- "orig_object_name": self.kwargs["object_name"],
+ "orig_object_name": orig,
+ "path": path,
"new_object_name": "%s copy" % self.kwargs["object_name"]}
def get_context_data(self, **kwargs):
diff --git a/horizon/dashboards/nova/templates/nova/containers/create.html b/horizon/dashboards/nova/templates/nova/containers/create.html
index 1facb05d..d9e1b561 100644
--- a/horizon/dashboards/nova/templates/nova/containers/create.html
+++ b/horizon/dashboards/nova/templates/nova/containers/create.html
@@ -9,7 +9,3 @@
{% block dash_main %}
{% include "nova/containers/_create.html" %}
{% endblock %}
-
-
-
-
diff --git a/horizon/dashboards/nova/templates/nova/containers/index.html b/horizon/dashboards/nova/templates/nova/containers/index.html
index c3f027c4..54140a92 100644
--- a/horizon/dashboards/nova/templates/nova/containers/index.html
+++ b/horizon/dashboards/nova/templates/nova/containers/index.html
@@ -3,9 +3,7 @@
{% block title %}Containers{% endblock %}
{% block page_header %}
- {% url horizon:nova:images_and_snapshots:images:index as refresh_link %}
- {# to make searchable false, just remove it from the include statement #}
- {% include "horizon/common/_page_header.html" with title=_("Containers") refresh_link=refresh_link searchable="true" %}
+ {% include "horizon/common/_page_header.html" with title=_("Containers") %}
{% endblock page_header %}
{% block dash_main %}
diff --git a/horizon/dashboards/nova/templates/nova/objects/_copy.html b/horizon/dashboards/nova/templates/nova/objects/_copy.html
index 0a8e808d..870c8689 100644
--- a/horizon/dashboards/nova/templates/nova/objects/_copy.html
+++ b/horizon/dashboards/nova/templates/nova/objects/_copy.html
@@ -14,7 +14,7 @@
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
- <p>{% trans "You may make a new copy of an existing object to store in this or another container." %}</p>
+ <p>{% trans "Make a new copy of an existing object to store in this or another container. You may also specify a path at which the new copy should live inside of the selected container." %}</p>
</div>
{% endblock %}
diff --git a/horizon/dashboards/nova/templates/nova/objects/index.html b/horizon/dashboards/nova/templates/nova/objects/index.html
index 986a2545..cd44c7a8 100644
--- a/horizon/dashboards/nova/templates/nova/objects/index.html
+++ b/horizon/dashboards/nova/templates/nova/objects/index.html
@@ -4,10 +4,15 @@
{% block page_header %}
<div class='page-header'>
- <h2>Objects <small>Container: {{ container_name }}</small></h2>
+ <h2>{% trans "Container" %}: {{ container_name }}<small>/{{ subfolder_path|default:"" }}</small></h2>
</div>
{% endblock page_header %}
{% block dash_main %}
- {{ table.render }}
+ <div id="subfolders">
+ {{ subfolders_table.render }}
+ </div>
+ <div id="objects">
+ {{ objects_table.render }}
+ </div>
{% endblock %}
diff --git a/horizon/tests/api_tests/swift_tests.py b/horizon/tests/api_tests/swift_tests.py
index 89902041..afe8b69c 100644
--- a/horizon/tests/api_tests/swift_tests.py
+++ b/horizon/tests/api_tests/swift_tests.py
@@ -68,7 +68,9 @@ class SwiftApiTests(test.APITestCase):
self.mox.StubOutWithMock(container, 'get_objects')
container.get_objects(limit=1001,
marker=None,
- prefix=None).AndReturn(objects)
+ prefix=None,
+ delimiter='/',
+ path=None).AndReturn(objects)
self.mox.ReplayAll()
(objs, more) = api.swift_get_objects(self.request, container.name)