diff --git a/app/routers/hosted_file.py b/app/routers/hosted_file.py index d7e17a4..bbbfb1b 100644 --- a/app/routers/hosted_file.py +++ b/app/routers/hosted_file.py @@ -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.responses import FileResponse, StreamingResponse -from fastapi.responses import StreamingResponse -from baize.asgi.responses import FileResponse +from fastapi.responses import FileResponse, StreamingResponse +# from fastapi.responses import StreamingResponse +# from baize.asgi.responses import FileResponse # from baize.wsgi.responses import FileResponse from pydantic import BaseModel, EmailStr, Field from typing import Dict, List, Optional, Set, Union @@ -160,7 +160,7 @@ async def directory_check( async def download_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 = False, + # streaming: bool = False, 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.') 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( @@ -206,38 +205,49 @@ async def download_hosted_file( # return FileResponse(file_path_w_subdir, filename=filename) log.info('Hosted file found on server.') - if streaming: - log.warning('Streaming!!!') - def iterfile(): # - with open(file_path_w_subdir, mode="rb") as file_like: # - yield from file_like # - return StreamingResponse(iterfile(), media_type='video/mp4') - else: - return FileResponse(file_path_w_subdir, filename=filename) + # if streaming: + # log.warning('Streaming!!!') + # def iterfile(): # + # with open(file_path_w_subdir, mode="rb") as file_like: # + # yield from file_like # + # 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 ### download_hosted_file() ### -# class ChunkFileResponse(Response): -# def __init__(self, *args, **kwargs) -> None: -# if len(args) == 1: -# kwargs = args[0] -# [kwargs.pop(k) for k in ['status_code', 'cookies', 'stat_result']] -# super().__init__(**kwargs) +# ### BEGIN ### API Hosted File ### file_streamer() ### +# Updated 2023-08-18 +async def file_streamer(path: str, start: int, end: int): + chunk_size = 8192 # 8KB + + 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() ### -# 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( +# Updated 2023-08-18 +@router.get('/{hosted_file_id}/stream_v2') 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, + # streaming: bool = True, + + range: str = Header(), 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.') 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( @@ -279,35 +288,120 @@ async def stream_hosted_file( 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.') + 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 - - if streaming: - log.warning('Streaming!!!') - 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) + return StreamingResponse( + file_streamer(file_path_w_subdir, start, end + 1), + # media_type=mimetypes.guess_type(file_path_w_subdir.name)[0], + media_type = mimetypes.guess_type(filename)[0], + status_code = status.HTTP_206_PARTIAL_CONTENT, + headers = { + 'Accept-Ranges': 'bytes', + 'Content-Range': f'bytes {start}-{end}/{file_size}', + 'Content-Length': str(content_length), + } + ) 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 ### 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() ### # This just needs to return the correct model for a hosted_file # Everything else seems to be working well