From 9cba83e51c97f63a05a8c6975b22b66936935126 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Wed, 20 Nov 2024 09:14:11 -0700 Subject: [PATCH] working on code to upload files... --- nextjs/package.json | 1 + nextjs/pnpm-lock.yaml | 3 + .../canvas/files/canvasFileService.ts | 96 ++++++++++++++++++ nextjs/src/services/htmlMarkdownUtils.ts | 4 +- requests/fileUpload.http | 52 ++++++++++ requests/image.png | Bin 0 -> 7339 bytes 6 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 nextjs/src/services/canvas/files/canvasFileService.ts create mode 100644 requests/fileUpload.http create mode 100644 requests/image.png 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 0000000000000000000000000000000000000000..38d54ab46ec149ccbf99a038422561e873641e52 GIT binary patch literal 7339 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 literal 0 HcmV?d00001