diff --git a/services/blob/src/database/errors.rs b/services/blob/src/database/errors.rs
--- a/services/blob/src/database/errors.rs
+++ b/services/blob/src/database/errors.rs
@@ -14,6 +14,7 @@
   #[display(...)]
   Attribute(DBItemError),
   #[display(...)]
+  #[from(ignore)]
   Blob(BlobDBError),
   #[display(...)]
   ItemAlreadyExists,
@@ -37,3 +38,9 @@
 }
 
 impl std::error::Error for BlobDBError {}
+
+impl From<S3PathError> for Error {
+  fn from(err: S3PathError) -> Self {
+    Error::Blob(BlobDBError::InvalidS3Path(err))
+  }
+}
diff --git a/services/blob/src/s3.rs b/services/blob/src/s3.rs
--- a/services/blob/src/s3.rs
+++ b/services/blob/src/s3.rs
@@ -8,7 +8,7 @@
   ops::{Bound, RangeBounds},
   sync::Arc,
 };
-use tracing::error;
+use tracing::{debug, error, trace};
 
 #[derive(
   Debug, derive_more::Display, derive_more::From, derive_more::Error,
@@ -85,6 +85,19 @@
   }
 }
 
+impl From<&S3Path> for String {
+  fn from(s3_path: &S3Path) -> Self {
+    s3_path.to_full_path()
+  }
+}
+
+impl TryFrom<&str> for S3Path {
+  type Error = S3PathError;
+  fn try_from(full_path: &str) -> Result<Self, Self::Error> {
+    Self::from_full_path(full_path)
+  }
+}
+
 #[derive(Clone)]
 pub struct S3Client {
   client: Arc<aws_sdk_s3::Client>,
@@ -220,6 +233,7 @@
       error!("Upload ID expected to be present");
       Error::MissingUploadID
     })?;
+    debug!("Started multipart upload session with ID: {}", upload_id);
 
     Ok(MultiPartUploadSession {
       client: client.clone(),
@@ -253,6 +267,12 @@
       .e_tag(upload_result.e_tag.unwrap_or_default())
       .part_number(part_number)
       .build();
+    trace!(
+      upload_id = self.upload_id,
+      e_tag = completed_part.e_tag.as_deref().unwrap_or("N/A"),
+      "Uploaded part {}.",
+      part_number
+    );
     self.upload_parts.push(completed_part);
     Ok(())
   }
@@ -281,6 +301,7 @@
         Error::AwsSdk(e.into())
       })?;
 
+    debug!(upload_id = self.upload_id, "Multipart upload complete");
     Ok(())
   }
 }