opencloudmesh 0.2.1

Implementation of the OpenCloudMesh protocol
Documentation
// SPDX-FileCopyrightText: 2026 Matthias Kraus <info@opengeomesh.org>
//
// SPDX-License-Identifier: LGPL-3.0-or-later

use http::{
    StatusCode, Uri,
    uri::{Builder, InvalidUri},
};
use ocm_types::{
    common::OcmAddress,
    discovery::{Capability, Discovery},
    error::{Error, ValidationError},
    share::{NewShare, Protocol, ShareCreationResponse},
};

use crate::{
    common::HttpClient,
    drivers::{protocols::MultiProtocol, resources::Resource, shares::ShareRepoError},
};

use super::SHARE_ENDPOINT;

/// Send a share to some remote recipient.
///
/// Discovers the receiving server based on the FQDN of the recipient given in "share_with",
/// checks preconditions for sending the share and notifies the receiving server about the share.
///
/// Tries to convert webdav shares to legacy protocal format for receiving OCM servers with version
/// < 1.1.0
///
/// On success the sent share is returned.
pub async fn send_share(
    client: &impl HttpClient,
    provider_id: String,
    sending_party: OcmAddress,
    receiving_server: &Discovery,
    receiving_party: OcmAddress,
    resource: &impl Resource,
    protocol: MultiProtocol,
) -> Result<(NewShare, ShareCreationResponse), SendShareError> {
    // * if the Receiving Server is trusted
    // TODO check allow list / deny list

    // The next step is for the Sending Server to additionally discover:

    // * if the Receiving Server supports OCM
    // * if so, which version and with which optional functionality
    // * at which URL
    // * the public key the Receiving Server will use for HTTP Signatures (if any)
    let receiving_server_endpoint: Uri = receiving_server
        .end_point
        .as_str()
        .try_into()
        .map_err(SendShareError::InvalidOcmEndpoint)?;

    // TODO decide if this check is actually necessary or complicates webfinger redirect setups
    if !receiving_party.get_server_url().ends_with(
        receiving_server_endpoint
            .authority()
            .map(|host| host.as_str())
            .unwrap_or(""),
    ) {
        Err(SendShareError::InvalidShareWith(format!(
            "FQDN of shareWith must match receiving server: {} <-> {}",
            receiving_server_endpoint
                .authority()
                .map(|host| host.as_str())
                .unwrap_or(""),
            receiving_party
        )))?
    }

    if !receiving_server.enabled {
        Err(SendShareError::RecievingServerNotEnabled)?
    };

    // Support for "multi" protocol was introduced in OpenCloudMesh v1.1. Translate "multi webdav"
    // shares to "legacy webdav" shares if "protocol-object" is not supported bei the server.
    let protocol = try_convert_to_legacy_share(protocol, receiving_server)?;

    let new_share = NewShare {
        share_with: receiving_party,
        name: resource.name().to_string(),
        description: None,
        provider_id,
        // FIXME owner should be provided by resource
        owner: sending_party.clone(),
        sender: sending_party,
        // TODO support for display names
        owner_display_name: None,
        sender_display_name: None,
        share_type: ocm_types::common::ShareType::User,
        resource_type: resource.resource_type().to_owned(),
        expiration: None,
        protocol,
    };

    // To create a Share, the Sending Server SHOULD make a HTTP POST request

    // * to the `/shares` path in the Receiving Server's OCM API
    // * using `application/json` as the `Content-Type` HTTP request header
    // * its request body containing a JSON document representing an object with the fields as described below
    // FIXME check if http fallback is allowed or leave this to the HTTP Client??
    // * using TLS
    // * using [httpsig](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12)
    let scheme = receiving_server_endpoint
        .scheme_str()
        .unwrap_or("https")
        .to_owned();
    let path = receiving_server_endpoint
        .path()
        .strip_suffix("/")
        .unwrap_or(receiving_server_endpoint.path())
        .to_string();
    let resp = client
        .post(
            &Builder::from(receiving_server_endpoint)
                .scheme(scheme.as_str())
                .path_and_query(path + SHARE_ENDPOINT)
                .build()
                .unwrap(),
            serde_json::to_value(&new_share).unwrap(),
        )
        .await
        .map_err(SendShareError::RequestError)?;

    let share_creation_response: ShareCreationResponse =
        serde_json::from_str(&resp).map_err(|e| SendShareError::RequestError(e.to_string()))?;

    Ok((new_share, share_creation_response))
}

