sharepoint-files 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/package.json +25 -0
- package/src/index.ts +429 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# sharepoint-files
|
|
2
|
+
|
|
3
|
+
Minimal Microsoft Graph client for SharePoint file access and text extraction.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Client-credentials authentication
|
|
8
|
+
- Site and drive discovery
|
|
9
|
+
- Folder traversal and file listing
|
|
10
|
+
- File download helpers
|
|
11
|
+
- Plain-text extraction from common Office files
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install sharepoint-files
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createSharePointClient } from "sharepoint-files";
|
|
23
|
+
|
|
24
|
+
const client = createSharePointClient({
|
|
25
|
+
tenantId: process.env.MICROSOFT_TENANT_ID!,
|
|
26
|
+
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
27
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const site = await client.getSite("contoso.sharepoint.com:/sites/Research");
|
|
31
|
+
const drives = await client.listDrives(site.id);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Notes
|
|
35
|
+
|
|
36
|
+
- Uses the Graph API directly
|
|
37
|
+
- Designed for service-to-service use
|
|
38
|
+
- Secrets must be provided via environment variables or host config
|
|
39
|
+
|
|
40
|
+
Good fit for ingestion jobs, knowledge sync tools, and internal search systems.
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sharepoint-files",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "M365 SharePoint file access via Microsoft Graph API — auth, drive discovery, file listing, download, and text extraction",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"typecheck": "tsc --noEmit --pretty false"
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["sharepoint", "m365", "microsoft-graph", "office365", "files", "sync", "pptx", "docx"],
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"typescript": "^5"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sharepoint-files
|
|
3
|
+
*
|
|
4
|
+
* Microsoft 365 SharePoint file access via the Graph API.
|
|
5
|
+
* Handles client-credentials auth, drive discovery, file listing,
|
|
6
|
+
* file download, and plain-text extraction from common Office formats.
|
|
7
|
+
*
|
|
8
|
+
* All config is passed at construction time — no hardcoded credentials,
|
|
9
|
+
* tenant IDs, or SharePoint URLs. Works in Node.js and Cloudflare Workers
|
|
10
|
+
* (uses the global fetch API, no Node-specific HTTP modules).
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* import { createSharePointClient } from "sharepoint-files";
|
|
14
|
+
*
|
|
15
|
+
* const sp = createSharePointClient({
|
|
16
|
+
* tenantId: process.env.MICROSOFT_TENANT_ID!,
|
|
17
|
+
* clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
18
|
+
* clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Get a site by its host:path identifier
|
|
22
|
+
* const site = await sp.getSite("contoso.sharepoint.com:/sites/Research");
|
|
23
|
+
*
|
|
24
|
+
* // List all drives (document libraries) on the site
|
|
25
|
+
* const drives = await sp.listDrives(site.id);
|
|
26
|
+
*
|
|
27
|
+
* // List files in a folder (BFS, up to maxDepth levels)
|
|
28
|
+
* const files = await sp.listFiles(drives[0].id, { maxDepth: 3, maxFiles: 200 });
|
|
29
|
+
*
|
|
30
|
+
* // Download a file as ArrayBuffer
|
|
31
|
+
* const buffer = await sp.downloadFile(drives[0].id, files[0].id);
|
|
32
|
+
*
|
|
33
|
+
* // Extract plain text from a downloaded buffer
|
|
34
|
+
* const text = await sp.extractText(buffer, files[0].name);
|
|
35
|
+
*
|
|
36
|
+
* // Combined: download + extract in one call
|
|
37
|
+
* const { text, webUrl } = await sp.downloadAndExtract(drives[0].id, files[0].id, files[0].name);
|
|
38
|
+
*
|
|
39
|
+
* Required Azure app permissions (application, not delegated):
|
|
40
|
+
* Files.Read.All — scoped to the specific SharePoint site
|
|
41
|
+
* (Sites.Read.All is NOT required when using the host:path site accessor)
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
45
|
+
|
|
46
|
+
// ── Config & Types ─────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export interface SharePointClientConfig {
|
|
49
|
+
/** Azure AD tenant ID */
|
|
50
|
+
tenantId: string;
|
|
51
|
+
/** Azure app client ID */
|
|
52
|
+
clientId: string;
|
|
53
|
+
/** Azure app client secret */
|
|
54
|
+
clientSecret: string;
|
|
55
|
+
/** Max pages to follow when paginating Graph API results (default 5) */
|
|
56
|
+
maxPages?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SharePointSite {
|
|
60
|
+
id: string;
|
|
61
|
+
displayName: string;
|
|
62
|
+
webUrl: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SharePointDrive {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
webUrl: string;
|
|
69
|
+
driveType: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SharePointFile {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
/** File MIME type (present on files, absent on folders) */
|
|
76
|
+
mimeType?: string;
|
|
77
|
+
/** File size in bytes */
|
|
78
|
+
size: number;
|
|
79
|
+
lastModifiedDateTime: string;
|
|
80
|
+
webUrl: string;
|
|
81
|
+
/** Full path relative to drive root, e.g. "Documents/Research/deck.pptx" */
|
|
82
|
+
drivePath: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ListFilesOptions {
|
|
86
|
+
/** Max BFS depth to traverse (default 4) */
|
|
87
|
+
maxDepth?: number;
|
|
88
|
+
/** Max total files to collect (default 500) */
|
|
89
|
+
maxFiles?: number;
|
|
90
|
+
/** Only return files whose name matches this regex */
|
|
91
|
+
nameFilter?: RegExp;
|
|
92
|
+
/** Skip files larger than this size in bytes (default: no limit) */
|
|
93
|
+
maxBytes?: number;
|
|
94
|
+
/** Only return files modified after this ISO date string */
|
|
95
|
+
modifiedAfter?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface DownloadAndExtractResult {
|
|
99
|
+
text: string | null;
|
|
100
|
+
webUrl: string;
|
|
101
|
+
sizeBytes: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Text extraction helpers ────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const TEXT_EXTENSIONS = new Set([".txt", ".md", ".csv", ".json", ".html", ".htm"]);
|
|
107
|
+
const OFFICE_EXTENSIONS = new Set([".docx", ".doc", ".pdf", ".pptx", ".xlsx"]);
|
|
108
|
+
|
|
109
|
+
export function isSupportedTextFile(filename: string): boolean {
|
|
110
|
+
const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
|
|
111
|
+
return TEXT_EXTENSIONS.has(ext) || OFFICE_EXTENSIONS.has(ext);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract plain text from a file buffer based on the file extension.
|
|
116
|
+
*
|
|
117
|
+
* Supported formats:
|
|
118
|
+
* .txt / .md / .csv / .json / .html — decoded as UTF-8
|
|
119
|
+
* .pptx — fflate unzip → per-slide <a:t> text runs (requires fflate to be installed)
|
|
120
|
+
* .docx — mammoth text extraction (requires mammoth to be installed)
|
|
121
|
+
* .pdf — unpdf text extraction (requires unpdf to be installed)
|
|
122
|
+
* .xlsx — basic shared-strings XML extraction via fflate
|
|
123
|
+
*
|
|
124
|
+
* All heavy dependencies are dynamically imported — install only what you need.
|
|
125
|
+
*
|
|
126
|
+
* @returns Extracted text, or null if the format is unsupported or extraction fails.
|
|
127
|
+
*/
|
|
128
|
+
export async function extractText(
|
|
129
|
+
buffer: ArrayBuffer,
|
|
130
|
+
filename: string,
|
|
131
|
+
): Promise<string | null> {
|
|
132
|
+
const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
|
|
133
|
+
|
|
134
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
135
|
+
return new TextDecoder().decode(buffer);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (ext === ".pptx" || ext === ".xlsx") {
|
|
139
|
+
try {
|
|
140
|
+
const fflate = "fflate";
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
const { unzipSync } = (await import(fflate)) as any;
|
|
143
|
+
const uint8 = new Uint8Array(buffer);
|
|
144
|
+
const unzipped = unzipSync(uint8);
|
|
145
|
+
const parts: string[] = [];
|
|
146
|
+
|
|
147
|
+
for (const [path, data] of Object.entries(unzipped)) {
|
|
148
|
+
// PPTX slides
|
|
149
|
+
if (ext === ".pptx" && /^ppt\/slides\/slide\d+\.xml$/.test(path)) {
|
|
150
|
+
const xml = new TextDecoder().decode(data as Uint8Array);
|
|
151
|
+
for (const m of xml.matchAll(/<a:t>([^<]+)<\/a:t>/g)) parts.push(m[1]);
|
|
152
|
+
}
|
|
153
|
+
// XLSX shared strings (cell values)
|
|
154
|
+
if (ext === ".xlsx" && path === "xl/sharedStrings.xml") {
|
|
155
|
+
const xml = new TextDecoder().decode(data as Uint8Array);
|
|
156
|
+
for (const m of xml.matchAll(/<t[^>]*>([^<]+)<\/t>/g)) parts.push(m[1]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return parts.length ? parts.join(" ") : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (ext === ".pdf") {
|
|
167
|
+
try {
|
|
168
|
+
const unpdf = "unpdf";
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
170
|
+
const mod = (await import(unpdf)) as any;
|
|
171
|
+
const result = await mod.extractText(new Uint8Array(buffer));
|
|
172
|
+
return (result?.text as string[] | undefined)?.join("\n\n") ?? null;
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (ext === ".docx" || ext === ".doc") {
|
|
179
|
+
try {
|
|
180
|
+
const mammothPkg = "mammoth";
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
182
|
+
const mammoth = (await import(mammothPkg)) as any;
|
|
183
|
+
const result = await mammoth.extractRawText({ buffer: Buffer.from(buffer) });
|
|
184
|
+
return (result?.value as string) || null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Client factory ─────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
export function createSharePointClient(config: SharePointClientConfig) {
|
|
196
|
+
const { tenantId, clientId, clientSecret, maxPages = 5 } = config;
|
|
197
|
+
let _token: string | null = null;
|
|
198
|
+
let _tokenExpiry = 0;
|
|
199
|
+
|
|
200
|
+
// ── Auth ──────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
async function getToken(): Promise<string> {
|
|
203
|
+
if (_token && Date.now() < _tokenExpiry - 60_000) return _token;
|
|
204
|
+
|
|
205
|
+
const res = await fetch(
|
|
206
|
+
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
|
207
|
+
{
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
210
|
+
body: new URLSearchParams({
|
|
211
|
+
grant_type: "client_credentials",
|
|
212
|
+
client_id: clientId,
|
|
213
|
+
client_secret: clientSecret,
|
|
214
|
+
scope: "https://graph.microsoft.com/.default",
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (!res.ok) {
|
|
220
|
+
const err = await res.text().catch(() => res.status.toString());
|
|
221
|
+
throw new Error(`M365 auth failed (${res.status}): ${err.slice(0, 200)}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const data = (await res.json()) as { access_token: string; expires_in: number };
|
|
225
|
+
_token = data.access_token;
|
|
226
|
+
_tokenExpiry = Date.now() + data.expires_in * 1_000;
|
|
227
|
+
return _token;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Graph helpers ─────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
async function graphGet<T>(path: string, params: Record<string, string> = {}): Promise<T> {
|
|
233
|
+
const token = await getToken();
|
|
234
|
+
const url = new URL(`${GRAPH_BASE}${path}`);
|
|
235
|
+
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
236
|
+
const res = await fetch(url.toString(), { headers: { Authorization: `Bearer ${token}` } });
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
const err = await res.text().catch(() => res.status.toString());
|
|
239
|
+
throw new Error(`Graph ${path} → ${res.status}: ${err.slice(0, 200)}`);
|
|
240
|
+
}
|
|
241
|
+
return res.json() as Promise<T>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function graphGetAll<T>(
|
|
245
|
+
path: string,
|
|
246
|
+
params: Record<string, string> = {},
|
|
247
|
+
): Promise<T[]> {
|
|
248
|
+
const items: T[] = [];
|
|
249
|
+
let data = await graphGet<{ value?: T[]; "@odata.nextLink"?: string }>(path, params);
|
|
250
|
+
items.push(...(data.value ?? []));
|
|
251
|
+
|
|
252
|
+
let pages = 1;
|
|
253
|
+
while (data["@odata.nextLink"] && pages < maxPages) {
|
|
254
|
+
const token = await getToken();
|
|
255
|
+
const res = await fetch(data["@odata.nextLink"], {
|
|
256
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
257
|
+
});
|
|
258
|
+
if (!res.ok) break;
|
|
259
|
+
data = await res.json();
|
|
260
|
+
items.push(...(data.value ?? []));
|
|
261
|
+
pages++;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return items;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
/**
|
|
271
|
+
* Get a SharePoint site by its "hostname:/path" identifier.
|
|
272
|
+
*
|
|
273
|
+
* Examples:
|
|
274
|
+
* "contoso.sharepoint.com:/sites/Research"
|
|
275
|
+
* "contoso.sharepoint.com:/teams/Engineering"
|
|
276
|
+
*
|
|
277
|
+
* Does not require Sites.Read.All — works with site-scoped Files.Read.All.
|
|
278
|
+
*/
|
|
279
|
+
async getSite(siteRef: string): Promise<SharePointSite> {
|
|
280
|
+
return graphGet<SharePointSite>(`/sites/${siteRef}`);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/** List all document library drives on a site */
|
|
284
|
+
async listDrives(siteId: string): Promise<SharePointDrive[]> {
|
|
285
|
+
return graphGetAll<SharePointDrive>(`/sites/${siteId}/drives`, {
|
|
286
|
+
$select: "id,name,webUrl,driveType",
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Recursively list all files in a drive using BFS.
|
|
292
|
+
*
|
|
293
|
+
* @param driveId Graph API drive ID
|
|
294
|
+
* @param options Filter + depth controls
|
|
295
|
+
*/
|
|
296
|
+
async listFiles(
|
|
297
|
+
driveId: string,
|
|
298
|
+
options: ListFilesOptions = {},
|
|
299
|
+
): Promise<SharePointFile[]> {
|
|
300
|
+
const {
|
|
301
|
+
maxDepth = 4,
|
|
302
|
+
maxFiles = 500,
|
|
303
|
+
nameFilter,
|
|
304
|
+
maxBytes,
|
|
305
|
+
modifiedAfter,
|
|
306
|
+
} = options;
|
|
307
|
+
|
|
308
|
+
const allFiles: SharePointFile[] = [];
|
|
309
|
+
const queue: { folderId: string; path: string; depth: number }[] = [
|
|
310
|
+
{ folderId: "root", path: "", depth: 0 },
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
while (queue.length && allFiles.length < maxFiles) {
|
|
314
|
+
const { folderId, path, depth } = queue.shift()!;
|
|
315
|
+
if (depth > maxDepth) continue;
|
|
316
|
+
|
|
317
|
+
let items: Array<{
|
|
318
|
+
id: string;
|
|
319
|
+
name: string;
|
|
320
|
+
file?: { mimeType: string };
|
|
321
|
+
folder?: object;
|
|
322
|
+
size?: number;
|
|
323
|
+
lastModifiedDateTime?: string;
|
|
324
|
+
webUrl?: string;
|
|
325
|
+
}>;
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
items = await graphGetAll(
|
|
329
|
+
`/drives/${driveId}/items/${folderId}/children`,
|
|
330
|
+
{
|
|
331
|
+
$select: "id,name,file,folder,size,lastModifiedDateTime,webUrl",
|
|
332
|
+
$orderby: "lastModifiedDateTime desc",
|
|
333
|
+
$top: "200",
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
} catch {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const item of items) {
|
|
341
|
+
if (item.folder) {
|
|
342
|
+
queue.push({
|
|
343
|
+
folderId: item.id,
|
|
344
|
+
path: path ? `${path}/${item.name}` : item.name,
|
|
345
|
+
depth: depth + 1,
|
|
346
|
+
});
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!item.file) continue;
|
|
351
|
+
|
|
352
|
+
// Apply filters
|
|
353
|
+
if (nameFilter && !nameFilter.test(item.name)) continue;
|
|
354
|
+
if (maxBytes && (item.size ?? 0) > maxBytes) continue;
|
|
355
|
+
if (modifiedAfter && item.lastModifiedDateTime) {
|
|
356
|
+
if (new Date(item.lastModifiedDateTime) < new Date(modifiedAfter)) continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
allFiles.push({
|
|
360
|
+
id: item.id,
|
|
361
|
+
name: item.name,
|
|
362
|
+
mimeType: item.file.mimeType,
|
|
363
|
+
size: item.size ?? 0,
|
|
364
|
+
lastModifiedDateTime: item.lastModifiedDateTime ?? "",
|
|
365
|
+
webUrl: item.webUrl ?? "",
|
|
366
|
+
drivePath: path ? `${path}/${item.name}` : item.name,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (allFiles.length >= maxFiles) break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return allFiles;
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/** Download a single file as ArrayBuffer */
|
|
377
|
+
async downloadFile(driveId: string, itemId: string): Promise<ArrayBuffer> {
|
|
378
|
+
const token = await getToken();
|
|
379
|
+
const res = await fetch(
|
|
380
|
+
`${GRAPH_BASE}/drives/${driveId}/items/${itemId}/content`,
|
|
381
|
+
{ headers: { Authorization: `Bearer ${token}` } },
|
|
382
|
+
);
|
|
383
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
384
|
+
return res.arrayBuffer();
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Fetch file metadata (size, webUrl, lastModifiedDateTime) without downloading content.
|
|
389
|
+
* Useful for pre-flight size checks before deciding whether to download.
|
|
390
|
+
*/
|
|
391
|
+
async getFileMeta(
|
|
392
|
+
driveId: string,
|
|
393
|
+
itemId: string,
|
|
394
|
+
): Promise<{ id: string; name: string; size: number; webUrl: string; lastModifiedDateTime: string }> {
|
|
395
|
+
return graphGet(`/drives/${driveId}/items/${itemId}`, {
|
|
396
|
+
$select: "id,name,size,webUrl,lastModifiedDateTime",
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Download a file and extract plain text from it.
|
|
402
|
+
* Returns null text if extraction fails or format is unsupported.
|
|
403
|
+
*
|
|
404
|
+
* @param maxBytes Skip download if file exceeds this size (default: no limit)
|
|
405
|
+
*/
|
|
406
|
+
async downloadAndExtract(
|
|
407
|
+
driveId: string,
|
|
408
|
+
itemId: string,
|
|
409
|
+
filename: string,
|
|
410
|
+
maxBytes?: number,
|
|
411
|
+
): Promise<DownloadAndExtractResult> {
|
|
412
|
+
const meta = await this.getFileMeta(driveId, itemId);
|
|
413
|
+
|
|
414
|
+
if (maxBytes && meta.size > maxBytes) {
|
|
415
|
+
return { text: null, webUrl: meta.webUrl, sizeBytes: meta.size };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const buffer = await this.downloadFile(driveId, itemId);
|
|
419
|
+
const text = await extractText(buffer, filename);
|
|
420
|
+
return { text, webUrl: meta.webUrl, sizeBytes: meta.size };
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
/** Re-export text extraction for use without downloading (e.g. from a cached buffer) */
|
|
424
|
+
extractText,
|
|
425
|
+
isSupportedTextFile,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export type SharePointClient = ReturnType<typeof createSharePointClient>;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|