
    +j                        d Z ddlmZ ddlZddlZddlZddlZddlmZm	Z	 ddl
mZ ddlmZmZmZmZmZ ddlmZ dd	lmZ  ej        e          Zd!dZd"dZd#dZddd$dZd%dZd&dZd'd ZdS )(u  
LightClaw — remote file storage client.
Mirrors: src/file-storage.ts

All binary transfer with ai-server happens here.  HTTP contract
(reverse-engineered from the TS client):

    Upload   : POST   /drive/save
               Content-Type: multipart/form-data
               Fields  : file (binary) + filePath (string)
               Auth    : Bearer <apiKey>, x-product: channel
               Success : { code: 0, data: { uploaded: true, ... } }

    Download : GET    /drive/preview?filePath=<filePath>
               Auth    : Bearer <apiKey>, x-product: channel
               Success : HTTP 200, body = raw file bytes

The server-side ``filePath`` is constructed client-side as
``${Date.now()}/${fileName}`` so that concurrent uploads of the same
name never collide.  The returned public URL is constructed locally as
``${SERVER_UPLOAD_BASE_URL}${API_PATH_DOWNLOAD}?filePath=<filePath>``
rather than trusting the server response — matches TS behaviour and
means the public URL is deterministic given a known ``filePath``.
    )annotationsN)OptionalTuple)quote   )API_PATH_DOWNLOADAPI_PATH_UPLOADDOWNLOAD_TIMEOUTSERVER_UPLOAD_BASE_URLUPLOAD_TIMEOUT)guess_mime_by_ext)build_auth_headers	file_pathstrreturnc                |    | sdS |                      d          r| S t           t           dt          | d           S )zBuild a public ``/drive/preview`` URL for *file_path*.

    Does not perform any network IO.  Mirrors TS ``getFileDownloadUrl``.
     zhttp://zhttps://z
?filePath=)safe)
startswithr   r   r   )r   s    @/home/agentuser/.hermes/plugins/lightclawbot/src/file_storage.pyget_file_download_urlr   4   sV    
  r344 $^&7^^5Y[C\C\C\^^^    	file_namec                R    t          t          j                    dz             d|  S )zDGenerate the remote ``filePath`` (millisecond timestamp dir + name).i  /)inttime)r   s    r   _make_server_file_pathr   A   s)    $)++$%%33	333r   databytesmimeapi_keyTuple[str, str]c                  K   ddl }t          |          }|                                }|                    d| ||           |                    d|           t           t
           }t          |          }		 |                    |||	|                    t                              4 d{V 	 }
|
j
        dk    r<|
                                 d{V dd         }t          d	|
j
         d
|           |
                    d           d{V }ddd          d{V  n# 1 d{V swxY w Y   n0# t          j        $ r}t          dt           d          |d}~ww xY wt!          |t"                    st          d|          |                    d          pi }|                    d          dk    r(|                    d          du r|t'          |          fS t          d|           )zInternal: POST /drive/save and return ``(file_path, public_url)``.

    Raises ``RuntimeError`` on any failure (HTTP error, timeout, server
    rejection).  The caller decides whether to fall back gracefully.
    r   Nfile)filenamecontent_typefilePathtotal)r    headerstimeout   zUpload HTTP : )r(   zUpload timeout after sz#Upload rejected: non-JSON response r    codeuploadedTzUpload rejected: )aiohttpr   FormData	add_fieldr   r	   r   postClientTimeoutr   statustextRuntimeErrorjsonasyncioTimeoutError
isinstancedictgetr   )r    r   r"   r#   sessionr3   r   formurlr,   respbodyresultexc
data_fields                  r   
_do_uploadrI   F   s      NNN&y11IDNN64)$NGGGNN:y)))#
6_
6
6C ))GO<<)))??	   
 
 		8 		8 		8 		8 		8 		8 		8 		8
 {c!!"iikk))))))4C40"#G$+#G#G#G#GHHH99$977777777F		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8 		8  O O OD>DDDEE3NO fd## MKKKLLLF##)rJzz&Q:>>*#=#=#E#E/	::::
3633
4
44s=   98D: 2A$D(D: (
D22D: 5D26D: :E'	E""E')custom_file_name
local_pathrJ   Optional[str]c                  K   | rt           j                            |           st          d|            t           j                            |           st          d|            |pt           j                            |           }t          |          }t          | d          5 }|	                                }ddd           n# 1 swxY w Y   t          |||||           d{V \  }}	t                              d| |	           ||	fS )a  Upload a local file to ai-server.

    Returns ``(server_file_path, public_url)``.

    :param local_path:        absolute path of the file to upload.
    :param api_key:           tenant API key (Bearer auth).
    :param session:           an ``aiohttp.ClientSession``.
    :param custom_file_name:  override the remote file name.
    :raises FileNotFoundError: when *local_path* does not exist or is
        not a regular file.
    :raises RuntimeError:      for any network / server rejection.
    zFile not found: zNot a regular file: rbNr    r   r"   r#   rA   u   [lightclaw] uploaded %s → %s)ospathexistsFileNotFoundErrorisfiler:   basenamer   openreadrI   loggerinfo)
