Skip to content

Commit

Permalink
Merge pull request #117 from OnroerendErfgoed/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
claeyswo authored Mar 6, 2024
2 parents 0608338 + a24ea3a commit 169718d
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 39 deletions.
5 changes: 5 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
0.8.0 (06-03-2024)
------------------

- Add replace file in archive and fetch file from archive (#115)

0.7.0 (22-12-2020)
------------------

Expand Down
100 changes: 69 additions & 31 deletions augeias/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,85 @@
def includeme(config):
"""this function adds some configuration for the application"""
# Rewrite urls with trailing slash
config.include('pyramid_rewrite')
config.add_rewrite_rule(r'/(?P<path>.*)/', r'/%(path)s')
config.include("pyramid_rewrite")
config.add_rewrite_rule(r"/(?P<path>.*)/", r"/%(path)s")

config.registry.collections = {}
config.add_route('home', '/')
config.add_route('list_collections', pattern='/collections', request_method="GET")
config.add_route('create_container_and_id', pattern='/collections/{collection_key}/containers',
request_method="POST")
config.add_route('create_container', pattern='/collections/{collection_key}/containers/{container_key}',
request_method="PUT")
config.add_route('delete_container', pattern='/collections/{collection_key}/containers/{container_key}',
request_method="DELETE")
config.add_route('list_object_keys_for_container',
pattern='/collections/{collection_key}/containers/{container_key}', request_method="GET",
accept='application/json')
config.add_route('get_container_data',
pattern='/collections/{collection_key}/containers/{container_key}',
request_method="GET", accept='application/zip')
config.add_route('create_object_and_id', pattern='/collections/{collection_key}/containers/{container_key}',
request_method="POST")
config.add_route('update_object', pattern='/collections/{collection_key}/containers/{container_key}/{object_key}',
request_method="PUT")
config.add_route('delete_object', pattern='/collections/{collection_key}/containers/{container_key}/{object_key}',
request_method="DELETE")
config.add_route('get_object', pattern='/collections/{collection_key}/containers/{container_key}/{object_key}',
request_method="GET")
config.add_route('get_object_info',
pattern='/collections/{collection_key}/containers/{container_key}/{object_key}/meta',
request_method="GET")
config.add_route("home", "/")
config.add_route("list_collections", pattern="/collections", request_method="GET")
config.add_route(
"create_container_and_id",
pattern="/collections/{collection_key}/containers",
request_method="POST",
)
config.add_route(
"create_container",
pattern="/collections/{collection_key}/containers/{container_key}",
request_method="PUT",
)
config.add_route(
"delete_container",
pattern="/collections/{collection_key}/containers/{container_key}",
request_method="DELETE",
)
config.add_route(
"list_object_keys_for_container",
pattern="/collections/{collection_key}/containers/{container_key}",
request_method="GET",
accept="application/json",
)
config.add_route(
"get_container_data",
pattern="/collections/{collection_key}/containers/{container_key}",
request_method="GET",
accept="application/zip",
)
config.add_route(
"create_object_and_id",
pattern="/collections/{collection_key}/containers/{container_key}",
request_method="POST",
)
config.add_route(
"update_object",
pattern="/collections/{collection_key}/containers/{container_key}/{object_key}",
request_method="PUT",
)
config.add_route(
"update_file_in_zip",
pattern="/collections/{collection_key}/containers/{container_key}/{object_key}/{file_name}",
request_method="PUT",
)
config.add_route(
"delete_object",
pattern="/collections/{collection_key}/containers/{container_key}/{object_key}",
request_method="DELETE",
)
config.add_route(
"get_object",
pattern="/collections/{collection_key}/containers/{container_key}/{object_key}",
request_method="GET",
)
config.add_route(
"get_object_info",
pattern="/collections/{collection_key}/containers/{container_key}/{object_key}/meta",
request_method="GET",
)
config.add_route(
"get_file_from_zip",
pattern="/collections/{collection_key}/containers/{container_key}/{object_key}/{file_name}",
request_method="GET",
)

config.scan()


def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
"""This function returns a Pyramid WSGI application."""
config = Configurator(settings=settings)

includeme(config)

if asbool(settings.get('augeias.init_collections', False)): # pragma: no cover
config.include('augeias.collections')
if asbool(settings.get("augeias.init_collections", False)): # pragma: no cover
config.include("augeias.collections")

return config.make_wsgi_app()
149 changes: 142 additions & 7 deletions augeias/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import io
import tarfile
import uuid
import zipfile

from pyramid.httpexceptions import HTTPBadRequest
from pyramid.httpexceptions import HTTPLengthRequired
from pyramid.httpexceptions import HTTPNotFound
from pyramid.response import Response
from pyramid.httpexceptions import HTTPLengthRequired, HTTPBadRequest, HTTPNotFound
from pyramid.view import view_config

from augeias.stores.error import NotFoundException
Expand Down Expand Up @@ -67,20 +72,53 @@ def update_object(self):
}
return res

