feat(routers): migrate hosted_file hash lookup to V3 actions

Ported the legacy '/hosted_file/hash/{hash}' endpoint to the V3 actions router.
The new endpoint '/v3/action/hosted_file/hash/{hosted_file_hash}' supports:
- ID Vision: returns random string IDs instead of internal integers
- Local Check: verifies physical file existence on disk (check_for_local=True)
- Deduplication: enables frontend to check for existing files before upload

Also added PROJECT document for AE Hosted Files migration tracking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-25 13:05:09 -04:00
parent b2adfe409b
commit b9742cfcd8
2 changed files with 158 additions and 1 deletions

View File

@@ -15,7 +15,8 @@ from app.config import settings
from app.db_sql import redis_lookup_id_random, sql_select, sql_update, sql_delete, get_id_random
from app.methods.hosted_file_methods import (
create_hosted_file_obj, load_hosted_file_obj, save_file,
create_hosted_file_link, delete_hosted_file_link, get_hosted_file_link_rec_list
create_hosted_file_link, delete_hosted_file_link, get_hosted_file_link_rec_list,
lookup_file_hash, check_for_hosted_file_hash_file
)
from app.methods.lib_media import convert_file_method
from app.methods.lib_media import clip_video_method
@@ -354,6 +355,38 @@ async def download_file_by_hash_action(
return FileResponse(full_file_path, filename=target_filename, media_type=media_type)
@router.get('/hash/{hosted_file_hash}', response_model=Resp_Body_Base)
async def check_hosted_file_obj_w_hash_action(
response: Response,
hosted_file_hash: str = Path(min_length=64, max_length=64),
check_for_local: Optional[bool] = Query(True),
account: AccountContext = Depends(get_account_context_optional),
delay: DelayParams = Depends(),
):
"""
Look up a hosted_file record by its hash (Deduplication Check).
Optionally verifies physical file existence on disk.
"""
if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s)
if hfid := lookup_file_hash(file_hash=hosted_file_hash):
obj_model = load_hosted_file_obj(hosted_file_id=hfid, model_as_dict=False)
if not obj_model:
return mk_resp(data=False, status_code=404, response=response, status_message="Record found but data could not be loaded.")
if check_for_local:
# We use the model directly to access subdirectory_path even if it's excluded from dicts
sub_dir = getattr(obj_model, 'subdirectory_path', '') or ''
if check := check_for_hosted_file_hash_file(file_hash=hosted_file_hash, sub_dir=sub_dir):
obj_model.hosted_file_found_check = True
obj_model.hosted_file_size_check = check['file_size']
# mk_resp will handle model->dict conversion with proper ID Vision mapping
return mk_resp(data=obj_model)
return mk_resp(data=False, status_code=404, response=response, status_message="No record found for this hash.")
@router.delete('/{hosted_file_id}', response_model=Resp_Body_Base)
async def delete_file_action(
hosted_file_id: str = Path(min_length=11, max_length=22),