diff --git a/nextjs/package.json b/nextjs/package.json index 83f7694..089a0f9 100644 --- a/nextjs/package.json +++ b/nextjs/package.json @@ -20,6 +20,7 @@ "@types/ws": "^8.5.13", "chokidar": "^4.0.1", "dotenv": "^16.4.5", + "form-data": "^4.0.1", "jsdom": "^25.0.0", "next": "^15.0.2", "react": "^18", diff --git a/nextjs/pnpm-lock.yaml b/nextjs/pnpm-lock.yaml index 64a398e..02f5e81 100644 --- a/nextjs/pnpm-lock.yaml +++ b/nextjs/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 + form-data: + specifier: ^4.0.1 + version: 4.0.1 jsdom: specifier: ^25.0.0 version: 25.0.1 diff --git a/nextjs/src/services/canvas/files/canvasFileService.ts b/nextjs/src/services/canvas/files/canvasFileService.ts new file mode 100644 index 0000000..b2d4db0 --- /dev/null +++ b/nextjs/src/services/canvas/files/canvasFileService.ts @@ -0,0 +1,96 @@ +import fs from "fs/promises"; +import path from "path"; +import axios from "axios"; +import { canvasApi } from "../canvasServiceUtils"; +import { axiosClient } from "@/services/axiosUtils"; +import FormData from "form-data"; + +export const downloadUrlToTempDirectory = async ( + sourceUrl: string +): Promise => { + try { + const fileName = + path.basename(new URL(sourceUrl).pathname) || `tempfile-${Date.now()}`; + const tempFilePath = path.join("/tmp", fileName); + const response = await axios.get(sourceUrl, { + responseType: "arraybuffer", + }); + await fs.writeFile(tempFilePath, response.data); + return tempFilePath; + } catch (error) { + console.error("Error downloading or saving the file:", error); + throw error; + } +}; + +const getFileSize = async (pathToFile: string): Promise => { + try { + const stats = await fs.stat(pathToFile); + return stats.size; + } catch (error) { + console.error("Error reading file size:", error); + throw error; + } +}; + +export const uploadToCanvasPart1 = async ( + pathToUpload: string, + canvasCourseId: string +) => { + try { + const url = `${canvasApi}/courses/${canvasCourseId}/assignment_groups`; + + const formData = new FormData(); + + formData.append("name", path.basename(pathToUpload)); + formData.append("size", (await getFileSize(pathToUpload)).toString()); + + const response = await axiosClient.post(url, formData); + + const upload_url = response.data.upload_url; + const upload_params = response.data.upload_params; + + return { upload_url, upload_params }; + } catch (error) { + console.error("Error uploading file to Canvas part 1:", error); + throw error; + } +}; + +export const uploadToCanvasPart2 = async ( + pathToUpload: string, + upload_url: string, + upload_params: { [key: string]: string } +) => { + try { + const formData = new FormData(); + + Object.keys(formData).forEach((key) => { + formData.append(key, upload_params[key]); + }); + + const fileBuffer = await fs.readFile(pathToUpload); + const fileName = path.basename(pathToUpload); + formData.append("file", fileBuffer, fileName); + + const response = await axiosClient.post(upload_url, formData, { + headers: formData.getHeaders(), + validateStatus: (status) => status < 500, + }); + + if (response.status === 301) { + const redirectUrl = response.headers.location; + if (!redirectUrl) { + throw new Error( + "Redirect URL not provided in the Location header on redirect from second part of canvas file upload" + ); + } + + const redirectResponse = await axiosClient.get(redirectUrl); + console.log("redirect response", redirectResponse.data); + } + } catch (error) { + console.error("Error uploading file to Canvas part 1:", error); + throw error; + } +}; diff --git a/nextjs/src/services/htmlMarkdownUtils.ts b/nextjs/src/services/htmlMarkdownUtils.ts index ff9dc90..93c3ee1 100644 --- a/nextjs/src/services/htmlMarkdownUtils.ts +++ b/nextjs/src/services/htmlMarkdownUtils.ts @@ -44,8 +44,8 @@ export function markdownToHTMLSafe( const clean = DOMPurify.sanitize( marked.parse(markdownString, { async: false, pedantic: false, gfm: true }) ); - return convertImagesToCanvasImages(clean, settings); - // return clean; + // return convertImagesToCanvasImages(clean, settings); + return clean; } export function markdownToHtmlNoImages(markdownString: string) { diff --git a/requests/fileUpload.http b/requests/fileUpload.http new file mode 100644 index 0000000..7a15f54 --- /dev/null +++ b/requests/fileUpload.http @@ -0,0 +1,52 @@ +# https://canvas.instructure.com/doc/api/file.file_uploads.html +### +GET https://snow.instructure.com/api/v1/courses +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} + +### 1st request +# @name createFile + +POST https://snow.instructure.com/api/v1/courses/1014748/files +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} + +name=image.png&size=7339 + +### 2nd request based on the results of the first +### all upload_params need to be sent in the form +# @name uploadFile +POST {{createFile.response.body.$.upload_url}} +Content-Type: multipart/form-data; boundary=boundary + +--boundary +Content-Disposition: form-data; name="file"; filename="image.png" +Content-Type: image/png + +< ./image.png +--boundary +Content-Disposition: form-data; name="filename" + +image.png +--boundary +Content-Disposition: form-data; name="content_type" + +image/png +--boundary-- + +### if second request is a 301, you must follow the redirect with a get request and authorization +### mine seems to be a 201, which means i am done + + + +### alternatively, you can just give canvas the public URL +### requires polling a status url afterwards to know when complete, yuck +POST https://snow.instructure.com/api/v1/courses/1014748/files +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} + +url=http://example.com/my_pic.jpg&name=profile_pic.jpg + +### +GET https://snow.instructure.com/api/v1/courses/1014748/files/170618085 +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} + + +# file links can be reset with \ No newline at end of file diff --git a/requests/image.png b/requests/image.png new file mode 100644 index 0000000..38d54ab Binary files /dev/null and b/requests/image.png differ