1 | import * as wmill from "windmill-client" |
2 | import { S3Object } from "windmill-client" |
3 |
|
4 | |
5 | * Upload File |
6 | * Upload a file from Windmill object storage to Notion. Files over 20 MiB are sent in 10 MiB parts. Pass `attach_to_page_id` to append it to a page as a file block; otherwise use the returned `id` within an hour (as a `file_upload` in a block, cover, icon or files property) or it expires. Free workspaces cap files at 5 MiB. |
7 | */ |
8 | export async function main( |
9 | auth: RT.Notion, |
10 | file: S3Object, |
11 | filename: string, |
12 | content_type: string | undefined, |
13 | attach_to_page_id: string | undefined |
14 | ) { |
15 | const bytes = await wmill.loadS3File(file) |
16 | if (!bytes) { |
17 | throw new Error("Could not load the file from object storage") |
18 | } |
19 | const headers = { |
20 | Authorization: `Bearer ${auth.token}`, |
21 | Accept: "application/json", |
22 | "Notion-Version": "2026-03-11", |
23 | } |
24 | const call = async (path: string, init: RequestInit) => { |
25 | const response = await fetch(`https://api.notion.com/v1/${path}`, { |
26 | ...init, |
27 | headers: { ...headers, ...init.headers }, |
28 | }) |
29 | if (!response.ok) { |
30 | throw new Error(`${response.status} ${await response.text()}`) |
31 | } |
32 | return await response.json() |
33 | } |
34 |
|
35 | const PART = 10 * 1024 * 1024 |
36 | const parts = |
37 | bytes.length > 20 * 1024 * 1024 ? Math.ceil(bytes.length / PART) : 1 |
38 | const create: { [key: string]: any } = { filename } |
39 | if (content_type !== undefined && content_type !== "") { |
40 | create.content_type = content_type |
41 | } |
42 | if (parts > 1) { |
43 | create.mode = "multi_part" |
44 | create.number_of_parts = parts |
45 | } |
46 | const upload = await call("file_uploads", { |
47 | method: "POST", |
48 | headers: { "Content-Type": "application/json" }, |
49 | body: JSON.stringify(create), |
50 | }) |
51 |
|
52 | let result = upload |
53 | for (let i = 0; i < parts; i++) { |
54 | const form = new FormData() |
55 | const chunk = parts > 1 ? bytes.subarray(i * PART, (i + 1) * PART) : bytes |
56 | form.append( |
57 | "file", |
58 | new Blob([chunk], { type: content_type || undefined }), |
59 | filename |
60 | ) |
61 | if (parts > 1) form.append("part_number", String(i + 1)) |
62 | result = await call(`file_uploads/${upload.id}/send`, { |
63 | method: "POST", |
64 | body: form, |
65 | }) |
66 | } |
67 | if (parts > 1) { |
68 | result = await call(`file_uploads/${upload.id}/complete`, { |
69 | method: "POST", |
70 | }) |
71 | } |
72 |
|
73 | if (attach_to_page_id !== undefined && attach_to_page_id !== "") { |
74 | await call(`blocks/${attach_to_page_id}/children`, { |
75 | method: "PATCH", |
76 | headers: { "Content-Type": "application/json" }, |
77 | body: JSON.stringify({ |
78 | children: [ |
79 | { |
80 | type: "file", |
81 | file: { type: "file_upload", file_upload: { id: upload.id } }, |
82 | }, |
83 | ], |
84 | }), |
85 | }) |
86 | } |
87 |
|
88 | return result |
89 | } |
90 |
|