@@ -275,32 +275,49 @@ def external(self):
275275
276276 @property
277277 def object_storage (self ) -> StorageBackend | None :
278- """Get the object storage backend for this table."""
279- if not hasattr (self , "_object_storage" ):
278+ """Get the default object storage backend for this table."""
279+ return self .get_object_storage ()
280+
281+ def get_object_storage (self , store_name : str | None = None ) -> StorageBackend | None :
282+ """
283+ Get the object storage backend for a specific store.
284+
285+ Args:
286+ store_name: Name of the store (None for default store)
287+
288+ Returns:
289+ StorageBackend instance or None if not configured
290+ """
291+ cache_key = f"_object_storage_{ store_name or 'default' } "
292+ if not hasattr (self , cache_key ):
280293 try :
281- spec = config .get_object_storage_spec ( )
282- self . _object_storage = StorageBackend (spec )
294+ spec = config .get_object_store_spec ( store_name )
295+ backend = StorageBackend (spec )
283296 # Verify/create store metadata on first use
284- verify_or_create_store_metadata (self ._object_storage , spec )
297+ verify_or_create_store_metadata (backend , spec )
298+ setattr (self , cache_key , backend )
285299 except DataJointError :
286- self . _object_storage = None
287- return self . _object_storage
300+ setattr ( self , cache_key , None )
301+ return getattr ( self , cache_key )
288302
289- def _process_object_value (self , name : str , value , row : dict ) -> str :
303+ def _process_object_value (self , name : str , value , row : dict , store_name : str | None = None ) -> str :
290304 """
291305 Process an object attribute value for insert.
292306
293307 Args:
294308 name: Attribute name
295309 value: Input value (file path, folder path, or (ext, stream) tuple)
296310 row: The full row dict (needed for primary key values)
311+ store_name: Name of the object store (None for default store)
297312
298313 Returns:
299314 JSON string for database storage
300315 """
301- if self .object_storage is None :
316+ backend = self .get_object_storage (store_name )
317+ if backend is None :
318+ store_desc = f"'{ store_name } '" if store_name else "default"
302319 raise DataJointError (
303- "Object storage is not configured. Set object_storage settings in datajoint.json "
320+ f "Object storage ( { store_desc } ) is not configured. Set object_storage settings in datajoint.json "
304321 "or DJ_OBJECT_STORAGE_* environment variables."
305322 )
306323
@@ -339,7 +356,7 @@ def _process_object_value(self, name: str, value, row: dict) -> str:
339356 )
340357
341358 # Get storage spec for path building
342- spec = config .get_object_storage_spec ( )
359+ spec = config .get_object_store_spec ( store_name )
343360 partition_pattern = spec .get ("partition_pattern" )
344361 token_length = spec .get ("token_length" , 8 )
345362 location = spec .get ("location" , "" )
@@ -362,24 +379,33 @@ def _process_object_value(self, name: str, value, row: dict) -> str:
362379 manifest = None
363380 if source_path :
364381 if is_dir :
365- manifest = self . object_storage .put_folder (source_path , full_storage_path )
382+ manifest = backend .put_folder (source_path , full_storage_path )
366383 size = manifest ["total_size" ]
367384 else :
368- self . object_storage .put_file (source_path , full_storage_path )
385+ backend .put_file (source_path , full_storage_path )
369386 elif stream :
370- self .object_storage .put_buffer (content , full_storage_path )
387+ backend .put_buffer (content , full_storage_path )
388+
389+ # Build full URL for the object
390+ url = self ._build_object_url (spec , full_storage_path )
371391
372392 # Build JSON metadata
373393 timestamp = datetime .now (timezone .utc ).isoformat ()
374394 metadata = {
375- "path" : relative_path ,
395+ "path" : full_storage_path ,
376396 "size" : size ,
377397 "hash" : None , # Hash is optional, not computed by default
378398 "ext" : ext ,
379399 "is_dir" : is_dir ,
380400 "timestamp" : timestamp ,
381401 }
382402
403+ # Add URL and store name
404+ if url :
405+ metadata ["url" ] = url
406+ if store_name :
407+ metadata ["store" ] = store_name
408+
383409 # Add mime_type for files
384410 if not is_dir and ext :
385411 mime_type , _ = mimetypes .guess_type (f"file{ ext } " )
@@ -392,6 +418,34 @@ def _process_object_value(self, name: str, value, row: dict) -> str:
392418
393419 return json .dumps (metadata )
394420
421+ def _build_object_url (self , spec : dict , path : str ) -> str | None :
422+ """
423+ Build a full URL for an object based on the storage spec.
424+
425+ Args:
426+ spec: Storage configuration dict
427+ path: Path within the storage
428+
429+ Returns:
430+ Full URL string or None for local storage
431+ """
432+ protocol = spec .get ("protocol" , "" )
433+ if protocol == "s3" :
434+ bucket = spec .get ("bucket" , "" )
435+ return f"s3://{ bucket } /{ path } "
436+ elif protocol == "gcs" :
437+ bucket = spec .get ("bucket" , "" )
438+ return f"gs://{ bucket } /{ path } "
439+ elif protocol == "azure" :
440+ container = spec .get ("container" , "" )
441+ return f"az://{ container } /{ path } "
442+ elif protocol == "file" :
443+ # For local storage, return file:// URL
444+ location = spec .get ("location" , "" )
445+ full_path = f"{ location } /{ path } " if location else path
446+ return f"file://{ full_path } "
447+ return None
448+
395449 def update1 (self , row ):
396450 """
397451 ``update1`` updates one existing entry in the table.
@@ -912,7 +966,7 @@ def __make_placeholder(self, name, value, ignore_extra_fields=False, row=None):
912966 raise DataJointError (
913967 f"Object attribute { name } requires full row context for insert. " "This is an internal error."
914968 )
915- value = self ._process_object_value (name , value , row )
969+ value = self ._process_object_value (name , value , row , store_name = attr . store )
916970 elif attr .numeric :
917971 value = str (int (value ) if isinstance (value , bool ) else value )
918972 elif attr .json :
0 commit comments