From c74cbe5065c4346f57b91cf043d1c52653d5c413 Mon Sep 17 00:00:00 2001 From: Joren Date: Sat, 20 Jun 2026 00:49:03 +0200 Subject: [PATCH] feat: add -a/--all flag to include concluded courses --- cmd/canvasarchiver/main.go | 11 ++++---- internal/canvas/client.go | 53 +++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/cmd/canvasarchiver/main.go b/cmd/canvasarchiver/main.go index d8237e0..d6d63e6 100644 --- a/cmd/canvasarchiver/main.go +++ b/cmd/canvasarchiver/main.go @@ -16,6 +16,7 @@ func main() { filesOnly := flag.Bool("fo", false, "Files only mode - download all files to a single directory without module structure") videosOnly := flag.Bool("vo", false, "Videos only mode - download only Panopto videos to a single directory") me := flag.Bool("me", false, "Download all enrolled courses") + all := flag.Bool("a", false, "Include concluded (old) courses when used with -me") moduleNumbers := flag.Bool("n", false, "Prefix modules with order numbers [1], [2], etc.") flag.Parse() @@ -29,7 +30,7 @@ func main() { } if *me { - canvasClient := canvas.NewClient(httpClient, accessToken, "", *filesOnly, *videosOnly, *moduleNumbers) + canvasClient := canvas.NewClient(httpClient, accessToken, "", *filesOnly, *videosOnly, *moduleNumbers, *all) courses, err := canvasClient.GetEnrolledCourses() if err != nil { fmt.Printf("Error fetching courses: %v\n", err) @@ -39,7 +40,7 @@ func main() { fmt.Printf("[+] Found %d enrolled courses\n", len(courses)) for _, course := range courses { fmt.Printf(" -> Downloading: %s (ID: %d)\n", course.Name, course.ID) - downloadCourse(httpClient, accessToken, fmt.Sprintf("%d", course.ID), *filesOnly, *videosOnly, *moduleNumbers) + downloadCourse(httpClient, accessToken, fmt.Sprintf("%d", course.ID), *filesOnly, *videosOnly, *moduleNumbers, *all) } return } @@ -48,11 +49,11 @@ func main() { fmt.Print("Enter Course ID: ") fmt.Scanln(&courseID) - downloadCourse(httpClient, accessToken, courseID, *filesOnly, *videosOnly, *moduleNumbers) + downloadCourse(httpClient, accessToken, courseID, *filesOnly, *videosOnly, *moduleNumbers, *all) } -func downloadCourse(httpClient *http.Client, accessToken, courseID string, filesOnly, videosOnly, moduleNumbers bool) { - canvasClient := canvas.NewClient(httpClient, accessToken, courseID, filesOnly, videosOnly, moduleNumbers) +func downloadCourse(httpClient *http.Client, accessToken, courseID string, filesOnly, videosOnly, moduleNumbers, all bool) { + canvasClient := canvas.NewClient(httpClient, accessToken, courseID, filesOnly, videosOnly, moduleNumbers, all) if err := canvasClient.GetCourseInfo(); err != nil { fmt.Printf("Error: %v\n", err) diff --git a/internal/canvas/client.go b/internal/canvas/client.go index a08281e..b958515 100644 --- a/internal/canvas/client.go +++ b/internal/canvas/client.go @@ -31,10 +31,11 @@ type Client struct { FilesOnly bool VideosOnly bool ModuleNumbers bool + All bool downloadedFiles map[string]bool } -func NewClient(httpClient *http.Client, accessToken, courseID string, filesOnly, videosOnly, moduleNumbers bool) *Client { +func NewClient(httpClient *http.Client, accessToken, courseID string, filesOnly, videosOnly, moduleNumbers, all bool) *Client { return &Client{ HTTPClient: httpClient, AccessToken: accessToken, @@ -42,6 +43,7 @@ func NewClient(httpClient *http.Client, accessToken, courseID string, filesOnly, FilesOnly: filesOnly, VideosOnly: videosOnly, ModuleNumbers: moduleNumbers, + All: all, downloadedFiles: make(map[string]bool), } } @@ -66,6 +68,11 @@ func (c *Client) GetCourseInfo() error { return nil } +type enrollmentResponse struct { + CourseID int `json:"course_id"` + EnrollmentState string `json:"enrollment_state"` +} + func (c *Client) GetEnrolledCourses() ([]models.Course, error) { req, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/courses?enrollment_state=active&per_page=100", config.BaseURL), nil) req.Header.Set("Authorization", "Bearer "+c.AccessToken) @@ -81,6 +88,50 @@ func (c *Client) GetEnrolledCourses() ([]models.Course, error) { var courses []models.Course json.NewDecoder(resp.Body).Decode(&courses) + + if c.All { + completed, err := c.getCompletedCourses() + if err != nil { + return nil, err + } + courses = append(courses, completed...) + } + + return courses, nil +} + +func (c *Client) getCompletedCourses() ([]models.Course, error) { + req, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/users/self/enrollments?state[]=completed&per_page=100", config.BaseURL), nil) + req.Header.Set("Authorization", "Bearer "+c.AccessToken) + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := checkAPIResponse(resp); err != nil { + return nil, err + } + + var enrollments []enrollmentResponse + json.NewDecoder(resp.Body).Decode(&enrollments) + + var courses []models.Course + for _, e := range enrollments { + courseReq, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/courses/%d", config.BaseURL, e.CourseID), nil) + courseReq.Header.Set("Authorization", "Bearer "+c.AccessToken) + courseResp, err := c.HTTPClient.Do(courseReq) + if err != nil { + continue + } + var course models.Course + json.NewDecoder(courseResp.Body).Decode(&course) + courseResp.Body.Close() + if course.ID != 0 { + courses = append(courses, course) + } + } + return courses, nil }