0

New or Updated File

by
Published today

Emits the files or folders created (or, with `events`, also modified; a folder counts as modified when something inside it changes) in your OneDrive, or in another drive when `drive_id` is set since the last poll, optionally only under one `folder` path (e.g. `/Reports`). The first run records the drive's current state and emits nothing. Polls the drive's delta feed because Microsoft Graph push needs a change-notification subscription renewed every few weeks against a public endpoint, which belongs in a Windmill native trigger; run this as a Scheduled Poll until then. Requires Files.Read (covered by the default Files.ReadWrite); a drive that is not your own needs Files.Read.All.

Scriptยท trigger ms_onedrive Verified

The script

Submitted by hugo989 Typescript (fetch-only)
Verified 4 hours ago
1
//native
2

3
import * as wmill from "windmill-client"
4

5
const graph = "https://graph.microsoft.com/v1.0"
6

7
function driveUrl(drive_id: string | undefined) {
8
  return drive_id != null && drive_id !== ""
9
    ? `${graph}/drives/${encodeURIComponent(drive_id)}`
10
    : `${graph}/me/drive`
11
}
12

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

25
/**
26
 * New or Updated File
27
 * Emits the files or folders created (or, with `events`, also modified; a folder counts as modified when something inside it changes) in your OneDrive, or in another drive when `drive_id` is set since the last poll, optionally only under one `folder` path (e.g. `/Reports`). The first run records the drive's current state and emits nothing. Polls the drive's delta feed because Microsoft Graph push needs a change-notification subscription renewed every few weeks against a public endpoint, which belongs in a Windmill native trigger; run this as a Scheduled Poll until then. Requires Files.Read (covered by the default Files.ReadWrite); a drive that is not your own needs Files.Read.All.
28
 */
29
export async function main(
30
  auth: RT.MsOnedrive,
31

32
  events: "created" | "created_or_updated" = "created",
33
  kind: "files" | "folders" | "all" = "files",
34
  folder: string | undefined,
35
  drive_id: string | undefined
36
) {
37
  const drive = driveUrl(drive_id)
38
  const state: { delta_link: string; since: string } | undefined =
39
    await wmill.getState()
40
  // Taken before the request so an item created while paging is not skipped next run
41
  const now = new Date().toISOString()
42
  const latest = `${drive}/root/delta?token=latest`
43

44
  // The feed can repeat an item: the last occurrence wins
45
  const items = new Map<string, { [key: string]: any }>()
46
  let deltaLink: string | undefined
47
  let expired = false
48
  let next: string | undefined = state?.delta_link ?? latest
49
  while (next) {
50
    const response = await fetch(next, {
51
      headers: {
52
        Authorization: `Bearer ${auth.token}`,
53
        Accept: "application/json",
54
      },
55
    })
56
    // 410 Gone: the stored delta link is too old, so start over from the current state
57
    if (response.status === 410 && !expired) {
58
      expired = true
59
      items.clear()
60
      next = latest
61
      continue
62
    }
63
    if (!response.ok) {
64
      throw new Error(`${response.status} ${await response.text()}`)
65
    }
66
    const page = (await response.json()) as {
67
      value: { [key: string]: any }[]
68
      "@odata.nextLink"?: string
69
      "@odata.deltaLink"?: string
70
    }
71
    for (const item of page.value) items.set(item.id, item)
72
    next = page["@odata.nextLink"]
73
    deltaLink = page["@odata.deltaLink"] ?? deltaLink
74
  }
75
  await wmill.setState({ delta_link: deltaLink, since: now })
76

77
  // First run (or an expired link): only record where the feed stands
78
  if (!state?.delta_link || expired) return []
79

80
  let under = ""
81
  let underId: string | undefined
82
  if (folder != null && folder !== "" && folder !== "/") {
83
    under = folder.replace(/\/+$/, "")
84
    const response = await fetch(`${drive}/${ref(under)}?$select=id`, {
85
      headers: {
86
        Authorization: `Bearer ${auth.token}`,
87
        Accept: "application/json",
88
      },
89
    })
90
    if (!response.ok) {
91
      throw new Error(`${response.status} ${await response.text()}`)
92
    }
93
    underId = ((await response.json()) as { id: string }).id
94
  }
95

96
  return [...items.values()].filter((item) => {
97
    if (item.deleted || item.root) return false
98
    if (kind === "files" && !item.file) return false
99
    if (kind === "folders" && !item.folder) return false
100
    if (
101
      events === "created" &&
102
      Date.parse(item.createdDateTime) < Date.parse(state.since)
103
    ) {
104
      return false
105
    }
106
    if (under === "") return true
107
    // parentReference.path looks like /drive/root:/Reports/2026, but the feed
108
    // may leave it out: direct children are then still matched by parent id
109
    const parent = decodeURIComponent(item.parentReference?.path ?? "").split(
110
      "root:"
111
    )[1]
112
    return (
113
      item.parentReference?.id === underId ||
114
      parent === under ||
115
      (parent ?? "").startsWith(under + "/")
116
    )
117
  })
118
}
119