use bytes::Bytes;
use futures::stream::{self, StreamExt, TryStreamExt};
use proton_sdk::crypto::ContentKey;
use proton_sdk::error::{ProtonError, Result};
use proton_sdk::ids::NodeUid;
use sha2::{Digest, Sha256};
use tokio::sync::{Mutex, OwnedSemaphorePermit, RwLock};
use crate::client::ProtonDriveClient;
use crate::dtos::BlockDto;
pub(crate) const MAX_CONCURRENT_BLOCK_DOWNLOADS: usize = 10;
pub(crate) async fn decrypt_block_blocking(
content_key: ContentKey,
ciphertext: Bytes,
) -> Result<Vec<u8>> {
join_decrypt(tokio::task::spawn_blocking(move || {
content_key.decrypt_block(&ciphertext)
}))
.await
}
pub(crate) async fn digest_and_decrypt_block_blocking(
content_key: ContentKey,
ciphertext: Bytes,
) -> Result<(Vec<u8>, Vec<u8>)> {
let handle = tokio::task::spawn_blocking(move || {
let digest = Sha256::digest(&ciphertext).to_vec();
content_key
.decrypt_block(&ciphertext)
.map(|pt| (digest, pt))
});
join_decrypt(handle).await
}
async fn join_decrypt<T>(
handle: tokio::task::JoinHandle<std::result::Result<T, proton_sdk::crypto::CryptoError>>,
) -> Result<T> {
handle
.await
.map_err(|e| ProtonError::invalid_operation(format!("block decrypt task failed: {e}")))?
.map_err(Into::into)
}
struct BlockTable {
blocks: Vec<BlockDto>,
generation: u64,
}
pub struct RevisionReader {
client: ProtonDriveClient,
uid: NodeUid,
revision_id: String,
content_key: ContentKey,
blocks: RwLock<BlockTable>,
refresh: Mutex<()>,
block_sizes: Vec<u64>,
file_size: u64,
}
impl RevisionReader {
pub(crate) fn new(
client: ProtonDriveClient,
uid: NodeUid,
revision_id: String,
content_key: ContentKey,
blocks: Vec<BlockDto>,
block_sizes: Vec<u64>,
) -> Self {
let file_size = block_sizes.iter().sum();
Self {
client,
uid,
revision_id,
content_key,
blocks: RwLock::new(BlockTable {
blocks,
generation: 0,
}),
refresh: Mutex::new(()),
block_sizes,
file_size,
}
}
pub fn uid(&self) -> &NodeUid {
&self.uid
}
pub fn revision_id(&self) -> &str {
&self.revision_id
}
pub fn size(&self) -> u64 {
self.file_size
}
pub fn block_sizes(&self) -> &[u64] {
&self.block_sizes
}
pub async fn read_at(&self, offset: u64, length: u64) -> Result<Vec<u8>> {
if length == 0 || offset >= self.file_size {
return Ok(Vec::new());
}
let end = offset.saturating_add(length).min(self.file_size);
let mut wanted: Vec<(usize, u64)> = Vec::new();
let mut block_start: u64 = 0;
for (index, &block_size) in self.block_sizes.iter().enumerate() {
if block_start >= end {
break;
}
let block_end = block_start + block_size;
if block_end > offset {
wanted.push((index, block_start));
}
block_start = block_end;
}
let indices: Vec<usize> = wanted.iter().map(|&(index, _)| index).collect();
let mut blocks = stream::iter(indices.into_iter().map(|index| self.block_plaintext(index)))
.buffered(MAX_CONCURRENT_BLOCK_DOWNLOADS);
let mut out = Vec::with_capacity((end - offset) as usize);
let mut next = 0_usize;
while let Some((plaintext, _permit)) = blocks.try_next().await? {
let (_, block_start) = wanted[next];
next += 1;
let from = offset.saturating_sub(block_start) as usize;
let to = ((end - block_start) as usize).min(plaintext.len());
if from < to {
out.extend_from_slice(&plaintext[from..to]);
}
}
Ok(out)
}
async fn block_plaintext(&self, index: usize) -> Result<(Vec<u8>, OwnedSemaphorePermit)> {
let permit = self
.client
.block_slots()
.acquire_owned()
.await
.map_err(|e| ProtonError::invalid_operation(format!("block slots closed: {e}")))?;
let (url, token, generation) = self.block_location(index).await?;
let ciphertext = match self.client.http().get_storage_blob(&url, &token).await {
Ok(bytes) => bytes,
Err(e) if is_expired_block_url(&e) => {
self.refresh_blocks(generation).await?;
let (url, token, _) = self.block_location(index).await?;
self.client.http().get_storage_blob(&url, &token).await?
}
Err(e) => return Err(e),
};
let plaintext = decrypt_block_blocking(self.content_key.clone(), ciphertext).await?;
Ok((plaintext, permit))
}
async fn block_location(&self, index: usize) -> Result<(String, String, u64)> {
let table = self.blocks.read().await;
let block = table.blocks.get(index).ok_or_else(|| {
ProtonError::invalid_operation(format!(
"block {index} is missing from revision {}",
self.revision_id
))
})?;
Ok((
block.bare_url.clone(),
block.token.clone(),
table.generation,
))
}
async fn refresh_blocks(&self, seen_generation: u64) -> Result<()> {
let _guard = self.refresh.lock().await;
if self.blocks.read().await.generation != seen_generation {
return Ok(());
}
let (_, blocks) = self
.client
.fetch_revision_blocks(&self.uid.volume_id, &self.uid.link_id, &self.revision_id)
.await?;
if blocks.len() != self.block_sizes.len() {
return Err(ProtonError::invalid_operation(format!(
"revision {} changed block count while open ({} -> {})",
self.revision_id,
self.block_sizes.len(),
blocks.len()
)));
}
let mut table = self.blocks.write().await;
table.blocks = blocks;
table.generation += 1;
Ok(())
}
}
fn is_expired_block_url(error: &ProtonError) -> bool {
matches!(
error,
ProtonError::Api(e) if matches!(e.http_status, 401 | 403 | 404)
)
}