Skip to content

Commit

Permalink
Add API to create series in Tobira
Browse files Browse the repository at this point in the history
This adds:
- a function to send a `create` request to Opencast
- the graphQL endpoint to call that function from the frontend

The function needs to be given ACL and a title as parameters,
and can be given a description. It then constructs the
metadata json that is expected by the Opencast endpoint and
appends this and the serialized ACL information.

The Opencast endpoint returns a new Opencast ID for the
series, which in conjunction with the title and ACL  can be
used to "prefigure" the series in Tobira.
Doing so allows a new series entry to be shown in the
"My series" table without having to wait for sync.
  • Loading branch information
owi92 committed Jan 28, 2025
1 parent 3610bc7 commit c338bd7
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 8 deletions.
57 changes: 50 additions & 7 deletions backend/src/api/model/series.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use crate::{
util::{impl_from_db, select},
},
model::{ExtraMetadata, Key},
prelude::*, sync::client::{AclInput, OcEndpoint},
prelude::*,
sync::client::{AclInput, OcEndpoint},
};

use self::acl::AclInputEntry;
Expand All @@ -30,6 +31,7 @@ use super::{
realm::{NewRealm, RealmSpecifier, RemoveMountedSeriesOutcome, UpdatedRealmName},
shared::{
load_writable_for_user,
AclForDB,
AssetMapping,
Connection,
LoadableAsset,
Expand Down Expand Up @@ -138,23 +140,64 @@ impl Series {
}
}

pub(crate) async fn create(series: NewSeries, context: &Context) -> ApiResult<Self> {
pub(crate) async fn create(
series: NewSeries,
context: &Context,
acl: Option<AclForDB>,
) -> ApiResult<Self> {
let (read_roles, write_roles) = match &acl {
Some(roles) => (Some(&roles.read_roles), Some(&roles.write_roles)),
None => (None, None),
};

let selection = Self::select();
let query = format!(
"insert into series (opencast_id, title, state, updated) \
values ($1, $2, 'waiting', '-infinity') \
"insert into series (opencast_id, title, state, updated, read_roles, write_roles) \
values ($1, $2, 'waiting', '-infinity', $3, $4) \
returning {selection}",
);
context.db(context.require_tobira_admin()?)
.query_one(&query, &[&series.opencast_id, &series.title])
.query_one(&query, &[&series.opencast_id, &series.title, &read_roles, &write_roles])
.await?
.pipe(|row| Self::from_row_start(&row))
.pipe(Ok)
}

pub(crate) async fn create_in_oc(
title: &str,
description: Option<&str>,
acl: Vec<AclInputEntry>,
context: &Context,
) -> ApiResult<Self> {
let response = context
.oc_client
.create_series(&acl, &title, description)
.await
.map_err(|e| {
error!("Failed to send series creation request: {}", e);
err::opencast_unavailable!("Failed to send series creation request")
})?;

let db_acl = Some(convert_acl_input(acl));

// If the request returned an Opencast identifier, the series was created successfully.
// The series is created in the database, so the user doesn't have to wait for sync to see
// the new series in the "My series" overview.
let series = Self::create(
NewSeries {
opencast_id: response.identifier,
title: title.to_owned(),
},
context,
db_acl,
).await?;

Ok(series)
}

pub(crate) async fn announce(series: NewSeries, context: &Context) -> ApiResult<Self> {
context.auth.required_trusted_external()?;
Self::create(series, context).await
Self::create(series, context, None).await
}

pub(crate) async fn add_mount_point(
Expand Down Expand Up @@ -272,7 +315,7 @@ impl Series {
}

// Create series
let series = Series::create(series, context).await?;
let series = Series::create(series, context, None).await?;

// Create realms
let target_realm = {
Expand Down
11 changes: 11 additions & 0 deletions backend/src/api/mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ impl Mutation {
Series::update_acl(id, acl, context).await
}

/// Sends an http request to Opencast to create a new series,
/// and stores the series in Tobira's DB.
async fn create_series(
title: String,
description: Option<String>,
acl: Vec<AclInputEntry>,
context: &Context,
) -> ApiResult<Series> {
Series::create_in_oc(&title, description.as_deref(), acl, context).await
}

/// Sets the order of all children of a specific realm.
///
/// `childIndices` must contain at least one element, i.e. do not call this
Expand Down
60 changes: 59 additions & 1 deletion backend/src/sync/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,59 @@ impl OcClient {
Ok(out.processing_state == "RUNNING")
}

pub async fn create_series(
&self,
acl: &[AclInputEntry],
title: &str,
description: Option<&str>,
) -> Result<CreateSeriesResponse> {
let access_policy: Vec<AclInput> = acl.iter()
.flat_map(|entry| entry.actions.iter()
.map(|action| AclInput {
allow: true,
action: action.clone(),
role: entry.role.clone(),
}))
.collect();

let metadata = serde_json::json!([
{
"label": "Opencast Series DublinCore",
"flavor": "dublincore/series",
"fields": [
{
"id": "title",
"value": title
},
{
"id": "description",
"value": description
},
]
}
]);

let params = Serializer::new(String::new())
.append_pair("acl", &serde_json::to_string(&access_policy).expect("Failed to serialize"))
.append_pair("metadata", &serde_json::to_string(&metadata).expect("Failed to serialize"))
.finish();

let req = self.authed_req_builder(&self.external_api_node, "/api/series")
.method(http::Method::POST)
.header(http::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(params.into())
.expect("failed to build request");

let uri = req.uri().clone();
let response = self.http_client.request(req).await
.with_context(|| format!("HTTP request failed (uri: '{uri}'"))?;

let (out, _) = self.deserialize_response::<CreateSeriesResponse>(response, &uri).await?;

Ok(out)
}


fn build_authed_req(&self, node: &HttpHost, path_and_query: &str) -> (Uri, Request<RequestBody>) {
let req = self.authed_req_builder(node, path_and_query)
.body(RequestBody::empty())
Expand Down Expand Up @@ -250,7 +303,7 @@ impl OcClient {
let body = download_body(body).await
.with_context(|| format!("failed to download body from '{uri}'"))?;

if parts.status != StatusCode::OK {
if parts.status != StatusCode::OK && parts.status != StatusCode::CREATED {
trace!("HTTP response: {:#?}", parts);
if parts.status == StatusCode::UNAUTHORIZED {
bail!(
Expand Down Expand Up @@ -287,6 +340,11 @@ pub(crate) struct AclInput {
pub role: String,
}

#[derive(Debug, Deserialize)]
pub(crate) struct CreateSeriesResponse {
pub identifier: String,
}

#[derive(Debug, Deserialize)]
pub struct EventStatus {
pub processing_state: String,
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,11 @@ type Mutation {
this means it might get overwritten again if the update in Opencast failed for some reason.
"""
updateSeriesAcl(id: ID!, acl: [AclInputEntry!]!): Series!
"""
Sends an http request to Opencast to create a new series,
and stores the series in Tobira's DB.
"""
createSeries(title: String!, description: String, acl: [AclInputEntry!]!): Series!
"""
Sets the order of all children of a specific realm.
Expand Down

0 comments on commit c338bd7

Please sign in to comment.