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