Remove placeholder objects for subfolders

A subfolder in a container is now represented solely by the prefix of
an object name delimited by a forward-slash (/). If an object exists
in a container that matches the implied subfolder of another object,
each will be displayed in the objects table.

Implements bp swift-folder-prefix

Change-Id: I05252c1db34fdf6584a71e8827ff6f8363bf0488
This commit is contained in:
Brian Waldon 2013-01-29 17:58:21 -08:00 committed by Gabriel Hurley
parent 4bd2204809
commit b36a285938
6 changed files with 29 additions and 98 deletions

View File

@ -23,7 +23,6 @@ import logging
import swiftclient import swiftclient
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _
from horizon import exceptions from horizon import exceptions
@ -45,56 +44,47 @@ class StorageObject(APIDictWrapper):
self.orig_name = orig_name self.orig_name = orig_name
self.data = data self.data = data
@property
def id(self):
return self.name
class PseudoFolder(APIDictWrapper): class PseudoFolder(APIDictWrapper):
"""
Wrapper to smooth out discrepencies between swift "subdir" items
and swift pseudo-folder objects.
"""
def __init__(self, apidict, container_name): def __init__(self, apidict, container_name):
super(PseudoFolder, self).__init__(apidict) super(PseudoFolder, self).__init__(apidict)
self.container_name = container_name self.container_name = container_name
def _has_content_type(self): @property
content_type = self._apidict.get("content_type", None) def id(self):
return content_type == "application/directory" return '%s/%s' % (self.container_name, self.name)
@property @property
def name(self): def name(self):
if self._has_content_type():
return self._apidict['name']
return self.subdir.rstrip(FOLDER_DELIMITER) return self.subdir.rstrip(FOLDER_DELIMITER)
@property @property
def bytes(self): def bytes(self):
if self._has_content_type():
return self._apidict['bytes']
return None return None
@property @property
def content_type(self): def content_type(self):
return "application/directory" return "application/pseudo-folder"
def _objectify(items, container_name): def _objectify(items, container_name):
""" Splits a listing of objects into their appropriate wrapper classes. """ """ Splits a listing of objects into their appropriate wrapper classes. """
objects = {} objects = []
subdir_markers = []
# Deal with objects and object pseudo-folders first, save subdirs for later # Deal with objects and object pseudo-folders first, save subdirs for later
for item in items: for item in items:
if item.get("content_type", None) == "application/directory": if item.get("subdir", None) is not None:
objects[item['name']] = PseudoFolder(item, container_name) object_cls = PseudoFolder
elif item.get("subdir", None) is not None:
subdir_markers.append(PseudoFolder(item, container_name))
else: else:
objects[item['name']] = StorageObject(item, container_name) object_cls = StorageObject
# Revisit subdirs to see if we have any non-duplicates
for item in subdir_markers: objects.append(object_cls(item, container_name))
if item.name not in objects.keys():
objects[item.name] = item return objects
return objects.values()
def swift_api(request): def swift_api(request):
@ -215,17 +205,6 @@ def swift_copy_object(request, orig_container_name, orig_object_name,
headers=headers) headers=headers)
def swift_create_subfolder(request, container_name, folder_name):
headers = {'content-type': 'application/directory',
'content-length': 0}
etag = swift_api(request).put_object(container_name,
folder_name,
None,
headers=headers)
obj_info = {'subdir': folder_name, 'etag': etag}
return PseudoFolder(obj_info, container_name)
def swift_upload_object(request, container_name, object_name, object_file): def swift_upload_object(request, container_name, object_name, object_file):
headers = {} headers = {}
headers['X-Object-Meta-Orig-Filename'] = object_file.name headers['X-Object-Meta-Orig-Filename'] = object_file.name

View File

