Skip to content

Commit

Permalink
Add @dossier-transfers/<id>/blob/<doc-UID> endpoint to download blobs.
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasgraf committed Apr 11, 2024
1 parent 15b1156 commit f551efc
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 8 deletions.
17 changes: 12 additions & 5 deletions opengever/dossiertransfer/api/base.py
Expand Up @@ -85,15 +85,19 @@ def has_valid_token(self):
return False

def _extract_transfer_id(self):
# We'll accept zero (listing) or one (get by id) params, but not more
if len(self.params) > 1:
# We'll accept
# - zero (listing)
# - one (get by id) or
# - three (blob download)
# params, but not more
if len(self.params) not in (0, 1, 3):
raise BadRequest(
'Must supply either exactly one {transfer_id} path parameter '
'to fetch a specific transfer, or no parameter for a '
'listing of all transfers.')
'to fetch a specific transfer, no parameter for a '
'listing of all transfers, or three to fetch a blob.')

# We have a valid number of parameters for the given endpoint
if len(self.params) == 1:
if len(self.params) in (1, 3):
try:
transfer_id = int(self.params[0])
except ValueError:
Expand All @@ -108,6 +112,9 @@ def _is_inbox_user(self, user_id):
return True
return False

def is_blob_request(self):
return len(self.params) == 3 and self.params[1] == 'blob'

def locate_transfer(self):
transfer_id = self.transfer_id
if not DossierTransfer.get(transfer_id):
Expand Down
66 changes: 63 additions & 3 deletions opengever/dossiertransfer/api/get.py
@@ -1,15 +1,39 @@
from opengever.base.behaviors.utils import set_attachment_content_disposition
from opengever.base.security import elevated_privileges
from opengever.document.behaviors import IBaseDocument
from opengever.dossiertransfer.api.base import DossierTransferLocator
from plone import api
from plone.namedfile.utils import stream_data
from plone.restapi.services import _no_content_marker
from zExceptions import NotFound
from zExceptions.unauthorized import Unauthorized
import json


class DossierTransfersGet(DossierTransferLocator):
"""API Endpoint that returns DossierTransfers.
GET /@dossier-transfers/42 HTTP/1.1
GET /@dossier-transfers HTTP/1.1
GET /@dossier-transfers/blob/<document-UID> HTTP/1.1
"""

def render(self):
self.check_permission()
content = self.reply()

# Exception for blob download: Do not attempt to JSON serialize
# blob download responses. Instead directly return the
# IStreamIterator and let ZPublisher handle it.
if self.is_blob_request():
return content

if content is not _no_content_marker:
self.request.response.setHeader("Content-Type", self.content_type)
return json.dumps(
content, indent=2, sort_keys=True, separators=(", ", ": ")
)

def check_permission(self):
if self.has_valid_token():
# Server-to-server requests to fetch the full transfer contents are
Expand All @@ -20,7 +44,7 @@ def check_permission(self):
return super(DossierTransfersGet, self).check_permission()

def reply(self):
if self.transfer_id:
if self.transfer_id and len(self.params) == 1:
# Get transfer by id
transfer = self.locate_transfer()
if self.full_content_requested():
Expand All @@ -31,8 +55,14 @@ def reply(self):
return self.serialize(transfer, full_content=True)
return self.serialize(transfer)

# List all transfers
return self.list()
elif self.is_blob_request():
return self.stream_document_blob()

if len(self.params) == 0:
# List all transfers
return self.list()
else:
raise NotFound

def list(self):
transfers = self.list_transfers()
Expand All @@ -44,3 +74,33 @@ def list(self):

def full_content_requested(self):
return bool(self.request.form.get('full_content', False))

def stream_document_blob(self):
transfer = self.locate_transfer()
document_uid = self.params[2]

with elevated_privileges():
root_dossier = api.content.uuidToObject(transfer.root)
catalog = api.portal.get_tool('portal_catalog')

