Add Swift pseudo-folder support to Horizon.

Implements blueprint swift-folders.

Change-Id: If29ad3cc1fcfb9b7bdb66d915a667f3363d38da0
This commit is contained in:
Ke Wu 2012-06-01 12:06:50 -07:00
parent f6802a9058
commit 59e862e422
12 changed files with 240 additions and 67 deletions

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ build
dist
AUTHORS
ChangeLog
tags

View File

@ -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)

View File

@ -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'])

View File

@ -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,)

View File

@ -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',

View File

@ -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')
)

View File

@ -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):

View File

@ -9,7 +9,3 @@
{% block dash_main %}
{% include "nova/containers/_create.html" %}
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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)