@ -77,7 +77,9 @@ class UploadObject(forms.SelfHandlingForm):
widget=forms.HiddenInput) widget=forms.HiddenInput)
name = forms.CharField(max_length=255, name = forms.CharField(max_length=255,
label=_("Object Name"), label=_("Object Name"),
validators=[no_slash_validator]) help_text=_("Slashes are allowed, and are treated "
"as pseudo-folders by the Object "
"Store."))
object_file = forms.FileField(label=_("File"), allow_empty_file=True) object_file = forms.FileField(label=_("File"), allow_empty_file=True)
container_name = forms.CharField(widget=forms.HiddenInput()) container_name = forms.CharField(widget=forms.HiddenInput())
@ -124,23 +126,6 @@ class CopyObject(forms.SelfHandlingForm):
path = path + "/" path = path + "/"
new_path = "%s%s" % (path, new_object) new_path = "%s%s" % (path, new_object)
# Iteratively make sure all the directory markers exist.
if path:
path_component = ""
for bit in [i for i in path.split("/") if i]:
path_component += bit
try:
api.swift.swift_create_subfolder(request,
new_container,
path_component)
except:
redirect = reverse(index,
args=(wrap_delimiter(orig_container),))
exceptions.handle(request,
_("Unable to copy object."),
redirect=redirect)
path_component += "/"
# Now copy the object itself. # Now copy the object itself.
try: try:
api.swift.swift_copy_object(request, api.swift.swift_copy_object(request,

View File

@ -144,18 +144,11 @@ class DeleteObject(tables.DeleteAction):
api.swift.swift_delete_object(request, container_name, obj_id) api.swift.swift_delete_object(request, container_name, obj_id)
class DeleteSubfolder(DeleteObject):
name = "delete_subfolder"
data_type_singular = _("Folder")
data_type_plural = _("Folders")
allowed_data_types = ("subfolders",)
class DeleteMultipleObjects(DeleteObject): class DeleteMultipleObjects(DeleteObject):
name = "delete_multiple_objects" name = "delete_multiple_objects"
data_type_singular = _("Object") data_type_singular = _("Object")
data_type_plural = _("Objects") data_type_plural = _("Objects")
allowed_data_types = ("subfolders", "objects",) allowed_data_types = ("objects",)
class CopyObject(tables.LinkAction): class CopyObject(tables.LinkAction):
@ -199,12 +192,12 @@ class ObjectFilterAction(tables.FilterAction):
def filter_subfolders_data(self, table, objects, filter_string): def filter_subfolders_data(self, table, objects, filter_string):
data = self._filtered_data(table, filter_string) data = self._filtered_data(table, filter_string)
return [datum for datum in data if return [datum for datum in data if
datum.content_type == "application/directory"] datum.content_type == "application/pseudo-folder"]
def filter_objects_data(self, table, objects, filter_string): def filter_objects_data(self, table, objects, filter_string):
data = self._filtered_data(table, filter_string) data = self._filtered_data(table, filter_string)
return [datum for datum in data if return [datum for datum in data if
datum.content_type != "application/directory"] datum.content_type != "application/pseudo-folder"]
def allowed(self, request, datum=None): def allowed(self, request, datum=None):
if self.table.kwargs.get('container_name', None): if self.table.kwargs.get('container_name', None):
@ -228,24 +221,6 @@ def get_link_subfolder(subfolder):
http.urlquote(wrap_delimiter(subfolder.name)))) http.urlquote(wrap_delimiter(subfolder.name))))
class CreateSubfolder(CreateContainer):
verbose_name = _("Create Folder")
url = "horizon:project:containers:create"
def get_link_url(self):
container = self.table.kwargs['container_name']
subfolders = self.table.kwargs['subfolder_path']
parent = FOLDER_DELIMITER.join((bit for bit in [container,
subfolders] if bit))
parent = parent.rstrip(FOLDER_DELIMITER)
return reverse(self.url, args=[http.urlquote(wrap_delimiter(parent))])
def allowed(self, request, datum=None):
if self.table.kwargs.get('container_name', None):
return True
return False
class ObjectsTable(tables.DataTable): class ObjectsTable(tables.DataTable):
name = tables.Column("name", name = tables.Column("name",
link=get_link_subfolder, link=get_link_subfolder,
@ -255,16 +230,12 @@ class ObjectsTable(tables.DataTable):
size = tables.Column(get_size, verbose_name=_('Size')) size = tables.Column(get_size, verbose_name=_('Size'))
def get_object_id(self, obj):
return obj.name
class Meta: class Meta:
name = "objects" name = "objects"
verbose_name = _("Objects") verbose_name = _("Objects")
table_actions = (ObjectFilterAction, CreateSubfolder, table_actions = (ObjectFilterAction, UploadObject,
UploadObject, DeleteMultipleObjects) DeleteMultipleObjects)
row_actions = (DownloadObject, CopyObject, DeleteObject, row_actions = (DownloadObject, CopyObject, DeleteObject)
DeleteSubfolder)
data_types = ("subfolders", "objects") data_types = ("subfolders", "objects")
browser_table = "content" browser_table = "content"
footer = False footer = False

View File

@ -15,7 +15,8 @@
</div> </div>
<div class="right"> <div class="right">
<h3>{% trans "Description" %}:</h3> <h3>{% trans "Description" %}:</h3>
<p>{% trans "An object is the basic storage entity and any optional metadata that represents the files you store in the OpenStack Object Storage system. When you upload data to OpenStack Object Storage, the data is stored as-is (no compression or encryption) and consists of a location (container), the object's name, and any metadata consisting of key/value pairs." %}</p> <p><strong>{% trans "Object" %}</strong>: {% trans "An object is the basic storage entity that represents a file you store in the OpenStack Object Storage system. When you upload data to OpenStack Object Storage, the data is stored as-is (no compression or encryption) and consists of a location (container), the object's name, and any metadata consisting of key/value pairs." %}</p>
<p><strong>{% trans "Pseudo-folder" %}</strong>: {% trans "Within a container you can group your objects into pseudo-folders, which behave similarly to folders in your desktop operating system, with the exception that they are virtual collections defined by a common prefix on the object's name. A slash (/) character is used as the delimiter for pseudo-folders in the Object Store." %}</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -157,12 +157,6 @@ class SwiftTests(test.TestCase):
args=[wrap_delimiter(container.name)]) args=[wrap_delimiter(container.name)])
self.assertRedirectsNoFollow(res, index_url) self.assertRedirectsNoFollow(res, index_url)
# Test invalid filename
formData['name'] = "contains/a/slash"
res = self.client.post(upload_url, formData)
self.assertNoMessages()
self.assertContains(res, "Slash is not an allowed character.")
@test.create_stubs({api.swift: ('swift_delete_object',)}) @test.create_stubs({api.swift: ('swift_delete_object',)})
def test_delete(self): def test_delete(self):
container = self.containers.first() container = self.containers.first()

View File

@ -86,7 +86,8 @@ class ContainerView(browsers.ResourceBrowserView):
return self._objects return self._objects
def is_subdir(self, item): def is_subdir(self, item):
return getattr(item, "content_type", None) == "application/directory" content_type = "application/pseudo-folder"
return getattr(item, "content_type", None) == content_type
def get_objects_data(self): def get_objects_data(self):
""" Returns a list of objects within the current folder. """ """ Returns a list of objects within the current folder. """