query = {
'path': '/'.join(root_dossier.getPhysicalPath()),
'object_provides': IBaseDocument.__identifier__,
'UID': document_uid,
}
brains = catalog.unrestrictedSearchResults(**query)

if len(brains) == 0:
raise NotFound

if not transfer.all_documents:
if document_uid not in transfer.documents:
raise Unauthorized

assert len(brains) == 1
document = brains[0].getObject()
namedfile = document.file

filename = getattr(namedfile, 'filename', u'document.bin').encode('utf-8')
set_attachment_content_disposition(self.request, filename, namedfile)
return stream_data(namedfile)
78 changes: 78 additions & 0 deletions opengever/dossiertransfer/tests/test_api_get.py
Expand Up @@ -4,6 +4,7 @@
from ftw.testbrowser import browsing
from ftw.testing import freeze
from opengever.base.model import create_session
from opengever.base.security import elevated_privileges
from opengever.dossier.behaviors.participation import IParticipationAware
from opengever.kub.testing import KuBIntegrationTestCase
from opengever.ogds.base.utils import get_current_admin_unit
Expand Down Expand Up @@ -1085,6 +1086,83 @@ def test_contact_membership_serialization(self, mocker, browser):

self.assertDictContainsSubset(expected_contact, contact)

@browsing
def test_document_blob_download(self, browser):
transfer = self.create_transfer()

headers = self.api_headers.copy()
headers.update({'X-GEVER-Dossier-Transfer-Token': transfer.token})

with elevated_privileges():
doc = self.resolvable_document
doc_uid = doc.UID()
filename = doc.file.filename
filesize = doc.file.getSize()
filedata = doc.file.data

with freeze(FROZEN_NOW):
browser.open(
self.portal,
view='@dossier-transfers/%s/blob/%s' % (transfer.id, doc_uid),
method='GET', headers=headers)

self.assertEqual(200, browser.status_code)

expected_headers = {
'content-disposition': 'attachment; filename="%s"' % filename,
'content-length': str(filesize),
'content-type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}
self.assertDictContainsSubset(expected_headers, browser.headers)
self.assertEqual(filesize, len(browser.contents))
self.assertEqual(filedata, browser.contents)

@browsing
def test_document_blob_download_requires_token(self, browser):
transfer = self.create_transfer()

with elevated_privileges():
doc_uid = self.resolvable_document.UID()

with freeze(FROZEN_NOW):
with browser.expect_http_error(code=401):
browser.open(
self.portal,
view='@dossier-transfers/%s/blob/%s' % (transfer.id, doc_uid),
method='GET', headers=self.api_headers)

@browsing
def test_document_blob_download_returns_404_for_nonexistent_doc(self, browser):
transfer = self.create_transfer()

headers = self.api_headers.copy()
headers.update({'X-GEVER-Dossier-Transfer-Token': transfer.token})

doc_uid = '404-doesnt-exist'
with freeze(FROZEN_NOW):
with browser.expect_http_error(code=404):
browser.open(
self.portal,
view='@dossier-transfers/%s/blob/%s' % (transfer.id, doc_uid),
method='GET', headers=headers)

@browsing
def test_document_blob_download_returns_unauthorized_for_unselected_doc(self, browser):
transfer = self.create_transfer(all_documents=False, documents=[])

headers = self.api_headers.copy()
headers.update({'X-GEVER-Dossier-Transfer-Token': transfer.token})

with elevated_privileges():
doc_uid = self.resolvable_document.UID()

with freeze(FROZEN_NOW):
with browser.expect_http_error(code=401):
browser.open(
self.portal,
view='@dossier-transfers/%s/blob/%s' % (transfer.id, doc_uid),
method='GET', headers=headers)


class TestDossierTransfersGetPermissionsBase(IntegrationTestCase):

Expand Down

0 comments on commit f551efc

Please sign in to comment.