rK   r#   rA   rJ   r   r"   fppayloadr   
public_urls
             r   upload_file_to_serverr]   z   sk     &  ARW^^J77 A ?: ? ?@@@7>>*%% @>*>>??? @BG$4$4Z$@$@IY''D	j$		 2''))               #-	# # #      Iz KK0*jIIIj  s   'CCCbufferc                  K   | t          d          t          | |pd|pd||           d{V \  }}t                              dt	          |           |           ||fS )zKUpload an in-memory buffer. Same contract as :func:`upload_file_to_server`.Nzbuffer must not be Noner&   application/octet-streamrO   u-   [lightclaw] uploaded buffer (%d bytes) → %s)r:   rI   rX   rY   len)r^   r   r"   r#   rA   r   r\   s          r   upload_buffer_to_serverrb      s       ~4555",y2F//# # #      Iz
 KK?VjYYYj  r   c               @   K   t          | ||           d{V \  }}|S )z;Convenience wrapper: upload and return only the public URL.)r#   rA   N)r]   )rK   r#   rA   _rC   s        r   upload_and_get_public_urlre      sL       )GW        FAs Jr   file_path_or_urlTuple[bytes, str, str]c               ,  K   ddl }| st          d          |                     d          r| nt          |           }t	          |          }	 |                    |||                    t                              4 d{V 	 }|j        dk    r<|	                                 d{V dd         }t          d|j         d	|           |
                                 d{V }|j                            d
d          }	ddd          d{V  n# 1 d{V swxY w Y   n0# t          j        $ r}
t          dt           d          |
d}
~
ww xY w|                     dd          d         }|                    dd          d         pd}|||	fS )u  GET ``/drive/preview`` for *file_path_or_url*.

    Returns ``(buffer, file_name, content_type)``.  If a full http(s) URL
    is passed, it's used directly — otherwise it's treated as a remote
    ``filePath`` and wrapped with :func:`get_file_download_url`.
    r   Nzfile_path_or_url is requiredr   r*   )r,   r-   r.   zDownload HTTP r/   zcontent-typer`   zDownload timeout after r0   ?r   r   r&   )r3   r:   r   r   r   r@   r7   r
   r8   r9   rW   r,   r<   r=   splitrsplit)rf   r#   rA   r3   rC   r,   rD   rE   bufr(   rG   tailr   s                r   download_file_from_serverro      s      NNN ;9::: ))*ABB8%&677  !))GS;;))0@)AA  
 
 		X 		X 		X 		X 		X 		X 		X 		X {c!!"iikk))))))4C40"#IDK#I#I4#I#IJJJ		######C<++N<VWWL		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X 		X  S S SH5EHHHIIsRS !!#q))!,DC##B'16I	<''s=   7D' A=DD' 
DD' "D#D' 'E6EE)r   r   r   r   )r   r   r   r   )
r    r!   r   r   r"   r   r#   r   r   r$   )rK   r   r#   r   rJ   rL   r   r$   )
r^   r!   r   r   r"   r   r#   r   r   r$   )rK   r   r#   r   r   r   )rf   r   r#   r   r   rg   ) __doc__
__future__r   r<   loggingrP   r   typingr   r   urllib.parser   configr   r	   r
   r   r   mediar   tenancyr   	getLogger__name__rX   r   r   rI   r]   rb   re   ro    r   r   <module>r{      s   2 # " " " " "   				  " " " " " " " "                    % $ $ $ $ $ ' ' ' ' ' '		8	$	$
_ 
_ 
_ 
_4 4 4 4
-5 -5 -5 -5r '+#! #! #! #! #! #!L! ! ! !(
 
 
 
"'( '( '( '( '( '(r   