Skip to content

Commit

Permalink
Merge pull request #304 from phenobarbital/dev
Browse files Browse the repository at this point in the history
new version with GCS File manager working
  • Loading branch information
phenobarbital authored Oct 4, 2024
2 parents ff209dc + 557dfe3 commit e8d56d8
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 21 deletions.
4 changes: 2 additions & 2 deletions examples/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from navigator.responses import HTMLResponse


# @is_authenticated()
# @user_session()
@is_authenticated()
@user_session()
class ChatHandler(BaseView):
"""
ChatHandler.
Expand Down
1 change: 1 addition & 0 deletions navigator/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
config.get("HOSTS", fallback="localhost").split(",")
)]
DOMAIN = config.get("DOMAIN", fallback="dev.local")
BASE_API_URL = config.get('BASE_API_URL', fallback='http://localhost:5000')
ENABLE_ACCESS_LOG = config.getboolean("ENABLE_ACCESS_LOG", fallback=False)
CORS_MAX_AGE = config.getint('CORS_MAX_AGE', fallback=7200)

Expand Down
88 changes: 70 additions & 18 deletions navigator/utils/gcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import google.auth
from google.cloud import storage
from google.oauth2 import service_account
from navconfig.logging import logging
from ..types import WebApp
from ..applications.base import BaseApplication

Expand Down Expand Up @@ -72,6 +73,10 @@ def __init__(
else:
self.client = storage.Client(credentials=self.credentials)
self.bucket = self.client.bucket(bucket_name)
self.logger = logging.getLogger('storage.GCS')
self.logger.info(
f"Started GCSFileManager for bucket: {bucket_name}"
)

def list_all_files(self, prefix=None):
"""
Expand Down Expand Up @@ -148,9 +153,7 @@ def delete_file(self, blob_name):
def get_response(
self,
reason: str = 'OK',
status: int = 200,
content_type: str = 'text/html',
binary: bool = True
status: int = 200
):
"""
Get a response object.
Expand All @@ -161,23 +164,19 @@ def get_response(
Returns:
aiohttp.web.Response: The response object.
"""
if binary is True:
content_type = 'application/octet-stream'
current = datetime.now(timezone.utc)
expires = current + timedelta(days=7)
last_modified = current - timedelta(hours=1)
return web.Response(
return web.StreamResponse(
status=status,
reason=reason,
content_type=content_type,
headers={
"Pragma": "public", # required,
"Last-Modified": last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT"),
"Expires": expires.strftime("%a, %d %b %Y %H:%M:%S GMT"),
"Connection": "keep-alive",
"Cache-Control": "private", # required for certain browsers,
"Content-Description": "File Transfer",
"Content-Type": content_type,
"Content-Transfer-Encoding": "binary",
"Date": current.strftime("%a, %d %b %Y %H:%M:%S GMT"),
},
Expand All @@ -193,12 +192,16 @@ async def handle_file(self, request):
Returns:
aiohttp.web.StreamResponse: The streaming response.
"""
filename = request.match_info.get('filename', None)
filename = request.match_info.get('filepath', None)

if not filename:
raise web.HTTPNotFound()

# Sanitize the filename to prevent path traversal attacks
filename = os.path.basename(filename)
try:
filename = PurePath(filename).relative_to('/')
except ValueError:
pass
blob = self.bucket.blob(filename)

if not blob.exists():
Expand All @@ -207,22 +210,71 @@ async def handle_file(self, request):
text='File not found'
)

# Get the blob size
blob.reload() # Fetch the latest blob metadata

response = self.get_response()
response.headers['Content-Disposition'] = f'attachment; filename="{filename}"'
response.enable_chunked_encoding = False # Disable chunked encoding
response.content_length = blob.size # Set the total content length

file = os.path.basename(filename)
response.headers['Content-Disposition'] = f'attachment; filename="{file}"'
response.headers['Content-Type'] = blob.content_type or 'application/octet-stream'

# Handle Range requests for partial content
if 'Range' in request.headers:
start, end = self.parse_range_header(
request.headers['Range'], blob.size
)
response.content_length = end - start + 1
response.set_status(206) # Partial Content
response.headers['Content-Range'] = f'bytes {start}-{end}/{blob.size}'
blob_file = blob.open('rb')
blob_file.seek(start)
else:
start = 0
end = blob.size - 1
blob_file = blob.open('rb')

await response.prepare(request)

# Stream the file in chunks to the client
chunk_size = 1024 * 1024 # 1 MB
with blob.open('rb') as blob_file:
while True:
chunk = blob_file.read(chunk_size)
if not chunk:
break
await response.write(chunk)
while start <= end:
chunk = blob_file.read(min(chunk_size, end - start + 1))
if not chunk:
break
await response.write(chunk)
start += len(chunk)

blob_file.close()
await response.write_eof()
return response

def parse_range_header(self, range_header, file_size):
"""
Parses a Range header to get the start and end byte positions.
Args:
range_header (str): The Range header value.
file_size (int): The total size of the file.
Returns:
tuple: A tuple containing the start and end byte positions.
"""
try:
unit, ranges = range_header.strip().split('=')
if unit != 'bytes':
raise ValueError('Invalid unit in Range header')
start, end = ranges.split('-')
start = int(start) if start else 0
end = int(end) if end else file_size - 1
if start > end or end >= file_size:
raise ValueError('Invalid range in Range header')
return start, end
except (ValueError, IndexError) as e:
raise web.HTTPBadRequest(reason=f'Invalid Range header: {e}')

def setup(
self,
app: Union[WebApp, web.Application],
Expand Down Expand Up @@ -250,7 +302,7 @@ def setup(

# Set the route with a wildcard
app.router.add_get(
route + "/{filename}", self.handle_file
route + "/{filepath:.*}", self.handle_file
)

def get_file_url(
Expand Down
2 changes: 1 addition & 1 deletion navigator/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
__description__ = (
"Navigator Web Framework based on aiohttp, " "with batteries included."
)
__version__ = "2.10.27"
__version__ = "2.10.28"
__author__ = "Jesus Lara"
__author_email__ = "[email protected]"
__license__ = "BSD"

0 comments on commit e8d56d8

Please sign in to comment.