use std::ops::RangeInclusive;
use std::sync::Arc;
use arrow_array::{ArrayRef, BinaryArray, StringArray, StructArray, UInt64Array};
use arrow_schema::{DataType, Field, Fields};
use spiral_contract::safelong::SafeUnsignedLong;
use uuid::Uuid;
pub use super::api::{Blob, BlobPart, BlobStat, BlobUpload};
use super::error::ScandalError;
pub const DEFAULT_UPLOAD_PART_BYTES: u64 = 64 << 20;
pub const UPLOAD_PART_BYTES: RangeInclusive<u64> = (8 << 20)..=(1 << 30);
pub const MAX_UPLOAD_PARTS: u32 = 10_000;
const BLOB_METADATA: &str = r#"{"eq":"identity","order":"none"}"#;
impl Blob {
pub fn new(
table_id: Uuid,
object_id: Uuid,
size: u64,
digest: [u8; 32],
media_type: impl Into<String>,
) -> Self {
Self {
table_id,
object_id,
size: SafeUnsignedLong(size),
digest: hex_digest(&digest),
media_type: media_type.into(),
attempt: None,
}
}
pub fn digest_bytes(&self) -> Result<[u8; 32], ScandalError> {
parse_digest(&self.digest)
}
pub fn key(&self) -> String {
object_key(self.attempt, self.object_id)
}
pub fn uri(&self) -> String {
format!("spiral://{}/{}", self.table_id, self.key())
}
pub fn field(name: &str) -> Field {
let fields: Fields = vec![
Field::new("bytes", DataType::Binary, true),
Field::new("uri", DataType::Utf8, true),
Field::new("offset", DataType::UInt64, true),
Field::new("size", DataType::UInt64, true),
Field::new("mime_type", DataType::Utf8, true),
Field::new("version", DataType::Utf8, true),
Field::new("etag", DataType::Utf8, true),
Field::new("digest", DataType::Binary, true),
]
.into();
Field::new(name, DataType::Struct(fields), false).with_metadata(
[
("ARROW:extension:name".into(), "spql.Blob".into()),
("ARROW:extension:metadata".into(), BLOB_METADATA.into()),
]
.into(),
)
}
pub fn array(blobs: &[Self]) -> Result<ArrayRef, ScandalError> {
let n = blobs.len();
let uris: Vec<_> = blobs.iter().map(Self::uri).collect();
let digests = blobs
.iter()
.map(|blob| {
let mut digest = vec![0x1e, 32];
digest.extend_from_slice(&blob.digest_bytes()?);
Ok(digest)
})
.collect::<Result<Vec<_>, ScandalError>>()?;
let columns: Vec<ArrayRef> = vec![
Arc::new(BinaryArray::from_iter((0..n).map(|_| None::<&[u8]>))),
Arc::new(StringArray::from_iter_values(&uris)),
Arc::new(UInt64Array::from(vec![0; n])),
Arc::new(UInt64Array::from_iter_values(
blobs.iter().map(|b| b.size.0),
)),
Arc::new(StringArray::from_iter_values(
blobs.iter().map(|b| b.media_type.as_str()),
)),
Arc::new(StringArray::from(vec![None::<&str>; n])),
Arc::new(StringArray::from(vec![None::<&str>; n])),
Arc::new(BinaryArray::from_iter_values(
digests.iter().map(Vec::as_slice),
)),
];
let DataType::Struct(fields) = Self::field("blob").data_type().clone() else {
unreachable!()
};
Ok(Arc::new(StructArray::new(fields, columns, None)))
}
}
impl BlobUpload {
pub fn key(&self) -> String {
object_key(Some(self.attempt), self.object_id)
}
pub fn blob(&self, size: u64, digest: [u8; 32]) -> Blob {
Blob {
attempt: Some(self.attempt),
..Blob::new(
self.table_id,
self.object_id,
size,
digest,
self.media_type.clone(),
)
}
}
}
fn object_key(attempt: Option<Uuid>, object_id: Uuid) -> String {
match attempt {
Some(attempt) => format!("attempts/{attempt}/blobs/{object_id}"),
None => format!("blobs/{object_id}"),
}
}
pub fn hex_digest(digest: &[u8; 32]) -> String {
digest.iter().map(|byte| format!("{byte:02x}")).collect()
}
pub fn parse_digest(value: &str) -> Result<[u8; 32], ScandalError> {
let invalid = || ScandalError::InvalidArgument {
message: "a blob digest is 64 hexadecimal digits".to_owned(),
};
let (pairs, []) = value.as_bytes().as_chunks::<2>() else {
return Err(invalid());
};
if pairs.len() != 32 {
return Err(invalid());
}
let mut digest = [0; 32];
for (byte, pair) in digest.iter_mut().zip(pairs) {
let pair = str::from_utf8(pair).map_err(|_| invalid())?;
*byte = u8::from_str_radix(pair, 16).map_err(|_| invalid())?;
}
Ok(digest)
}