Skip to main content

canvas_lms_api/resources/
content_migration.rs

1use crate::{error::Result, http::Requester, pagination::PageStream};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6/// A Canvas content migration job.
7#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct ContentMigration {
9    pub id: u64,
10    pub migration_type: Option<String>,
11    pub migration_type_title: Option<String>,
12    pub course_id: Option<u64>,
13    pub account_id: Option<u64>,
14    pub group_id: Option<u64>,
15    pub user_id: Option<u64>,
16    pub workflow_state: Option<String>,
17    pub started_at: Option<DateTime<Utc>>,
18    pub finished_at: Option<DateTime<Utc>>,
19    pub pre_attachment: Option<serde_json::Value>,
20    pub progress_url: Option<String>,
21    pub migration_issues_url: Option<String>,
22    pub migration_issues_count: Option<u64>,
23    pub attachment: Option<serde_json::Value>,
24    pub settings: Option<serde_json::Value>,
25
26    #[serde(skip)]
27    pub(crate) requester: Option<Arc<Requester>>,
28}
29
30impl ContentMigration {
31    fn req(&self) -> &Arc<Requester> {
32        self.requester.as_ref().expect("requester not initialized")
33    }
34
35    fn parent_type(&self) -> &'static str {
36        if self.course_id.is_some() {
37            "course"
38        } else if self.group_id.is_some() {
39            "group"
40        } else if self.account_id.is_some() {
41            "account"
42        } else {
43            "user"
44        }
45    }
46
47    fn parent_id(&self) -> u64 {
48        self.course_id
49            .or(self.group_id)
50            .or(self.account_id)
51            .or(self.user_id)
52            .expect("ContentMigration missing parent id")
53    }
54
55    /// Fetch a single migration issue.
56    ///
57    /// # Canvas API
58    /// `GET /api/v1/courses/:course_id/content_migrations/:id/migration_issues/:issue_id`
59    pub async fn get_migration_issue(&self, issue_id: u64) -> Result<MigrationIssue> {
60        let mut issue: MigrationIssue = self
61            .req()
62            .get(
63                &format!(
64                    "{}s/{}/content_migrations/{}/migration_issues/{issue_id}",
65                    self.parent_type(),
66                    self.parent_id(),
67                    self.id
68                ),
69                &[],
70            )
71            .await?;
72        issue.requester = self.requester.clone();
73        Ok(issue)
74    }
75
76    /// Stream all migration issues for this migration.
77    ///
78    /// # Canvas API
79    /// `GET /api/v1/courses/:course_id/content_migrations/:id/migration_issues`
80    pub fn get_migration_issues(&self) -> PageStream<MigrationIssue> {
81        let parent_type = self.parent_type();
82        let parent_id = self.parent_id();
83        let migration_id = self.id;
84        PageStream::new_with_injector(
85            Arc::clone(self.req()),
86            &format!(
87                "{parent_type}s/{parent_id}/content_migrations/{migration_id}/migration_issues"
88            ),
89            vec![],
90            |mut issue: MigrationIssue, req| {
91                issue.requester = Some(Arc::clone(&req));
92                issue
93            },
94        )
95    }
96
97    /// Update this content migration.
98    ///
99    /// # Canvas API
100    /// `PUT /api/v1/courses/:course_id/content_migrations/:id`
101    pub async fn update(&self, params: &[(String, String)]) -> Result<ContentMigration> {
102        let mut migration: ContentMigration = self
103            .req()
104            .put(
105                &format!(
106                    "{}s/{}/content_migrations/{}",
107                    self.parent_type(),
108                    self.parent_id(),
109                    self.id
110                ),
111                params,
112            )
113            .await?;
114        migration.requester = self.requester.clone();
115        Ok(migration)
116    }
117}
118
119/// An issue encountered during a content migration.
120#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct MigrationIssue {
122    pub id: u64,
123    pub content_migration_url: Option<String>,
124    pub description: Option<String>,
125    pub workflow_state: Option<String>,
126    pub fix_issue_html_url: Option<String>,
127    pub issue_type: Option<String>,
128    pub error_report_html_url: Option<String>,
129    pub error_message: Option<String>,
130    pub created_at: Option<DateTime<Utc>>,
131    pub updated_at: Option<DateTime<Utc>>,
132
133    #[serde(skip)]
134    pub(crate) requester: Option<Arc<Requester>>,
135}
136
137impl MigrationIssue {
138    fn req(&self) -> &Arc<Requester> {
139        self.requester.as_ref().expect("requester not initialized")
140    }
141
142    /// Update the workflow state of this migration issue.
143    ///
144    /// `workflow_state` should be `"active"` or `"resolved"`.
145    ///
146    /// # Canvas API
147    /// `PUT /api/v1/.../content_migrations/:migration_id/migration_issues/:id`
148    pub async fn update(&self, workflow_state: &str) -> Result<MigrationIssue> {
149        let migration_url = self
150            .content_migration_url
151            .as_deref()
152            .expect("MigrationIssue missing content_migration_url");
153        let params = vec![("workflow_state".to_string(), workflow_state.to_string())];
154        // Canvas returns content_migration_url as "/api/v1/..." — strip that prefix
155        // so Requester can join the relative path to base_url without doubling it.
156        let raw = format!("{}/migration_issues/{}", migration_url, self.id);
157        let endpoint = raw.trim_start_matches('/');
158        let endpoint = endpoint.strip_prefix("api/v1/").unwrap_or(endpoint);
159        let mut issue: MigrationIssue = self.req().put(endpoint, &params).await?;
160        issue.requester = self.requester.clone();
161        Ok(issue)
162    }
163}
164
165/// Metadata about an available content migration type.
166#[derive(Debug, Clone, Deserialize, Serialize)]
167pub struct Migrator {
168    pub r#type: Option<String>,
169    pub requires_file_upload: Option<bool>,
170    pub name: Option<String>,
171    pub links: Option<serde_json::Value>,
172}