1 | |
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 | |
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 | |
41 | const now = new Date().toISOString() |
42 | const latest = `${drive}/root/delta?token=latest` |
43 |
|
44 | |
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 | |
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 | |
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 | |
108 | |
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 |
|