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:
parent
4bd2204809
commit
b36a285938
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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. """
|
||||||
|
Loading…
Reference in New Issue
Block a user