Add basic support for "download" and "rawbytes" API views

This commit is contained in:
Lance Edgar 2021-01-06 13:12:27 -06:00
parent fd1342c605
commit 4d8e29c892
2 changed files with 74 additions and 9 deletions

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2020 Lance Edgar # Copyright © 2010-2021 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -26,6 +26,7 @@ Tailbone Web API - Master View (v2)
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from pyramid.response import FileResponse
from cornice import resource, Service from cornice import resource, Service
from tailbone.api import APIMasterView from tailbone.api import APIMasterView
@ -41,6 +42,8 @@ class APIMasterView2(APIMasterView):
editable = True editable = True
deletable = True deletable = True
supports_autocomplete = False supports_autocomplete = False
supports_download = False
supports_rawbytes = False
@classmethod @classmethod
def establish_method(cls, method_name): def establish_method(cls, method_name):
@ -85,6 +88,48 @@ class APIMasterView2(APIMasterView):
self.Session.delete(obj) self.Session.delete(obj)
self.Session.flush() self.Session.flush()
##############################
# download
##############################
def download(self):
"""
GET view allowing for download of a single file, which is attached to a
given record.
"""
obj = self.get_object()
filename = self.request.GET.get('filename', None)
if not filename:
raise self.notfound()
path = self.download_path(obj, filename)
response = self.file_response(path)
return response
def download_path(self, obj, filename):
"""
Should return absolute path on disk, for the given object and filename.
Result will be used to return a file response to client.
"""
raise NotImplementedError
def rawbytes(self):
"""
GET view allowing for direct access to the raw bytes of a file, which
is attached to a given record. Basically the same as 'download' except
this does not come as an attachment.
"""
obj = self.get_object()
filename = self.request.GET.get('filename', None)
if not filename:
raise self.notfound()
path = self.download_path(obj, filename)
response = self.file_response(path, attachment=False)
return response
@classmethod @classmethod
def defaults(cls, config): def defaults(cls, config):
cls._defaults(config) cls._defaults(config)
@ -137,5 +182,24 @@ class APIMasterView2(APIMasterView):
if cls.supports_autocomplete: if cls.supports_autocomplete:
autocomplete = Service(name='{}.autocomplete'.format(route_prefix), autocomplete = Service(name='{}.autocomplete'.format(route_prefix),
path='{}/autocomplete'.format(collection_url_prefix)) path='{}/autocomplete'.format(collection_url_prefix))
autocomplete.add_view('GET', 'autocomplete', klass=cls) autocomplete.add_view('GET', 'autocomplete', klass=cls,
permission='{}.list'.format(permission_prefix))
config.add_cornice_service(autocomplete) config.add_cornice_service(autocomplete)
# download
if cls.supports_download:
download = Service(name='{}.download'.format(route_prefix),
# TODO: probably should allow for other (composite?) key fields
path='{}/{{uuid}}/download'.format(object_url_prefix))
download.add_view('GET', 'download', klass=cls,
permission='{}.download'.format(permission_prefix))
config.add_cornice_service(download)
# rawbytes
if cls.supports_rawbytes:
rawbytes = Service(name='{}.rawbytes'.format(route_prefix),
# TODO: probably should allow for other (composite?) key fields
path='{}/{{uuid}}/rawbytes'.format(object_url_prefix))
rawbytes.add_view('GET', 'rawbytes', klass=cls,
permission='{}.download'.format(permission_prefix))
config.add_cornice_service(rawbytes)

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2020 Lance Edgar # Copyright © 2010-2021 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -166,7 +166,7 @@ class View(object):
return render_to_response('json', data, return render_to_response('json', data,
request=self.request) request=self.request)
def file_response(self, path, filename=None): def file_response(self, path, filename=None, attachment=True):
""" """
Returns a generic FileResponse from the given path Returns a generic FileResponse from the given path
""" """
@ -174,11 +174,12 @@ class View(object):
return self.notfound() return self.notfound()
response = FileResponse(path, request=self.request) response = FileResponse(path, request=self.request)
response.content_length = os.path.getsize(path) response.content_length = os.path.getsize(path)
if not filename: if attachment:
filename = os.path.basename(path) if not filename:
if six.PY2: filename = os.path.basename(path)
filename = filename.encode('ascii', 'replace') if six.PY2:
response.content_disposition = str('attachment; filename="{}"'.format(filename)) filename = filename.encode('ascii', 'replace')
response.content_disposition = str('attachment; filename="{}"'.format(filename))
return response return response
def get_quickie_context(self): def get_quickie_context(self):