tus-protocol 0.0.1

Rust implementation of the TUS resumable upload protocol
use crate::config::Config;
use crate::error::{Error, Result};
use crate::hooks::{HookExecutor, HookRequestInfo};
use crate::locking::Locker;
use crate::state::{StateStore, UploadState};
use crate::storage::Storage;

use super::{
    FinalUploadMaterializer, ensure_active, reconcile_state_offset, reconcile_stored_completion,
    stored_bytes_complete_upload,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PreparedUploadAccess {
    pub(crate) facts: UploadAccessFacts,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct UploadAccessFacts {
    pub(crate) offset: Option<u64>,
    pub(crate) length: Option<u64>,
    pub(crate) defer_length: bool,
}

impl UploadAccessFacts {
    fn for_regular_upload(state: &UploadState) -> Self {
        Self {
            offset: Some(state.offset()),
            length: state.length(),
            defer_length: state.length().is_none(),
        }
    }

    fn for_final_upload(state: &UploadState) -> Self {
        Self {
            offset: state.is_complete().then_some(state.offset()),
            length: state.length(),
            defer_length: false,
        }
    }
}

pub(crate) async fn prepare_upload_mutation_access<S, St, H>(
    storage: &S,
    state_store: &St,
    hooks: &H,
    request_info: &HookRequestInfo,
    state: &mut UploadState,
) -> Result<()>
where
    S: Storage + ?Sized,
    St: StateStore + ?Sized,
    H: HookExecutor + ?Sized,
{
    if state.is_final() {
        ensure_active(state)?;
        return Err(Error::FinalUploadModificationForbidden(
            state.id().to_string(),
        ));
    }

    prepare_regular_upload_access(storage, state_store, hooks, request_info, state).await?;

    Ok(())
}

pub(crate) async fn prepare_upload_observation_access<S, St, L, H>(
    storage: &S,
    state_store: &St,
    locker: &L,
    hooks: &H,
    config: &Config,
    request_info: &HookRequestInfo,
    state: &mut UploadState,
) -> Result<PreparedUploadAccess>
where
    S: Storage + ?Sized,
    St: StateStore + ?Sized,
    L: Locker + ?Sized,
    H: HookExecutor + ?Sized,
{
    prepare_upload_read_access(
        storage,
        state_store,
        locker,
        hooks,
        config,
        request_info,
        state,
    )
    .await
}

pub(crate) async fn prepare_upload_download_access<S, St, L, H>(
    storage: &S,
    state_store: &St,
    locker: &L,
    hooks: &H,
    config: &Config,
    request_info: &HookRequestInfo,
    state: &mut UploadState,
) -> Result<PreparedUploadAccess>
where
    S: Storage + ?Sized,
    St: StateStore + ?Sized,
    L: Locker + ?Sized,
    H: HookExecutor + ?Sized,
{
    let prepared = prepare_upload_read_access(
        storage,
        state_store,
        locker,
        hooks,
        config,
        request_info,
        state,
    )
    .await?;

    if !state.is_complete() {
        return Err(Error::IncompleteUpload(state.id().to_string()));
    }

    Ok(prepared)
}

pub(crate) async fn prepare_upload_reclamation_access<S>(
    storage: &S,
    state: &mut UploadState,
) -> Result<bool>
where
    S: Storage + ?Sized,
{
    // Reclamation runs without request or hook context, so it must not persist
    // a recovered completion here: doing so would finalize the upload while
    // permanently skipping its `PreFinish`/`PostFinish` hooks. Detecting the
    // completion is enough to know the upload is completed content rather than
    // an abandoned resource, and therefore not reclaimable. The next client
    // HEAD/PATCH/GET runs the finish gate and persists the completion.
    if stored_bytes_complete_upload(storage, state).await? {
        return Ok(false);
    }

    Ok(state.is_expired())
}

async fn prepare_upload_read_access<S, St, L, H>(
    storage: &S,
    state_store: &St,
    locker: &L,
    hooks: &H,
    config: &Config,
    request_info: &HookRequestInfo,
    state: &mut UploadState,
) -> Result<PreparedUploadAccess>
where
    S: Storage + ?Sized,
    St: StateStore + ?Sized,
    L: Locker + ?Sized,
    H: HookExecutor + ?Sized,
{
    if state.is_final() {
        let materializer =
            FinalUploadMaterializer::new(storage, state_store, locker, hooks, config, request_info);
        let prepared = materializer.prepare_read(state).await?;
        debug_assert!(
            prepared,
            "final upload preparation should handle final uploads"
        );
        ensure_active(state)?;

        return Ok(PreparedUploadAccess {
            facts: UploadAccessFacts::for_final_upload(state),
        });
    }

    prepare_regular_upload_access(storage, state_store, hooks, request_info, state).await?;

    Ok(PreparedUploadAccess {
        facts: UploadAccessFacts::for_regular_upload(state),
    })
}

async fn prepare_regular_upload_access<S, St, H>(
    storage: &S,
    state_store: &St,
    hooks: &H,
    request_info: &HookRequestInfo,
    state: &mut UploadState,
) -> Result<()>
where
    S: Storage + ?Sized,
    St: StateStore + ?Sized,
    H: HookExecutor + ?Sized,
{
    reconcile_stored_completion(storage, state_store, hooks, request_info, state).await?;
    ensure_active(state)?;
    reconcile_state_offset(storage, state_store, state).await?;

    Ok(())
}