1 | import * as wmill from "windmill-client" |
2 | import { S3Object } from "windmill-client" |
3 |
|
4 | const graph = "https://graph.microsoft.com/v1.0" |
5 |
|
6 | export type DynSelect_site_id = string |
7 |
|
8 | export async function site_id(auth: RT.MsSharepoint) { |
9 | const response = await fetch( |
10 | `${graph}/sites?search=*&$select=id,displayName,webUrl&$top=200`, |
11 | { |
12 | headers: { |
13 | Authorization: `Bearer ${auth.token}`, |
14 | Accept: "application/json", |
15 | }, |
16 | } |
17 | ) |
18 | if (!response.ok) { |
19 | throw new Error(`${response.status} ${await response.text()}`) |
20 | } |
21 | const { value } = (await response.json()) as { |
22 | value: { id: string; displayName: string; webUrl: string }[] |
23 | } |
24 | return value.map((s) => ({ |
25 | value: s.id, |
26 | label: `${s.displayName} (${s.webUrl})`, |
27 | })) |
28 | } |
29 |
|
30 | export type DynSelect_drive_id = string |
31 |
|
32 | export async function drive_id(auth: RT.MsSharepoint, site_id: string) { |
33 | const response = await fetch( |
34 | `${graph}/sites/${site_id}/drives?$select=id,name`, |
35 | { |
36 | headers: { |
37 | Authorization: `Bearer ${auth.token}`, |
38 | Accept: "application/json", |
39 | }, |
40 | } |
41 | ) |
42 | if (!response.ok) { |
43 | throw new Error(`${response.status} ${await response.text()}`) |
44 | } |
45 | const { value } = (await response.json()) as { |
46 | value: { id: string; name: string }[] |
47 | } |
48 | return value.map((d) => ({ value: d.id, label: d.name })) |
49 | } |
50 |
|
51 | |
52 | function driveUrl(site_id: string, drive_id: string | undefined) { |
53 | return drive_id != null && drive_id !== "" |
54 | ? `${graph}/drives/${encodeURIComponent(drive_id)}` |
55 | : `${graph}/sites/${site_id}/drive` |
56 | } |
57 |
|
58 | |
59 | function ref(item: string | undefined) { |
60 | if (item == null || item === "" || item === "/") return "root" |
61 | if (!item.startsWith("/")) return `items/${encodeURIComponent(item)}` |
62 | const path = item |
63 | .replace(/\/+$/, "") |
64 | .split("/") |
65 | .map(encodeURIComponent) |
66 | .join("/") |
67 | return `root:${path}:` |
68 | } |
69 |
|
70 | |
71 | * Upload File |
72 | * Upload a file from Windmill object storage to a folder in a site's default document library, or in another of its libraries when `drive_id` is set. `folder` is an id, or a path from the drive root starting with "/" (e.g. `/Reports/2026`), the root when empty; `name` defaults to the object's file name. Files over 4 MB go through a resumable upload session in 10 MB chunks. Requires Sites.ReadWrite.All (a connection default). |
73 | */ |
74 | export async function main( |
75 | auth: RT.MsSharepoint, |
76 | site_id: DynSelect_site_id, |
77 | file: S3Object, |
78 | folder: string | undefined, |
79 | name: string | undefined, |
80 | conflict_behavior: "rename" | "replace" | "fail" = "rename", |
81 | drive_id: DynSelect_drive_id | undefined |
82 | ) { |
83 | const drive = driveUrl(site_id, drive_id) |
84 | |
85 | const bytes = await wmill.loadS3File(file) |
86 | if (!bytes) throw new Error(`Could not load ${file.s3}`) |
87 |
|
88 | const fileName = encodeURIComponent( |
89 | name != null && name !== "" ? name : file.s3.split("/").pop()! |
90 | ) |
91 | const parent = ref(folder) |
92 | const target = parent.startsWith("items/") |
93 | ? `${drive}/${parent}:/${fileName}:` |
94 | : `${drive}/root:${parent === "root" ? "" : parent.slice(5, -1)}/${fileName}:` |
95 |
|
96 | if (bytes.length <= 4 * 1024 * 1024) { |
97 | const response = await fetch( |
98 | `${target}/[email protected]=${conflict_behavior}`, |
99 | { |
100 | method: "PUT", |
101 | headers: { |
102 | Authorization: `Bearer ${auth.token}`, |
103 | "Content-Type": "application/octet-stream", |
104 | Accept: "application/json", |
105 | }, |
106 | body: bytes, |
107 | } |
108 | ) |
109 | if (!response.ok) { |
110 | throw new Error(`${response.status} ${await response.text()}`) |
111 | } |
112 | return await response.json() |
113 | } |
114 |
|
115 | const session = await fetch(`${target}/createUploadSession`, { |
116 | method: "POST", |
117 | headers: { |
118 | Authorization: `Bearer ${auth.token}`, |
119 | "Content-Type": "application/json", |
120 | Accept: "application/json", |
121 | }, |
122 | body: JSON.stringify({ |
123 | item: { "@microsoft.graph.conflictBehavior": conflict_behavior }, |
124 | }), |
125 | }) |
126 | if (!session.ok) { |
127 | throw new Error(`${session.status} ${await session.text()}`) |
128 | } |
129 | const { uploadUrl } = (await session.json()) as { uploadUrl: string } |
130 |
|
131 | |
132 | const chunk = 10 * 1024 * 1024 |
133 | for (let start = 0; ; start += chunk) { |
134 | const end = Math.min(start + chunk, bytes.length) |
135 | const response = await fetch(uploadUrl, { |
136 | method: "PUT", |
137 | headers: { |
138 | "Content-Range": `bytes ${start}-${end - 1}/${bytes.length}`, |
139 | }, |
140 | body: bytes.subarray(start, end), |
141 | }) |
142 | if (!response.ok) { |
143 | throw new Error(`${response.status} ${await response.text()}`) |
144 | } |
145 | if (end === bytes.length) return await response.json() |
146 | } |
147 | } |
148 |
|