use tonic::Status;
use crate::proto::udb::core::storage::entity::v1 as storage_entity_pb;
use super::StorageServiceImpl;
use super::config::OBJECT_DELETE_ORPHANED;
pub(crate) enum PresignOutcome {
Url { url: String, expires_at: i64 },
Degraded,
Failed(String),
}
pub(crate) enum ObjectCheck {
Unchecked,
Absent,
Present { size: i64, etag: String },
}
impl StorageServiceImpl {
pub(crate) async fn delete_object_bytes(&self, project_id: &str, object_key: &str) {
let Some(runtime) = self.runtime.as_ref() else {
return;
};
if object_key.trim().is_empty() {
return;
}
let request_json = crate::runtime::core::setup_data::object_request_json(
"delete",
&self.object_bucket,
object_key,
"",
);
if let Err(err) = runtime
.delete_object_backend_target(&self.object_backend, None, project_id, &request_json)
.await
{
tracing::warn!(
error = %err,
object_key,
bucket = %self.object_bucket,
code = OBJECT_DELETE_ORPHANED,
"storage object byte delete failed; metadata soft-deleted (auditable), bytes orphaned"
);
}
}
pub(crate) async fn presign(
&self,
project_id: &str,
object_key: &str,
method: &str,
content_type: &str,
ttl_minutes: i32,
) -> PresignOutcome {
let Some(runtime) = self.runtime.as_ref() else {
return PresignOutcome::Degraded;
};
let ttl_secs = (ttl_minutes.max(1) as i64 * 60).min(7 * 24 * 3600) as i32;
match runtime
.presign_object_backend_target(
&self.object_backend,
project_id,
&self.object_bucket,
object_key,
method,
content_type,
ttl_secs,
)
.await
{
Ok((url, expires_at_unix)) => PresignOutcome::Url {
url,
expires_at: expires_at_unix,
},
Err(err) => {
tracing::warn!(error = %err, object_key, method, "storage presign failed; returning empty url");
if err.message().contains("feature is not enabled") {
PresignOutcome::Degraded
} else {
PresignOutcome::Failed(err.message().to_string())
}
}
}
}
pub(crate) async fn object_exists(
&self,
file: &storage_entity_pb::File,
) -> Result<ObjectCheck, Status> {
let Some(runtime) = self.runtime.as_ref() else {
return Ok(ObjectCheck::Unchecked);
};
if file.object_key.trim().is_empty() {
return Ok(ObjectCheck::Absent);
}
let backend = if file.backend.trim().is_empty() {
self.object_backend.as_str()
} else {
file.backend.as_str()
};
let bucket = if file.bucket.trim().is_empty() {
self.object_bucket.as_str()
} else {
file.bucket.as_str()
};
match runtime
.object_exists_backend_target(backend, &file.project_id, bucket, &file.object_key)
.await?
{
Some((size, etag)) => Ok(ObjectCheck::Present { size, etag }),
None => Ok(ObjectCheck::Absent),
}
}
}