Skip to content

Commit

Permalink
fix recursive delete and clean up docstrings.
Browse files Browse the repository at this point in the history
  • Loading branch information
Johnetordoff committed Jan 11, 2018
1 parent 78230fc commit e63f8bf
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 56 deletions.
29 changes: 9 additions & 20 deletions tests/providers/cloudfiles/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ async def test_upload_checksum_mismatch(self,
async def test_delete_folder(self, connected_provider, folder_root_empty, file_header_metadata):

path = WaterButlerPath('/delete/')
query = {'prefix': path.path}
query = {'prefix': path.path, 'delimiter': '/'}
url = connected_provider.build_url('', **query)
body = json.dumps(folder_root_empty).encode('utf-8')

Expand All @@ -215,15 +215,14 @@ async def test_delete_folder(self, connected_provider, folder_root_empty, file_h

await connected_provider.delete(path)

assert aiohttpretty.has_call(method='DELETE', uri=delete_url_content)
assert aiohttpretty.has_call(method='DELETE', uri=delete_url_folder)

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_delete_root(self, connected_provider, folder_root_empty, file_header_metadata):

path = WaterButlerPath('/')
query = {'prefix': path.path}
query = {'prefix': path.path, 'delimiter': '/'}
url = connected_provider.build_url('', **query)
body = json.dumps(folder_root_empty).encode('utf-8')

Expand All @@ -237,12 +236,14 @@ async def test_delete_root(self, connected_provider, folder_root_empty, file_hea

aiohttpretty.register_uri('DELETE', delete_url_content, status=200)

with pytest.raises(exceptions.DeleteError):
with pytest.raises(exceptions.DeleteError) as exc:
await connected_provider.delete(path)

assert exc.value.message == 'query argument confirm_delete=1 is required for' \
' deleting the entire root contents.'

await connected_provider.delete(path, confirm_delete=1)

assert aiohttpretty.has_call(method='DELETE', uri=delete_url_content)

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
Expand Down Expand Up @@ -377,7 +378,7 @@ async def test_metadata_404(self, connected_provider):
with pytest.raises(exceptions.MetadataError) as exc:
await connected_provider.metadata(path)

assert exc.value.message == "Could not retrieve folder '/level1/'"
assert exc.value.message == "'/level1/' could not be found."

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
Expand Down Expand Up @@ -474,7 +475,7 @@ async def test_metadata_file_does_not_exist(self, connected_provider):
with pytest.raises(exceptions.MetadataError) as exc:
await connected_provider.metadata(path)

assert exc.value.message == "Could not retrieve '/does_not.exist'"
assert exc.value.message == "'/does_not.exist' could not be found."

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
Expand All @@ -488,20 +489,8 @@ async def test_metadata_folder_does_not_exist(self, connected_provider):
with pytest.raises(exceptions.MetadataError) as exc:
await connected_provider.metadata(path)

assert exc.value.message == "Could not retrieve '/does_not_exist/'"

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_metadata_file_bad_content_type(self, connected_provider, file_metadata):
item = file_metadata
item['Content-Type'] = 'application/directory'
path = WaterButlerPath('/does_not.exist')
url = connected_provider.build_url(path.path)
aiohttpretty.register_uri('HEAD', url, headers=item)
with pytest.raises(exceptions.MetadataError) as exc:
await connected_provider.metadata(path)
assert exc.value.message == "'/does_not_exist/' could not be found."

assert exc.value.message == "Could not retrieve '/does_not.exist'"

class TestV1ValidatePath:

Expand Down
64 changes: 28 additions & 36 deletions waterbutler/providers/cloudfiles/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async def intra_copy(self, dest_provider, source_path, dest_path):
async def download(self,
path: WaterButlerPath,
accept_url: bool=False,
range: tuple=None,
request_range: tuple=None,
version: str=None,
revision: str=None,
displayName: str=None,
Expand All @@ -102,12 +102,12 @@ async def download(self,

version = revision or version
if version:
return await self._download_revision(range, version)
return await self._download_revision(request_range, version)

resp = await self.make_request(
'GET',
functools.partial(self.sign_url, path),
range=range,
range=request_range,
expects=(200, 206),
throws=exceptions.DownloadError,
)
Expand Down Expand Up @@ -157,36 +157,33 @@ async def upload(self,

@ensure_connection
async def delete(self, path: WaterButlerPath, confirm_delete: int=0) -> None:
"""Deletes the key at the specified path
:param str path: The path of the key to delete
:param int confirm_delete: Must be 1 to confirm root folder delete, this deletes entire
container object.
:rtype None:
"""
"""Deletes the key at the specified path."""

if path.is_root and not confirm_delete:
if path.is_root and confirm_delete != 1:
raise exceptions.DeleteError(
'query argument confirm_delete=1 is required for deleting the entire container.',
'query argument confirm_delete=1 is required for'
' deleting the entire root contents.',
code=400
)

if path.is_dir:
await self._delete_folder_contents(path)

if not path.is_root: # deleting the root "item" deletes the whole bucket.
await self._delete_folder(path)
else:
await self._delete_item(path)

@ensure_connection
async def _delete_folder_contents(self, path: WaterButlerPath) -> None:
async def _delete_folder(self, path: WaterButlerPath) -> None:
"""Folders must be emptied of all contents before they can be deleted"""

metadata = await self._metadata_folder(path, recursive=True)
metadata = await self._metadata_folder(path)

delete_files = [
os.path.join('/', self.container, path.child(item.name).path)
for item in metadata
]
delete_files = []
for item in metadata:
if item.kind == 'folder':
await self._delete_folder(path.from_metadata(item))
else:
delete_files.append(os.path.join('/', self.container, path.child(item.name).path))

delete_files.append(os.path.join('/', self.container, path.path))
query = {'bulk-delete': ''}
resp = await self.make_request(
'DELETE',
Expand All @@ -200,6 +197,9 @@ async def _delete_folder_contents(self, path: WaterButlerPath) -> None:
)
await resp.release()

if not path.is_root: # deleting root here would destory the container
await self._delete_item(path)

@ensure_connection
async def _delete_item(self, path: WaterButlerPath) -> None:

Expand All @@ -213,14 +213,13 @@ async def _delete_item(self, path: WaterButlerPath) -> None:

@ensure_connection
async def metadata(self, path: WaterButlerPath,
recursive: bool=False,
version: str=None,
revision: str=None,
**kwargs) -> Union[CloudFilesHeaderMetadata, List]:
"""Get Metadata about the requested file or the metadata of a folder's contents"""

if path.is_dir:
return await self._metadata_folder(path, recursive=recursive)
return await self._metadata_folder(path)
elif version or revision:
return await self._metadata_revision(path, version, revision)
else:
Expand All @@ -231,11 +230,7 @@ def build_url(self,
_endpoint: str=None,
container: str=None,
**query) -> str:
"""Build the url for the specified object
:param args segments: URI segments
:param kwargs query: Query parameters
:rtype str:
"""
"""Build the url for the specified object."""
endpoint = _endpoint or self.endpoint
container = container or self.container
return provider.build_url(endpoint, container, *path.split('/'), **query)
Expand Down Expand Up @@ -348,22 +343,19 @@ async def _metadata_item(self, path: WaterButlerPath) -> CloudFilesHeaderMetadat
)
await resp.release()

if resp.status == 404 or resp.headers['Content-Type'] == 'application/directory' and path.is_file:
if resp.status == 404:
raise exceptions.MetadataError(
'Could not retrieve \'{0}\''.format(str(path)),
'\'{}\' could not be found.'.format(str(path)),
code=404,
)

return CloudFilesHeaderMetadata(dict(resp.headers), path)

async def _metadata_folder(self, path: WaterButlerPath, recursive: bool=False) -> \
async def _metadata_folder(self, path: WaterButlerPath) -> \
List[Union[CloudFilesFolderMetadata, CloudFilesFileMetadata]]:
"""Get Metadata about the contents of requested folder"""
# prefix must be blank when searching the root of the container
query = {'prefix': path.path}
self.metrics.add('metadata.folder.is_recursive', True if recursive else False)
if not recursive:
query.update({'delimiter': '/'})
query = {'prefix': path.path, 'delimiter': '/'}
resp = await self.make_request(
'GET',
functools.partial(self.build_url, '', **query),
Expand All @@ -380,7 +372,7 @@ async def _metadata_folder(self, path: WaterButlerPath, recursive: bool=False) -
metadata = await self._metadata_item(dir_marker)
if not metadata:
raise exceptions.MetadataError(
'Could not retrieve folder \'{0}\''.format(str(path)),
'\'{0}\' could not be found.'.format(str(path)),
code=404,
)

Expand Down

0 comments on commit e63f8bf

Please sign in to comment.