0

Upload File

by
Published today

Upload a file from Windmill object storage to a folder in your OneDrive, or in another drive 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 Files.ReadWrite (a connection default); a drive that is not your own needs Files.ReadWrite.All.

Script ms_onedrive Verified

The script

Submitted by hugo989 Bun
Verified 4 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
function driveUrl(drive_id: string | undefined) {
7
  return drive_id != null && drive_id !== ""
8
    ? `${graph}/drives/${encodeURIComponent(drive_id)}`
9
    : `${graph}/me/drive`
10
}
11

12
// An item is addressed by its id, or by a path from the drive root starting with "/"
13
function ref(item: string | undefined) {
14
  if (item == null || item === "" || item === "/") return "root"
15
  if (!item.startsWith("/")) return `items/${encodeURIComponent(item)}`
16
  const path = item
17
    .replace(/\/+$/, "")
18
    .split("/")
19
    .map(encodeURIComponent)
20
    .join("/")
21
  return `root:${path}:`
22
}
23

24
/**
25
 * Upload File
26
 * Upload a file from Windmill object storage to a folder in your OneDrive, or in another drive 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 Files.ReadWrite (a connection default); a drive that is not your own needs Files.ReadWrite.All.
27
 */
28
export async function main(
29
  auth: RT.MsOnedrive,
30

31
  file: S3Object,
32
  folder: string | undefined,
33
  name: string | undefined,
34
  conflict_behavior: "rename" | "replace" | "fail" = "rename",
35
  drive_id: string | undefined
36
) {
37
  const drive = driveUrl(drive_id)
38
  // ponytail: the whole file is held in memory; stream chunks from loadS3FileStream if multi-GB uploads matter
39
  const bytes = await wmill.loadS3File(file)
40
  if (!bytes) throw new Error(`Could not load ${file.s3}`)
41

42
  const fileName = encodeURIComponent(
43
    name != null && name !== "" ? name : file.s3.split("/").pop()!
44
  )
45
  const parent = ref(folder)
46
  const target = parent.startsWith("items/")
47
    ? `${drive}/${parent}:/${fileName}:`
48
    : `${drive}/root:${parent === "root" ? "" : parent.slice(5, -1)}/${fileName}:`
49

50
  if (bytes.length <= 4 * 1024 * 1024) {
51
    const response = await fetch(
52
      `${target}/[email protected]=${conflict_behavior}`,
53
      {
54
        method: "PUT",
55
        headers: {
56
          Authorization: `Bearer ${auth.token}`,
57
          "Content-Type": "application/octet-stream",
58
          Accept: "application/json",
59
        },
60
        body: bytes,
61
      }
62
    )
63
    if (!response.ok) {
64
      throw new Error(`${response.status} ${await response.text()}`)
65
    }
66
    return await response.json()
67
  }
68

69
  const session = await fetch(`${target}/createUploadSession`, {
70
    method: "POST",
71
    headers: {
72
      Authorization: `Bearer ${auth.token}`,
73
      "Content-Type": "application/json",
74
      Accept: "application/json",
75
    },
76
    body: JSON.stringify({
77
      item: { "@microsoft.graph.conflictBehavior": conflict_behavior },
78
    }),
79
  })
80
  if (!session.ok) {
81
    throw new Error(`${session.status} ${await session.text()}`)
82
  }
83
  const { uploadUrl } = (await session.json()) as { uploadUrl: string }
84

85
  // Chunks must be a multiple of 320 KiB; the upload URL is pre-authenticated
86
  const chunk = 10 * 1024 * 1024
87
  for (let start = 0; ; start += chunk) {
88
    const end = Math.min(start + chunk, bytes.length)
89
    const response = await fetch(uploadUrl, {
90
      method: "PUT",
91
      headers: {
92
        "Content-Range": `bytes ${start}-${end - 1}/${bytes.length}`,
93
      },
94
      body: bytes.subarray(start, end),
95
    })
96
    if (!response.ok) {
97
      throw new Error(`${response.status} ${await response.text()}`)
98
    }
99
    if (end === bytes.length) return await response.json()
100
  }
101
}
102