#[derive(Debug)]
/// Errors when sending a share.
pub enum SendShareError {
    InvalidOcmEndpoint(InvalidUri),
    RecievingServerNotEnabled,
    VersionCompatiblity(String),
    RequestError(String),
    StoringShareFailed(ShareRepoError),
    InvalidShareWith(String),
    InvalidSender(),
}

impl SendShareError {
    pub fn status_code(&self) -> http::StatusCode {
        match self {
            SendShareError::RecievingServerNotEnabled => StatusCode::BAD_GATEWAY,
            SendShareError::VersionCompatiblity(_) => StatusCode::BAD_GATEWAY,
            SendShareError::RequestError(_) => StatusCode::BAD_GATEWAY,
            SendShareError::StoringShareFailed(_) => StatusCode::INTERNAL_SERVER_ERROR,
            SendShareError::InvalidShareWith(_) => StatusCode::NOT_ACCEPTABLE,
            SendShareError::InvalidSender() => StatusCode::NOT_ACCEPTABLE,
            SendShareError::InvalidOcmEndpoint(_) => StatusCode::NOT_ACCEPTABLE,
        }
    }
}

impl From<SendShareError> for ocm_types::error::Error {
    fn from(value: SendShareError) -> Self {
        match value {
            SendShareError::RecievingServerNotEnabled => Error {
                message: "DISABLED_OCM_SERVER".to_string(),
                validation_errors: vec![],
            },
            SendShareError::VersionCompatiblity(v) => Error {
                message: "UNSUPPORTED_OCM_VERSION".to_string(),
                validation_errors: vec![ValidationError {
                    name: Some("UNSUPPORTED_OCM_VERSION".to_string()),
                    message: Some(v),
                }],
            },
            SendShareError::RequestError(e) => Error {
                message: "REQUEST_ERROR".to_string(),
                validation_errors: vec![ValidationError {
                    name: Some("OCM Server rejected request".to_string()),
                    message: Some(e.to_string()),
                }],
            },
            SendShareError::StoringShareFailed(_share_repo_error) => Error {
                message: "STORAGE_ERROR".to_string(),
                validation_errors: vec![ValidationError {
                    name: Some("Failed to store sent share".to_string()),
                    // TODO include share_repo_error
                    message: None,
                }],
            },
            SendShareError::InvalidSender() => Error {
                message: "INVALID_SENDER".to_string(),
                validation_errors: vec![],
            },
            SendShareError::InvalidShareWith(e) => Error {
                message: "INVALID_RECIPIENT".to_string(),
                validation_errors: vec![ValidationError {
                    name: None,
                    message: Some(e.to_string()),
                }],
            },
            SendShareError::InvalidOcmEndpoint(e) => Error {
                message: "INVALID_OCM_ENDPOINT".to_string(),
                validation_errors: vec![ValidationError {
                    name: Some("INVALID_OCM_ENDPOINT".to_string()),
                    message: Some(e.to_string()),
                }],
            },
        }
    }
}

impl From<ShareRepoError> for SendShareError {
    fn from(value: ShareRepoError) -> Self {
        Self::StoringShareFailed(value)
    }
}

#[allow(deprecated)] // using deprecated api to support legacy protocol versions
fn try_convert_to_legacy_share(
    protocol: MultiProtocol,
    recieving_server: &ocm_types::discovery::Discovery,
) -> Result<Protocol, SendShareError> {
    if recieving_server
        .capabilities
        .as_ref()
        .is_none_or(|c| !c.contains(&Capability::ProtocolObject))
    {
        if protocol.webdav.is_some()
            && (protocol.webapp.is_some()
                || protocol.ssh.is_some()
                || !protocol.additional_protocols.is_empty())
        {
            Err(SendShareError::VersionCompatiblity(
                "OCM v1.0 does not support multi protocol".to_string(),
            ))
        } else if let Some(webdav) = protocol.webdav.clone() {
            Ok(Protocol{
            name: "webdav".to_string(),
            options: Some(
                webdav
                    .try_into()
                    .map_err(|e| SendShareError::VersionCompatiblity(
                        format!("failed to convert multi webdav share to legacy webdav share to support v1.0 OCM recipient: {e}"))
                    )?,
            ),
                ..Default::default()

            })
        } else {
            panic!("Got empty protocol object: {protocol:?}");
        }
    } else {
        Ok(protocol.into())
    }
}