0

Upload File

by
Published today

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).

Script ms_sharepoint Verified

The script

Submitted by hugo989 Bun
Verified 5 hours ago
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
// The site's default document library, or the library given by drive_id
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
// An item is addressed by its id, or by a path from the drive root starting with "/"
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
  // ponytail: the whole file is held in memory; stream chunks from loadS3FileStream if multi-GB uploads matter
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
  // Chunks must be a multiple of 320 KiB; the upload URL is pre-authenticated
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