22
33from __future__ import annotations
44
5+ import io
6+ import asyncio
7+ import tarfile
58from typing import Dict , Mapping , Optional
69from pathlib import Path
10+ from datetime import timedelta
711from typing_extensions import Unpack
812
913import httpx
@@ -350,7 +354,7 @@ async def upload_from_file(
350354 path = Path (file_path )
351355
352356 try :
353- content = path .read_bytes ()
357+ content = await asyncio . to_thread ( lambda : path .read_bytes () )
354358 except OSError as error :
355359 raise OSError (f"Failed to read file { path } : { error } " ) from error
356360
@@ -361,6 +365,50 @@ async def upload_from_file(
361365 await obj .complete ()
362366 return obj
363367
368+ async def upload_from_dir (
369+ self ,
370+ dir_path : str | Path ,
371+ * ,
372+ name : Optional [str ] = None ,
373+ metadata : Optional [Dict [str , str ]] = None ,
374+ ttl : Optional [timedelta ] = None ,
375+ ** options : Unpack [LongRequestOptions ],
376+ ) -> AsyncStorageObject :
377+ """Create and upload an object from a local directory.
378+
379+ The resulting object will be uploaded as a compressed tarball.
380+
381+ :param dir_path: Local filesystem directory path to tar
382+ :type dir_path: str | Path
383+ :param name: Optional object name; defaults to the directory name + '.tar.gz'
384+ :type name: Optional[str]
385+ :param metadata: Optional key-value metadata
386+ :type metadata: Optional[Dict[str, str]]
387+ :param ttl: Optional Time-To-Live, after which the object is automatically deleted
388+ :type ttl: Optional[timedelta]
389+ :param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
390+ :return: Wrapper for the uploaded object
391+ :rtype: AsyncStorageObject
392+ :raises OSError: If the local file cannot be read
393+ """
394+ path = Path (dir_path )
395+ name = name or f"{ path .name } .tar.gz"
396+ ttl_ms = int (ttl .total_seconds ()) * 1000 if ttl else None
397+
398+ def synchronous_io () -> io .BytesIO :
399+ tar_buffer = io .BytesIO ()
400+ with tarfile .open (fileobj = tar_buffer , mode = "w:gz" ) as tar :
401+ tar .add (path , arcname = "." , recursive = True )
402+ tar_buffer .seek (0 )
403+ return tar_buffer
404+
405+ tar_buffer = await asyncio .to_thread (synchronous_io )
406+
407+ obj = await self .create (name = name , content_type = "tgz" , metadata = metadata , ttl_ms = ttl_ms , ** options )
408+ await obj .upload_content (tar_buffer )
409+ await obj .complete ()
410+ return obj
411+
364412 async def upload_from_text (
365413 self ,
366414 text : str ,
0 commit comments