Files
CanvasArchiver/internal/canvas/client.go
2026-02-13 20:42:53 +01:00

248 lines
6.7 KiB
Go

package canvas
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"git.directme.in/Joren/CanvasArchiver/internal/config"
"git.directme.in/Joren/CanvasArchiver/internal/models"
"git.directme.in/Joren/CanvasArchiver/internal/panopto"
"git.directme.in/Joren/CanvasArchiver/internal/utils"
)
type Client struct {
HTTPClient *http.Client
AccessToken string
CourseID string
CourseName string
}
func NewClient(httpClient *http.Client, accessToken, courseID string) *Client {
return &Client{
HTTPClient: httpClient,
AccessToken: accessToken,
CourseID: courseID,
}
}
func (c *Client) GetCourseInfo() error {
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/courses/%s", config.BaseURL, c.CourseID), nil)
req.Header.Set("Authorization", "Bearer "+c.AccessToken)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var course models.Course
json.NewDecoder(resp.Body).Decode(&course)
c.CourseName = fmt.Sprintf("%s [%s]", course.Name, c.CourseID)
return nil
}
func (c *Client) DownloadCourseFiles(root string) {
fmt.Println("\n[*] Fetching regular course files...")
fReq, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/courses/%s/folders?per_page=100", config.BaseURL, c.CourseID), nil)
fReq.Header.Set("Authorization", "Bearer "+c.AccessToken)
fResp, err := c.HTTPClient.Do(fReq)
if err != nil {
fmt.Printf("[!] Error fetching folders: %v\n", err)
return
}
var folders []models.Folder
json.NewDecoder(fResp.Body).Decode(&folders)
fResp.Body.Close()
folderMap := make(map[int]string)
for _, f := range folders {
folderMap[f.ID] = f.FullName
}
fileReq, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/courses/%s/files?per_page=100", config.BaseURL, c.CourseID), nil)
fileReq.Header.Set("Authorization", "Bearer "+c.AccessToken)
fileResp, err := c.HTTPClient.Do(fileReq)
if err != nil {
fmt.Printf("[!] Error fetching files: %v\n", err)
return
}
var files []models.File
json.NewDecoder(fileResp.Body).Decode(&files)
fileResp.Body.Close()
fileCount := 0
for _, file := range files {
rawFolderPath := folderMap[file.FolderID]
safeFolderPath := utils.SanitizePath(rawFolderPath)
subDir := filepath.Join(root, "Course Files", safeFolderPath)
os.MkdirAll(subDir, 0o755)
path := filepath.Join(subDir, utils.Sanitize(file.DisplayName))
if _, err := os.Stat(path); err == nil {
continue
}
fmt.Printf(" -> Downloading: %s\n", file.DisplayName)
out, err := os.Create(path)
if err != nil {
fmt.Printf(" [!] Error creating file: %v\n", err)
continue
}
fData, err := c.HTTPClient.Get(file.URL)
if err != nil {
fmt.Printf(" [!] Error downloading file: %v\n", err)
out.Close()
continue
}
io.Copy(out, fData.Body)
fData.Body.Close()
out.Close()
fileCount++
}
fmt.Printf("[+] Downloaded %d regular files\n", fileCount)
}
func (c *Client) DownloadModules(courseRoot string) {
fmt.Println("\n[*] Fetching course modules...")
apiURL := fmt.Sprintf("%s/api/v1/courses/%s/modules?include[]=items&per_page=9999", config.BaseURL, c.CourseID)
req, _ := http.NewRequest("GET", apiURL, nil)
req.Header.Set("Authorization", "Bearer "+c.AccessToken)
resp, _ := c.HTTPClient.Do(req)
var modules []models.Module
json.NewDecoder(resp.Body).Decode(&modules)
resp.Body.Close()
for _, mod := range modules {
modBaseDir := filepath.Join(courseRoot, "Modules", utils.Sanitize(mod.Name))
os.MkdirAll(modBaseDir, 0o755)
fmt.Printf("\n[Module] %s\n", mod.Name)
subHeaderStack := []string{}
lastIndent := 0
for _, item := range mod.Items {
targetDir := modBaseDir
if len(subHeaderStack) > 0 {
targetDir = filepath.Join(modBaseDir, filepath.Join(subHeaderStack...))
}
os.MkdirAll(targetDir, 0o755)
switch item.Type {
case "SubHeader":
currentIndent := item.Indent
if currentIndent <= lastIndent && len(subHeaderStack) > 0 {
levelsToKeep := currentIndent
if levelsToKeep < 0 {
levelsToKeep = 0
}
if levelsToKeep < len(subHeaderStack) {
subHeaderStack = subHeaderStack[:levelsToKeep]
}
}
subHeaderStack = append(subHeaderStack, utils.Sanitize(item.Title))
lastIndent = currentIndent
indent := strings.Repeat(" ", len(subHeaderStack))
fmt.Printf("%s--- %s ---\n", indent, item.Title)
case "File":
c.downloadModuleFile(item, targetDir)
case "ExternalTool":
indent := strings.Repeat(" ", len(subHeaderStack)+1)
fmt.Printf("%s- Found video tool: %s\n", indent, item.Title)
panopto.DownloadVideo(c.HTTPClient, c.AccessToken, c.CourseID, targetDir, item.URL, item.Title)
case "ExternalUrl":
if strings.Contains(item.ExternalURL, "panopto.eu") {
indent := strings.Repeat(" ", len(subHeaderStack)+1)
fmt.Printf("%s- Found direct video link: %s\n", indent, item.Title)
panopto.DownloadVideo(c.HTTPClient, c.AccessToken, c.CourseID, targetDir, item.ExternalURL, item.Title)
}
case "Page":
c.searchPageForVideos(item, targetDir)
}
}
}
}
func (c *Client) downloadModuleFile(item models.ModuleItem, dir string) {
req, _ := http.NewRequest("GET", item.URL, nil)
req.Header.Set("Authorization", "Bearer "+c.AccessToken)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
var fileMeta struct {
URL string `json:"url"`
DisplayName string `json:"display_name"`
}
json.NewDecoder(resp.Body).Decode(&fileMeta)
if fileMeta.URL == "" {
return
}
ext := filepath.Ext(fileMeta.DisplayName)
origBase := strings.TrimSuffix(fileMeta.DisplayName, ext)
fileName := fileMeta.DisplayName
if !strings.EqualFold(origBase, item.Title) && item.Title != "" {
fileName = fmt.Sprintf("%s (%s)%s", origBase, item.Title, ext)
}
path := filepath.Join(dir, utils.Sanitize(fileName))
if _, err := os.Stat(path); err == nil {
return
}
fmt.Printf(" -> File: %s\n", fileName)
out, _ := os.Create(path)
fData, _ := c.HTTPClient.Get(fileMeta.URL)
io.Copy(out, fData.Body)
fData.Body.Close()
out.Close()
}
func (c *Client) searchPageForVideos(item models.ModuleItem, dir string) {
req, _ := http.NewRequest("GET", item.URL, nil)
req.Header.Set("Authorization", "Bearer "+c.AccessToken)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
var page struct {
Body string `json:"body"`
}
json.NewDecoder(resp.Body).Decode(&page)
re := regexp.MustCompile(`https?://vub\.cloud\.panopto\.eu/Panopto/Pages/[^"']+id=([a-f0-9-]{36})`)
matches := re.FindAllString(page.Body, -1)
if len(matches) > 0 {
fmt.Printf(" - Scanning Page '%s' for embedded videos...\n", item.Title)
for _, videoURL := range matches {
panopto.DownloadVideo(c.HTTPClient, c.AccessToken, c.CourseID, dir, videoURL, item.Title)
}
}
}