@view_config(route_name='create_object_and_id', permission='edit')
@view_config(route_name="update_file_in_zip", permission="edit")
def update_file_in_zip(self):
"""
Update a file in an archive object in the data store.
"""
file_content = _get_object_data(self.request)
collection = _retrieve_collection(self.request)
container_key = self.request.matchdict["container_key"]
object_key = self.request.matchdict["object_key"]
file_to_replace = self.request.matchdict["file_name"]
if "new_file_name" not in self.request.params:
raise HTTPBadRequest("new_file_name parameter is required")
new_file_name = self.request.params["new_file_name"]
zip_content = collection.object_store.get_object(
container_key, object_key
)
new_archive = replace_file_in_zip(
zip_content, file_to_replace, file_content.read(), new_file_name
)
collection.object_store.update_object(
container_key, object_key, new_archive
)
res = Response(content_type="application/json", status=200)
res.json_body = {
"container_key": container_key,
"object_key": object_key,
"uri": collection.uri_generator.generate_object_uri(
collection=collection.name,
container=container_key,
object=object_key)
}
return res

@view_config(route_name="create_object_and_id", permission="edit")
def create_object_and_id(self):
"""create an object in the data store and generate an id"""
object_data = _get_object_data(self.request)
collection = _retrieve_collection(self.request)
container_key = self.request.matchdict['container_key']
container_key = self.request.matchdict["container_key"]
object_key = str(uuid.uuid4())
collection.object_store.update_object(
container_key, object_key, object_data)
res = Response(content_type='application/json', status=201)
res = Response(content_type="application/json", status=201)
res.json_body = {
'container_key': container_key,
'object_key': object_key,
'uri': collection.uri_generator.generate_object_uri(
"container_key": container_key,
"object_key": object_key,
"uri": collection.uri_generator.generate_object_uri(
collection=collection.name,
container=container_key,
object=object_key)
Expand Down Expand Up @@ -119,6 +157,20 @@ def get_object(self):
res.body = object_data
return res

@view_config(route_name='get_file_from_zip', permission='view')
def get_object_from_archive(self):
"""retrieve a file from an archive object from the data store"""
collection = _retrieve_collection(self.request)
container_key = self.request.matchdict['container_key']
object_key = self.request.matchdict['object_key']
object_data = collection.object_store.get_object(
container_key, object_key
)
file = get_file_from_archive(object_data, self.request.matchdict['file_name'])
res = Response(content_type='application/octet-stream', status=200)
res.body = file
return res

@view_config(route_name='get_object_info', permission='view')
def get_object_info(self):
"""retrieve object info (mimetype, size, time last modification) from the data store"""
Expand Down Expand Up @@ -276,3 +328,86 @@ def _retrieve_collection(request):
else:
raise HTTPNotFound('collection not found')
return collection


def get_file_from_archive(archive_bytes, file_name):
"""
get a file from a zip/tar in the data store
:param container_key: key of the container in the data store
:param object_key: specific object key for the object in the container
:param file_name: name of the file to get from the zip
:return content of the file or None
"""
archive_bytes_as_file = io.BytesIO(archive_bytes)
for name, file_bytes in get_archive_members(archive_bytes_as_file):
if name == file_name:
return file_bytes
raise HTTPBadRequest("File not found in archive")


def get_archive_members(archive_content):
"""
yield the members of a zip or tar archive
:param archive_content: content of the archive
:return: generator of the members of the archive
"""
with open_archive(archive_content) as archive:
if isinstance(archive, zipfile.ZipFile):
for name in archive.namelist():
yield name, archive.read(name)
else:
for member in archive.getmembers():
f = archive.extractfile(member)
yield member.name, f.read()


def open_archive(archive_content):
"""
Open a zip or tar archive
:param archive_content: content of the archive
:return: the opened archive
"""
if zipfile.is_zipfile(archive_content):
archive_content.seek(0) # the zip check actually reads the bytes, so reset.
return zipfile.ZipFile(archive_content)
else:
archive_content.seek(0)
return tarfile.open(fileobj=archive_content)


def replace_file_in_zip(zip_content, file_to_replace, file_content, new_file_name):
"""
Replace a file in a zip file with new content
:param zip_content: content of the original zip file
:param file_to_replace: name of the file to replace
:param file_content: content of the new file
:param new_file_name: name of the new file
:return: content of the updated zip file
"""
with io.BytesIO(zip_content) as original_zip_buffer:
with zipfile.ZipFile(original_zip_buffer, "r") as original_zip:
# Create a new in-memory zip file
with io.BytesIO() as new_zip_buffer:
with zipfile.ZipFile(
new_zip_buffer, "a", zipfile.ZIP_DEFLATED
) as new_zip:
current_file_names = original_zip.namelist()
if file_to_replace not in current_file_names:
raise HTTPBadRequest("File to replace not found in archive")
for filename in current_file_names:
# Skip the file to be replaced
if filename == file_to_replace:
continue
# Copy all other files
content = original_zip.read(filename)
new_zip.writestr(filename, content)
# Add the new file
new_zip.writestr(new_file_name, file_content)
# Get the content of the new zip file
updated_zip_content = new_zip_buffer.getvalue()

return updated_zip_content
Binary file added fixtures/kerk.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added fixtures/test_archive.zip
Binary file not shown.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
]

setup(name='augeias',
version='0.7.0',
version='0.8.0',
description='Augeias. Stores your files.',
long_description=README + '\n\n' + HISTORY,
classifiers=[
Expand Down
Loading

0 comments on commit 169718d

Please sign in to comment.