Working on range request and seeking for file downloads and streaming

This commit is contained in:
Scott Idem
2023-08-18 11:06:37 -04:00
parent bea4975e7e
commit 79ea4a9856

View File

@@ -1,8 +1,8 @@
import datetime, hashlib, os, pathlib, shutil, time import aiofiles, datetime, hashlib, mimetypes, os, pathlib, shutil, time
from fastapi import APIRouter, Body, Depends, File, Form, Header, HTTPException, Query, Response, status, UploadFile from fastapi import APIRouter, Body, Depends, File, Form, Header, HTTPException, Query, Response, status, UploadFile
# from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from fastapi.responses import StreamingResponse # from fastapi.responses import StreamingResponse
from baize.asgi.responses import FileResponse # from baize.asgi.responses import FileResponse
# from baize.wsgi.responses import FileResponse # from baize.wsgi.responses import FileResponse
from pydantic import BaseModel, EmailStr, Field from pydantic import BaseModel, EmailStr, Field
from typing import Dict, List, Optional, Set, Union from typing import Dict, List, Optional, Set, Union
@@ -160,7 +160,7 @@ async def directory_check(
async def download_hosted_file( async def download_hosted_file(
hosted_file_id: str = Query(..., min_length=11, max_length=22), hosted_file_id: str = Query(..., min_length=11, max_length=22),
filename: str = Query(None, min_length=4, max_length=100), filename: str = Query(None, min_length=4, max_length=100),
streaming: bool = False, # streaming: bool = False,
commons: Common_Route_Params = Depends(common_route_params), commons: Common_Route_Params = Depends(common_route_params),
): ):
@@ -172,7 +172,6 @@ async def download_hosted_file(
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The hosted_file ID was invalid or not found.') else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The hosted_file ID was invalid or not found.')
hosted_files_path = settings.FILES_PATH['hosted_files_root'] hosted_files_path = settings.FILES_PATH['hosted_files_root']
# hosted_files_path = '/home/scott/tmp/hosted_files_dev/'
log.info(f'Hosted Files Path: {hosted_files_path}') log.info(f'Hosted Files Path: {hosted_files_path}')
if hosted_file_obj := load_hosted_file_obj( if hosted_file_obj := load_hosted_file_obj(
@@ -206,38 +205,49 @@ async def download_hosted_file(
# return FileResponse(file_path_w_subdir, filename=filename) # return FileResponse(file_path_w_subdir, filename=filename)
log.info('Hosted file found on server.') log.info('Hosted file found on server.')
if streaming: # if streaming:
log.warning('Streaming!!!') # log.warning('Streaming!!!')
def iterfile(): # # def iterfile(): #
with open(file_path_w_subdir, mode="rb") as file_like: # # with open(file_path_w_subdir, mode="rb") as file_like: #
yield from file_like # # yield from file_like #
return StreamingResponse(iterfile(), media_type='video/mp4') # return StreamingResponse(iterfile(), media_type='video/mp4')
else: # else:
return FileResponse(file_path_w_subdir, filename=filename)
return FileResponse(file_path_w_subdir, filename=filename)
else: else:
log.error(f'The hosted file was not found on the server. Hash: {hash_sha256}') log.error(f'The hosted file was not found on the server. Hash: {hash_sha256}')
return mk_resp(data=None, status_code=404, response=commons.response, status_message=f'The hosted file was not found on the server. Hash: {hash_sha256}') # Not Found return mk_resp(data=None, status_code=404, response=commons.response, status_message=f'The hosted file was not found on the server. Hash: {hash_sha256}') # Not Found
# ### END ### API Hosted File ### download_hosted_file() ### # ### END ### API Hosted File ### download_hosted_file() ###
# class ChunkFileResponse(Response): # ### BEGIN ### API Hosted File ### file_streamer() ###
# def __init__(self, *args, **kwargs) -> None: # Updated 2023-08-18
# if len(args) == 1: async def file_streamer(path: str, start: int, end: int):
# kwargs = args[0] chunk_size = 8192 # 8KB
# [kwargs.pop(k) for k in ['status_code', 'cookies', 'stat_result']]
# super().__init__(**kwargs) async with aiofiles.open(path, mode='rb') as f:
await f.seek(start)
while True:
chunk_start = await f.tell()
if chunk_start >= end:
break
bytes_to_read = min(chunk_size, end - chunk_start)
data = await f.read(bytes_to_read)
if not data:
break
yield data
# ### END ### API Hosted File ### file_streamer() ###
# ### BEGIN ### API Hosted File ### stream_hosted_file() ### # ### BEGIN ### API Hosted File ### stream_hosted_file() ###
# Updated 2023-08-17 # Updated 2023-08-18
# @router.get('/{hosted_file_id}/stream', response_model=Resp_Body_Base) @router.get('/{hosted_file_id}/stream_v2')
# @router.get('/{hosted_file_id}/stream', response_class=Resp_Body_Base)
# @router.get('/{hosted_file_id}/stream', response_class=FileResponse)
@router.get('/{hosted_file_id}/stream')
# def stream_hosted_file(
async def stream_hosted_file( async def stream_hosted_file(
hosted_file_id: str = Query(..., min_length=11, max_length=22), hosted_file_id: str = Query(..., min_length=11, max_length=22),
filename: str = Query(None, min_length=4, max_length=100), filename: str = Query(None, min_length=4, max_length=100),
streaming: bool = True, # streaming: bool = True,
range: str = Header(),
commons: Common_Route_Params = Depends(common_route_params), commons: Common_Route_Params = Depends(common_route_params),
): ):
@@ -249,7 +259,6 @@ async def stream_hosted_file(
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The hosted_file ID was invalid or not found.') else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The hosted_file ID was invalid or not found.')
hosted_files_path = settings.FILES_PATH['hosted_files_root'] hosted_files_path = settings.FILES_PATH['hosted_files_root']
# hosted_files_path = '/home/scott/tmp/hosted_files_dev/'
log.info(f'Hosted Files Path: {hosted_files_path}') log.info(f'Hosted Files Path: {hosted_files_path}')
if hosted_file_obj := load_hosted_file_obj( if hosted_file_obj := load_hosted_file_obj(
@@ -279,35 +288,120 @@ async def stream_hosted_file(
log.info(f'Full file path with subdirectory: {file_path_w_subdir}') log.info(f'Full file path with subdirectory: {file_path_w_subdir}')
if os.path.exists(file_path_w_subdir): if os.path.exists(file_path_w_subdir):
# log.info('Hosted file found on server.')
# return FileResponse(file_path_w_subdir, filename=filename)
log.info('Hosted file found on server.') log.info('Hosted file found on server.')
file_size = os.stat(file_path_w_subdir).st_size
range_parts = range.replace('bytes=', '').split('-')
start = int(range_parts[0])
end = int(range_parts[1]) if len(range_parts) > 1 and range_parts[1] else file_size - 1
# return ChunkFileResponse(filepath=file_path_w_subdir) if start >= file_size:
raise HTTPException(status_code=status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE)
# return FileResponse(file_path_w_subdir, filename=filename) end = min(end, file_size - 1)
content_length = end - start + 1
return StreamingResponse(
if streaming: file_streamer(file_path_w_subdir, start, end + 1),
log.warning('Streaming!!!') # media_type=mimetypes.guess_type(file_path_w_subdir.name)[0],
return FileResponse(filepath=file_path_w_subdir, content_type='video/mp4', download_name=filename) media_type = mimetypes.guess_type(filename)[0],
status_code = status.HTTP_206_PARTIAL_CONTENT,
# def iterfile(): # headers = {
# with open(file_path_w_subdir, mode="rb") as file_like: # 'Accept-Ranges': 'bytes',
# yield from file_like # 'Content-Range': f'bytes {start}-{end}/{file_size}',
# return FileResponse(iterfile(), download_name=filename) 'Content-Length': str(content_length),
}
# return StreamingResponse(iterfile(), media_type='video/mp4') )
else:
return FileResponse(file_path_w_subdir, filename=filename)
else: else:
log.error(f'The hosted file was not found on the server. Hash: {hash_sha256}') log.error(f'The hosted file was not found on the server. Hash: {hash_sha256}')
return mk_resp(data=None, status_code=404, response=commons.response, status_message=f'The hosted file was not found on the server. Hash: {hash_sha256}') # Not Found return mk_resp(data=None, status_code=404, response=commons.response, status_message=f'The hosted file was not found on the server. Hash: {hash_sha256}') # Not Found
# ### END ### API Hosted File ### stream_hosted_file() ### # ### END ### API Hosted File ### stream_hosted_file() ###
# # ### BEGIN ### API Hosted File ### stream_hosted_file() ###
# # Updated 2023-08-17
# # @router.get('/{hosted_file_id}/stream', response_model=Resp_Body_Base)
# # @router.get('/{hosted_file_id}/stream', response_class=Resp_Body_Base)
# # @router.get('/{hosted_file_id}/stream', response_class=FileResponse)
# @router.get('/{hosted_file_id}/stream')
# # def stream_hosted_file(
# async def stream_hosted_file(
# hosted_file_id: str = Query(..., min_length=11, max_length=22),
# filename: str = Query(None, min_length=4, max_length=100),
# streaming: bool = True,
# commons: Common_Route_Params = Depends(common_route_params),
# ):
# log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
# log.debug(locals())
# # ### SECTION ### Secondary data validation
# if hosted_file_id := redis_lookup_id_random(record_id_random=hosted_file_id, table_name='hosted_file'): pass
# else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The hosted_file ID was invalid or not found.')
# hosted_files_path = settings.FILES_PATH['hosted_files_root']
# # hosted_files_path = '/home/scott/tmp/hosted_files_dev/'
# log.info(f'Hosted Files Path: {hosted_files_path}')
# if hosted_file_obj := load_hosted_file_obj(
# hosted_file_id = hosted_file_id,
# # inc_hosted_file = True,
# inc_hosted_file_link_list = True,
# ):
# pass
# else:
# return mk_resp(data=False, status_code=400, response=commons.response) # Bad Request
# if not filename:
# filename = hosted_file_obj.filename
# log.info(f'Filename: {filename}')
# dir_path = hosted_file_obj.directory_path
# subdir_path = hosted_file_obj.subdirectory_path
# hash_sha256 = hosted_file_obj.hash_sha256
# hash_filename = hash_sha256+'.file'
# if subdir_path:
# full_subdirectory_path = os.path.join(hosted_files_path, subdir_path)
# else:
# full_subdirectory_path = hosted_files_path
# log.debug(full_subdirectory_path)
# pathlib.Path(full_subdirectory_path).mkdir(parents=True, exist_ok=True)
# file_path_w_subdir = os.path.join(full_subdirectory_path, hash_filename)
# log.info(f'Full file path with subdirectory: {file_path_w_subdir}')
# if os.path.exists(file_path_w_subdir):
# # log.info('Hosted file found on server.')
# # return FileResponse(file_path_w_subdir, filename=filename)
# log.info('Hosted file found on server.')
# # return ChunkFileResponse(filepath=file_path_w_subdir)
# # return FileResponse(file_path_w_subdir, filename=filename)
# if streaming:
# from baize.asgi.responses import FileResponse
# log.warning('Streaming!!!')
# return FileResponse(filepath=file_path_w_subdir, content_type='application/octet-stream', download_name=filename)
# # return FileResponse(filepath=file_path_w_subdir, content_type='video/mp4', download_name=filename)
# # def iterfile(): #
# # with open(file_path_w_subdir, mode="rb") as file_like: #
# # yield from file_like #
# # return FileResponse(iterfile(), download_name=filename)
# # return StreamingResponse(iterfile(), media_type='video/mp4')
# else:
# return FileResponse(file_path_w_subdir, filename=filename)
# else:
# log.error(f'The hosted file was not found on the server. Hash: {hash_sha256}')
# return mk_resp(data=None, status_code=404, response=commons.response, status_message=f'The hosted file was not found on the server. Hash: {hash_sha256}') # Not Found
# # ### END ### API Hosted File ### stream_hosted_file() ###
# ### BEGIN ### API Hosted File Route ### upload_files() ### # ### BEGIN ### API Hosted File Route ### upload_files() ###
# This just needs to return the correct model for a hosted_file # This just needs to return the correct model for a hosted_file
# Everything else seems to be working well # Everything else seems to be working well