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 std::collections::HashMap;
use std::collections::HashSet;

use http::Uri;
use ocm_types::share::SshProperties;
use ocm_types::{
    discovery::Discovery,
    error::{Error, ValidationError},
    share::{NewShare, WebAppProperties, WebDavProperties},
};
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;

use super::shares::ProviderId;

/// This trait represents protocols which can be used for data transfer between OCM Servers.
pub trait Protocol: Send + Sync + 'static {
    // TODO drop from trait from trait
    fn new(endpoint: Uri) -> Self
    where
        Self: Sized;

    /// Share a resource using this protocol.
    ///
    /// Returns the necessary metadata to access the shared resource at the Sending Server,
    /// including e.g. shared secrets and URLs for accessing the share. URLs can be relative to
    /// the protocol specific endpoint prefix advertised in discovery.
    fn share_resource(
        &self,
        // TODO pass resource as ref as well?
        provider_id: &ProviderId,
        permissions: &HashSet<Permission>
    ) -> Result<MultiProtocol, &'static str>;

    /// Protocol specific URL prefix to access resources shared by this OCM Server using this
    /// protocol.
    fn endpoint(&self) -> &Uri;

    /// Identifier of this protocol used in [Discovery] and in
    /// [shares](ocm_types::share::NewShare).
    ///
    /// Examples: `webdav`, `datatx` or `webapp`
    fn identifier(&self) -> &'static str;

    /// [Resource](super::resources::Resource) types which can be accessed using this protocol.
    ///
    /// Examples: `file`, `folder`, `calendar`, ...
    fn supported_resource_types(&self) -> &[&str];

    /// This translates URLs relative to URL prefixes to URLs relative to Sending Server root path.
    fn resolve_client_properties(
        &self,
        sending_server: &Discovery,
        new_share: &NewShare,
    ) -> Result<MultiProtocol, ProtocolError>;
}

#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub enum Permission {
    Read,
    Write
}


/// unifies legacy protocol formats. Should store resolved absolute protocol endpoints for received
/// shares.
#[derive(Debug, Clone, Default)]
pub struct MultiProtocol {
    pub webdav: Option<WebDavProperties>,
    pub webapp: Option<WebAppProperties>,
    pub ssh: Option<SshProperties>,
    pub additional_protocols: HashMap<String, Value>,
}

impl MultiProtocol {
    /// Combines multiple NormalizedProtocolProperties into one. Picks the first value on
    /// duplicates.
    pub fn merge(self, mut other: Self) -> Self {
        let Self {
            webdav,
            webapp,
            ssh,
            additional_protocols,
        } = self;
        other.additional_protocols.extend(additional_protocols);
        Self {
            webdav: webdav.or(other.webdav),
            webapp: webapp.or(other.webapp),
            ssh: ssh.or(other.ssh),
            additional_protocols: other.additional_protocols,
        }
    }

    pub fn is_empty(&self) -> bool {
        self.webdav.is_none()
            & self.webapp.is_none()
            & self.ssh.is_none()
            & self.additional_protocols.is_empty()
    }
}

impl From<MultiProtocol> for ocm_types::share::Protocol {
    fn from(value: MultiProtocol) -> Self {
        let MultiProtocol {
            webdav,
            webapp,
            ssh,
            additional_protocols,
        } = value;
        ocm_types::share::Protocol {
            name: "multi".to_string(),
            webdav,
            webapp,
            ssh,
            additional_protocols,
            ..Default::default()
        }
    }
}

/// extract protocol properties for a given protocol identifier like `webdav` or `webapp` from a
/// share.
// FIXME this is only usable for additonal or legacy protocol properties.
pub fn get_share_protocol<'a>(
    share: &'a NewShare,
    protocol: &'static str,
) -> Option<&'a serde_json::Map<String, serde_json::Value>> {
    // Allow reading legacy protocol options for backward compatibility
    #[allow(deprecated)]
    let protocol_properties = if share.protocol.name == "multi" {
        share
            .protocol
            .additional_protocols
            .get(protocol)
            .and_then(|p| p.as_object())
    } else if share.protocol.name == protocol
        && let Some(ref options) = share.protocol.options
    {
        Some(options)
    } else {
        None
    };
    protocol_properties
}

pub enum ProtocolError {
    /// Protocol not supported for this share
    NotSupported,
    InvalidProtocolProperties(String),
}

impl From<ProtocolError> for Error {
    fn from(value: ProtocolError) -> Self {
        match value {
            ProtocolError::NotSupported => Error {
                message: "SHARE_PROTOCOL_UNSUPPORTED".to_string(),
                validation_errors: vec![ValidationError {
                    name: None,
                    message: Some("Protocol not suppored for this share".to_string()),
                }],
            },
            ProtocolError::InvalidProtocolProperties(e) => Error {
                message: "INVALID_SHARE_PROTOCOL".to_string(),
                validation_errors: vec![ValidationError {
                    name: None,
                    message: Some(e),
                }],
            },
        }
    }
}