OpenRAG 0.5.0.dev22__py3-none-any.whl → 0.5.0.dev24__py3-none-any.whl
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.
- connectors/onedrive/connector.py +194 -144
- connectors/onedrive/oauth.py +88 -47
- {openrag-0.5.0.dev22.dist-info → openrag-0.5.0.dev24.dist-info}/METADATA +1 -1
- {openrag-0.5.0.dev22.dist-info → openrag-0.5.0.dev24.dist-info}/RECORD +8 -8
- {openrag-0.5.0.dev22.dist-info → openrag-0.5.0.dev24.dist-info}/WHEEL +0 -0
- {openrag-0.5.0.dev22.dist-info → openrag-0.5.0.dev24.dist-info}/entry_points.txt +0 -0
- {openrag-0.5.0.dev22.dist-info → openrag-0.5.0.dev24.dist-info}/licenses/LICENSE +0 -0
- {openrag-0.5.0.dev22.dist-info → openrag-0.5.0.dev24.dist-info}/top_level.txt +0 -0
connectors/onedrive/connector.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import List, Dict, Any, Optional
|
|
4
2
|
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
5
|
from urllib.parse import urlparse
|
|
6
|
+
|
|
6
7
|
import httpx
|
|
7
8
|
|
|
8
9
|
from ..base import BaseConnector, ConnectorDocument, DocumentACL
|
|
@@ -23,7 +24,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
23
24
|
CONNECTOR_DESCRIPTION = "Add knowledge from OneDrive"
|
|
24
25
|
CONNECTOR_ICON = "onedrive"
|
|
25
26
|
|
|
26
|
-
def __init__(self, config:
|
|
27
|
+
def __init__(self, config: dict[str, Any]):
|
|
27
28
|
super().__init__(config)
|
|
28
29
|
|
|
29
30
|
logger.debug(f"OneDrive connector __init__ called with config type: {type(config)}")
|
|
@@ -63,6 +64,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
63
64
|
|
|
64
65
|
# Token file setup - use data directory for persistence
|
|
65
66
|
from config.paths import get_data_file
|
|
67
|
+
|
|
66
68
|
token_file = config.get("token_file") or get_data_file("onedrive_token.json")
|
|
67
69
|
Path(token_file).parent.mkdir(parents=True, exist_ok=True)
|
|
68
70
|
|
|
@@ -91,23 +93,29 @@ class OneDriveConnector(BaseConnector):
|
|
|
91
93
|
self.oauth = None
|
|
92
94
|
|
|
93
95
|
# Track subscription ID for webhooks (note: change notifications might not be available for personal accounts)
|
|
94
|
-
self._subscription_id:
|
|
96
|
+
self._subscription_id: str | None = None
|
|
95
97
|
|
|
96
98
|
# Graph API defaults
|
|
97
99
|
self._graph_api_version = "v1.0"
|
|
98
|
-
self._default_params = {
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
100
|
+
self._default_params: dict[str, Any] = {}
|
|
101
|
+
|
|
102
102
|
# Selective sync support (similar to Google Drive)
|
|
103
|
-
self.cfg = type(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
self.cfg = type(
|
|
104
|
+
"OneDriveConfig",
|
|
105
|
+
(),
|
|
106
|
+
{
|
|
107
|
+
"file_ids": config.get("file_ids")
|
|
108
|
+
or config.get("selected_files")
|
|
109
|
+
or config.get("selected_file_ids"),
|
|
110
|
+
"folder_ids": config.get("folder_ids")
|
|
111
|
+
or config.get("selected_folders")
|
|
112
|
+
or config.get("selected_folder_ids"),
|
|
113
|
+
},
|
|
114
|
+
)()
|
|
115
|
+
|
|
108
116
|
# Cache for file metadata including download URLs
|
|
109
117
|
# This allows direct download without Graph API for sharing IDs
|
|
110
|
-
self._file_infos:
|
|
118
|
+
self._file_infos: dict[str, dict[str, Any]] = {}
|
|
111
119
|
|
|
112
120
|
@property
|
|
113
121
|
def _graph_base_url(self) -> str:
|
|
@@ -115,20 +123,20 @@ class OneDriveConnector(BaseConnector):
|
|
|
115
123
|
return f"https://graph.microsoft.com/{self._graph_api_version}"
|
|
116
124
|
|
|
117
125
|
@property
|
|
118
|
-
def base_url(self) ->
|
|
126
|
+
def base_url(self) -> str | None:
|
|
119
127
|
"""Generic base URL property (OneDrive/SharePoint domain)"""
|
|
120
128
|
return self._base_url
|
|
121
|
-
|
|
129
|
+
|
|
122
130
|
@base_url.setter
|
|
123
131
|
def base_url(self, value: str):
|
|
124
132
|
"""Set base URL"""
|
|
125
133
|
self._base_url = value
|
|
126
134
|
|
|
127
|
-
def set_file_infos(self, file_infos:
|
|
135
|
+
def set_file_infos(self, file_infos: list[dict[str, Any]]) -> None:
|
|
128
136
|
"""
|
|
129
137
|
Cache file metadata including download URLs for later use.
|
|
130
138
|
This allows direct download without Graph API calls for sharing IDs.
|
|
131
|
-
|
|
139
|
+
|
|
132
140
|
Args:
|
|
133
141
|
file_infos: List of file info dicts with {id, name, mimeType, downloadUrl, size}
|
|
134
142
|
"""
|
|
@@ -140,7 +148,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
140
148
|
if info.get("downloadUrl"):
|
|
141
149
|
logger.debug(f"Cached download URL for file {file_id}: {info.get('name')}")
|
|
142
150
|
|
|
143
|
-
def get_cached_file_info(self, file_id: str) ->
|
|
151
|
+
def get_cached_file_info(self, file_id: str) -> dict[str, Any] | None:
|
|
144
152
|
"""Get cached file info by ID."""
|
|
145
153
|
return self._file_infos.get(file_id)
|
|
146
154
|
|
|
@@ -167,7 +175,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
167
175
|
|
|
168
176
|
self._authenticated = authenticated
|
|
169
177
|
return authenticated
|
|
170
|
-
except Exception
|
|
178
|
+
except Exception:
|
|
171
179
|
logger.exception("[CONNECTOR] OneDrive authentication failed")
|
|
172
180
|
self._authenticated = False
|
|
173
181
|
return False
|
|
@@ -178,7 +186,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
178
186
|
raise RuntimeError("OneDrive OAuth not initialized - missing credentials")
|
|
179
187
|
return self.oauth.create_authorization_url(self.redirect_uri)
|
|
180
188
|
|
|
181
|
-
async def handle_oauth_callback(self, auth_code: str) ->
|
|
189
|
+
async def handle_oauth_callback(self, auth_code: str) -> dict[str, Any]:
|
|
182
190
|
"""Handle OAuth callback."""
|
|
183
191
|
if not self.oauth:
|
|
184
192
|
raise RuntimeError("OneDrive OAuth not initialized - missing credentials")
|
|
@@ -193,39 +201,43 @@ class OneDriveConnector(BaseConnector):
|
|
|
193
201
|
logger.error(f"OAuth callback failed: {e}")
|
|
194
202
|
raise
|
|
195
203
|
|
|
196
|
-
async def _detect_base_url(self) ->
|
|
204
|
+
async def _detect_base_url(self) -> str | None:
|
|
197
205
|
"""Override base class method to detect OneDrive URL"""
|
|
198
206
|
return await self._detect_onedrive_url()
|
|
199
|
-
|
|
200
|
-
async def _detect_onedrive_url(self) ->
|
|
207
|
+
|
|
208
|
+
async def _detect_onedrive_url(self) -> str | None:
|
|
201
209
|
"""Auto-detect OneDrive URL from Microsoft Graph API"""
|
|
202
210
|
logger.info("_detect_onedrive_url: Starting OneDrive URL detection")
|
|
203
211
|
try:
|
|
204
212
|
if not self.oauth:
|
|
205
213
|
logger.warning("_detect_onedrive_url: OAuth not initialized")
|
|
206
214
|
return None
|
|
207
|
-
|
|
215
|
+
|
|
208
216
|
access_token = self.oauth.get_access_token()
|
|
209
|
-
logger.debug(
|
|
210
|
-
|
|
217
|
+
logger.debug(
|
|
218
|
+
f"_detect_onedrive_url: Got access token (length: {len(access_token) if access_token else 0})"
|
|
219
|
+
)
|
|
220
|
+
|
|
211
221
|
headers = {
|
|
212
222
|
"Authorization": f"Bearer {access_token}",
|
|
213
223
|
"Content-Type": "application/json",
|
|
214
224
|
}
|
|
215
|
-
|
|
225
|
+
|
|
216
226
|
async with httpx.AsyncClient() as client:
|
|
217
227
|
# Get user's default drive to extract OneDrive URL
|
|
218
228
|
url = f"{self._graph_base_url}/me/drive"
|
|
219
229
|
logger.info(f"_detect_onedrive_url: Calling Graph API: {url}")
|
|
220
|
-
|
|
230
|
+
|
|
221
231
|
response = await client.get(url, headers=headers, timeout=30.0)
|
|
222
|
-
logger.info(
|
|
223
|
-
|
|
232
|
+
logger.info(
|
|
233
|
+
f"_detect_onedrive_url: Graph API response status: {response.status_code}"
|
|
234
|
+
)
|
|
235
|
+
|
|
224
236
|
if response.status_code == 200:
|
|
225
237
|
data = response.json()
|
|
226
238
|
web_url = data.get("webUrl", "")
|
|
227
239
|
logger.info(f"_detect_onedrive_url: webUrl from response: {web_url}")
|
|
228
|
-
|
|
240
|
+
|
|
229
241
|
# Extract the domain from the webUrl
|
|
230
242
|
# e.g., "https://onedrive.live.com/..." or "https://company-my.sharepoint.com/..."
|
|
231
243
|
if web_url:
|
|
@@ -236,9 +248,11 @@ class OneDriveConnector(BaseConnector):
|
|
|
236
248
|
else:
|
|
237
249
|
logger.warning("_detect_onedrive_url: webUrl is empty in response")
|
|
238
250
|
else:
|
|
239
|
-
logger.warning(
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
logger.warning(
|
|
252
|
+
f"[CONNECTOR] OneDrive detect URL failed, status_code: {response.status_code}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
except Exception:
|
|
242
256
|
logger.exception("[CONNECTOR] OneDrive URL detection failed")
|
|
243
257
|
|
|
244
258
|
return None
|
|
@@ -261,13 +275,15 @@ class OneDriveConnector(BaseConnector):
|
|
|
261
275
|
doc = await self.get_file_content(file_id)
|
|
262
276
|
self.emit(doc)
|
|
263
277
|
except Exception as e:
|
|
264
|
-
logger.error(
|
|
278
|
+
logger.error(
|
|
279
|
+
f"Failed to sync OneDrive file {file_info.get('name', 'unknown')}: {e}"
|
|
280
|
+
)
|
|
265
281
|
continue
|
|
266
282
|
except Exception as e:
|
|
267
283
|
logger.error(f"OneDrive sync_once failed: {e}")
|
|
268
284
|
raise
|
|
269
285
|
|
|
270
|
-
if hasattr(asyncio,
|
|
286
|
+
if hasattr(asyncio, "run"):
|
|
271
287
|
asyncio.run(_async_sync())
|
|
272
288
|
else:
|
|
273
289
|
loop = asyncio.get_event_loop()
|
|
@@ -278,7 +294,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
278
294
|
Set up real-time subscription for file changes.
|
|
279
295
|
NOTE: Change notifications may not be available for personal OneDrive accounts.
|
|
280
296
|
"""
|
|
281
|
-
webhook_url = self.config.get(
|
|
297
|
+
webhook_url = self.config.get("webhook_url")
|
|
282
298
|
if not webhook_url:
|
|
283
299
|
logger.warning("No webhook URL configured, skipping OneDrive subscription setup")
|
|
284
300
|
return "no-webhook-configured"
|
|
@@ -308,7 +324,9 @@ class OneDriveConnector(BaseConnector):
|
|
|
308
324
|
url = f"{self._graph_base_url}/subscriptions"
|
|
309
325
|
|
|
310
326
|
async with httpx.AsyncClient() as client:
|
|
311
|
-
response = await client.post(
|
|
327
|
+
response = await client.post(
|
|
328
|
+
url, json=subscription_data, headers=headers, timeout=30
|
|
329
|
+
)
|
|
312
330
|
response.raise_for_status()
|
|
313
331
|
|
|
314
332
|
result = response.json()
|
|
@@ -328,15 +346,13 @@ class OneDriveConnector(BaseConnector):
|
|
|
328
346
|
def _get_subscription_expiry(self) -> str:
|
|
329
347
|
"""Get subscription expiry time (Graph caps duration; often <= 3 days)."""
|
|
330
348
|
from datetime import datetime, timedelta
|
|
349
|
+
|
|
331
350
|
expiry = datetime.utcnow() + timedelta(days=3)
|
|
332
351
|
return expiry.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
333
352
|
|
|
334
353
|
async def list_files(
|
|
335
|
-
self,
|
|
336
|
-
|
|
337
|
-
max_files: Optional[int] = None,
|
|
338
|
-
**kwargs
|
|
339
|
-
) -> Dict[str, Any]:
|
|
354
|
+
self, page_token: str | None = None, max_files: int | None = None, **kwargs
|
|
355
|
+
) -> dict[str, Any]:
|
|
340
356
|
"""List files from OneDrive using Microsoft Graph."""
|
|
341
357
|
try:
|
|
342
358
|
if not await self.authenticate():
|
|
@@ -346,7 +362,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
346
362
|
if self.cfg.file_ids or self.cfg.folder_ids:
|
|
347
363
|
return await self._list_selected_files()
|
|
348
364
|
|
|
349
|
-
files:
|
|
365
|
+
files: list[dict[str, Any]] = []
|
|
350
366
|
max_files_value = max_files if max_files is not None else 100
|
|
351
367
|
|
|
352
368
|
base_url = f"{self._graph_base_url}/me/drive/root/children"
|
|
@@ -363,23 +379,28 @@ class OneDriveConnector(BaseConnector):
|
|
|
363
379
|
items = data.get("value", [])
|
|
364
380
|
for item in items:
|
|
365
381
|
if item.get("file"): # include files only
|
|
366
|
-
files.append(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
382
|
+
files.append(
|
|
383
|
+
{
|
|
384
|
+
"id": item.get("id", ""),
|
|
385
|
+
"name": item.get("name", ""),
|
|
386
|
+
"path": f"/drive/items/{item.get('id')}",
|
|
387
|
+
"size": int(item.get("size", 0)),
|
|
388
|
+
"modified": item.get("lastModifiedDateTime"),
|
|
389
|
+
"created": item.get("createdDateTime"),
|
|
390
|
+
"mime_type": item.get("file", {}).get(
|
|
391
|
+
"mimeType", self._get_mime_type(item.get("name", ""))
|
|
392
|
+
),
|
|
393
|
+
"url": item.get("webUrl", ""),
|
|
394
|
+
"download_url": item.get("@microsoft.graph.downloadUrl"),
|
|
395
|
+
}
|
|
396
|
+
)
|
|
377
397
|
|
|
378
398
|
# Next page
|
|
379
399
|
next_page_token = None
|
|
380
400
|
next_link = data.get("@odata.nextLink")
|
|
381
401
|
if next_link:
|
|
382
|
-
from urllib.parse import
|
|
402
|
+
from urllib.parse import parse_qs, urlparse
|
|
403
|
+
|
|
383
404
|
parsed = urlparse(next_link)
|
|
384
405
|
query_params = parse_qs(parsed.query)
|
|
385
406
|
if "$skiptoken" in query_params:
|
|
@@ -391,7 +412,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
391
412
|
logger.error(f"Failed to list OneDrive files: {e}")
|
|
392
413
|
return {"files": [], "next_page_token": None}
|
|
393
414
|
|
|
394
|
-
async def _extract_onedrive_acl(self, file_id: str, file_metadata:
|
|
415
|
+
async def _extract_onedrive_acl(self, file_id: str, file_metadata: dict) -> DocumentACL:
|
|
395
416
|
"""
|
|
396
417
|
Extract ACL from OneDrive item.
|
|
397
418
|
|
|
@@ -406,8 +427,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
406
427
|
"""
|
|
407
428
|
try:
|
|
408
429
|
# Get access token
|
|
409
|
-
|
|
410
|
-
access_token = token_data.get("access_token")
|
|
430
|
+
access_token = self.oauth.get_access_token()
|
|
411
431
|
|
|
412
432
|
if not access_token:
|
|
413
433
|
logger.warning(f"No access token available for ACL extraction: {file_id}")
|
|
@@ -419,8 +439,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
419
439
|
# Fetch permissions
|
|
420
440
|
async with httpx.AsyncClient() as client:
|
|
421
441
|
response = await client.get(
|
|
422
|
-
permissions_url,
|
|
423
|
-
headers={"Authorization": f"Bearer {access_token}"}
|
|
442
|
+
permissions_url, headers={"Authorization": f"Bearer {access_token}"}
|
|
424
443
|
)
|
|
425
444
|
|
|
426
445
|
if response.status_code != 200:
|
|
@@ -487,13 +506,11 @@ class OneDriveConnector(BaseConnector):
|
|
|
487
506
|
if cached_info and cached_info.get("downloadUrl"):
|
|
488
507
|
logger.info(f"Using cached download URL for file {file_id}")
|
|
489
508
|
content = await self._download_file_from_url(cached_info["downloadUrl"])
|
|
490
|
-
|
|
509
|
+
|
|
491
510
|
acl = DocumentACL(
|
|
492
511
|
owner="",
|
|
493
|
-
user_permissions={},
|
|
494
|
-
group_permissions={},
|
|
495
512
|
)
|
|
496
|
-
|
|
513
|
+
|
|
497
514
|
return ConnectorDocument(
|
|
498
515
|
id=file_id,
|
|
499
516
|
filename=cached_info.get("name", "Unknown"),
|
|
@@ -513,13 +530,15 @@ class OneDriveConnector(BaseConnector):
|
|
|
513
530
|
file_metadata = await self._get_file_metadata_by_id(file_id)
|
|
514
531
|
if not file_metadata:
|
|
515
532
|
# Last-resort: try shares endpoint download directly if this is a sharing ID
|
|
516
|
-
if
|
|
517
|
-
logger.info(
|
|
533
|
+
if "!" in file_id and file_id.split("!", 1)[1].startswith("s"):
|
|
534
|
+
logger.info(
|
|
535
|
+
f"No metadata for sharing ID {file_id}, attempting direct shares download"
|
|
536
|
+
)
|
|
518
537
|
token = self.oauth.get_access_token()
|
|
519
538
|
headers = {"Authorization": f"Bearer {token}"}
|
|
520
539
|
shares_content = await self._download_via_shares_endpoint(file_id, headers)
|
|
521
540
|
if shares_content is not None:
|
|
522
|
-
acl = DocumentACL(owner=""
|
|
541
|
+
acl = DocumentACL(owner="")
|
|
523
542
|
return ConnectorDocument(
|
|
524
543
|
id=file_id,
|
|
525
544
|
filename="Unknown",
|
|
@@ -564,9 +583,9 @@ class OneDriveConnector(BaseConnector):
|
|
|
564
583
|
logger.error(f"Failed to get OneDrive file content {file_id}: {e}")
|
|
565
584
|
raise
|
|
566
585
|
|
|
567
|
-
async def _get_file_metadata_by_id(self, file_id: str) ->
|
|
586
|
+
async def _get_file_metadata_by_id(self, file_id: str) -> dict[str, Any] | None:
|
|
568
587
|
"""Get file metadata by ID using Graph API.
|
|
569
|
-
|
|
588
|
+
|
|
570
589
|
Handles multiple ID formats:
|
|
571
590
|
- Standard item ID: uses /me/drive/items/{id}
|
|
572
591
|
- Personal OneDrive format (driveId!itemId): uses /drives/{driveId}/items/{itemId}
|
|
@@ -575,7 +594,7 @@ class OneDriveConnector(BaseConnector):
|
|
|
575
594
|
try:
|
|
576
595
|
# Try different endpoints based on ID format
|
|
577
596
|
item = await self._fetch_item_metadata(file_id)
|
|
578
|
-
|
|
597
|
+
|
|
579
598
|
if not item:
|
|
580
599
|
return None
|
|
581
600
|
|
|
@@ -595,7 +614,9 @@ class OneDriveConnector(BaseConnector):
|
|
|
595
614
|
"size": int(item.get("size", 0)),
|
|
596
615
|
"modified": item.get("lastModifiedDateTime"),
|
|
597
616
|
"created": item.get("createdDateTime"),
|
|
598
|
-
"mime_type": item.get("file", {}).get(
|
|
617
|
+
"mime_type": item.get("file", {}).get(
|
|
618
|
+
"mimeType", self._get_mime_type(item.get("name", ""))
|
|
619
|
+
),
|
|
599
620
|
"url": item.get("webUrl", ""),
|
|
600
621
|
"download_url": item.get("@microsoft.graph.downloadUrl"),
|
|
601
622
|
"isFolder": False,
|
|
@@ -607,69 +628,79 @@ class OneDriveConnector(BaseConnector):
|
|
|
607
628
|
logger.error(f"Failed to get file metadata for {file_id}: {e}")
|
|
608
629
|
return None
|
|
609
630
|
|
|
610
|
-
async def _fetch_item_metadata(self, file_id: str) ->
|
|
631
|
+
async def _fetch_item_metadata(self, file_id: str) -> dict[str, Any] | None:
|
|
611
632
|
"""Fetch item metadata, trying multiple endpoints for different ID formats."""
|
|
612
633
|
import base64
|
|
634
|
+
|
|
613
635
|
params = dict(self._default_params)
|
|
614
|
-
|
|
636
|
+
|
|
615
637
|
# Check if ID contains '!' which indicates driveId!itemId format
|
|
616
|
-
if
|
|
617
|
-
parts = file_id.split(
|
|
638
|
+
if "!" in file_id:
|
|
639
|
+
parts = file_id.split("!", 1)
|
|
618
640
|
if len(parts) == 2:
|
|
619
641
|
drive_id = parts[0]
|
|
620
642
|
item_id = parts[1]
|
|
621
|
-
|
|
643
|
+
|
|
622
644
|
# Handle sharing IDs (item ID starts with 's')
|
|
623
|
-
if item_id.startswith(
|
|
645
|
+
if item_id.startswith("s"):
|
|
624
646
|
logger.info(f"Detected sharing ID format for {file_id}")
|
|
625
|
-
|
|
647
|
+
|
|
626
648
|
# Try multiple encoding approaches for the shares endpoint
|
|
627
649
|
share_encodings = [
|
|
628
650
|
# Approach 1: Encode the full ID with "u!" prefix
|
|
629
|
-
base64.urlsafe_b64encode(f"u!{file_id}".encode()).decode().rstrip(
|
|
651
|
+
base64.urlsafe_b64encode(f"u!{file_id}".encode()).decode().rstrip("="),
|
|
630
652
|
# Approach 2: Encode just the share token with "s!" prefix
|
|
631
|
-
base64.urlsafe_b64encode(f"s!{item_id}".encode()).decode().rstrip(
|
|
653
|
+
base64.urlsafe_b64encode(f"s!{item_id}".encode()).decode().rstrip("="),
|
|
632
654
|
# Approach 3: Encode the ID directly
|
|
633
|
-
base64.urlsafe_b64encode(file_id.encode()).decode().rstrip(
|
|
655
|
+
base64.urlsafe_b64encode(file_id.encode()).decode().rstrip("="),
|
|
634
656
|
# Approach 4: Use the ID as-is (some APIs accept this)
|
|
635
657
|
f"u!{file_id}",
|
|
636
658
|
]
|
|
637
|
-
|
|
659
|
+
|
|
638
660
|
for i, encoded in enumerate(share_encodings):
|
|
639
661
|
try:
|
|
640
662
|
url = f"{self._graph_base_url}/shares/{encoded}/driveItem"
|
|
641
|
-
logger.debug(f"Trying shares endpoint approach {i+1}: {url}")
|
|
663
|
+
logger.debug(f"Trying shares endpoint approach {i + 1}: {url}")
|
|
642
664
|
response = await self._make_graph_request(url, params=params)
|
|
643
665
|
if response.status_code == 200:
|
|
644
|
-
logger.info(f"Shares endpoint approach {i+1} succeeded")
|
|
666
|
+
logger.info(f"Shares endpoint approach {i + 1} succeeded")
|
|
645
667
|
return response.json()
|
|
646
668
|
else:
|
|
647
|
-
logger.debug(
|
|
669
|
+
logger.debug(
|
|
670
|
+
f"Shares approach {i + 1} failed with status {response.status_code}"
|
|
671
|
+
)
|
|
648
672
|
except Exception as e:
|
|
649
|
-
logger.debug(f"Shares approach {i+1} failed: {e}")
|
|
650
|
-
|
|
651
|
-
# Try: /drives/{driveId}/items/{
|
|
652
|
-
logger.info(f"Trying drives endpoint: /drives/{drive_id}/items/{
|
|
673
|
+
logger.debug(f"Shares approach {i + 1} failed: {e}")
|
|
674
|
+
|
|
675
|
+
# Try: /drives/{driveId}/items/{file_id} with full file ID
|
|
676
|
+
logger.info(f"Trying drives endpoint: /drives/{drive_id}/items/{file_id}")
|
|
653
677
|
try:
|
|
654
|
-
url = f"{self._graph_base_url}/drives/{drive_id}/items/{
|
|
678
|
+
url = f"{self._graph_base_url}/drives/{drive_id}/items/{file_id}"
|
|
655
679
|
response = await self._make_graph_request(url, params=params)
|
|
656
680
|
if response.status_code == 200:
|
|
657
681
|
return response.json()
|
|
682
|
+
else:
|
|
683
|
+
logger.warning(
|
|
684
|
+
f"Drives endpoint failed with status {response.status_code}: {response.text}"
|
|
685
|
+
)
|
|
658
686
|
except Exception as e:
|
|
659
|
-
logger.debug(f"Drives endpoint
|
|
660
|
-
|
|
661
|
-
# Try: /drives/{driveId}/items/{
|
|
662
|
-
if item_id.startswith(
|
|
687
|
+
logger.debug(f"Drives endpoint exception: {e}")
|
|
688
|
+
|
|
689
|
+
# Try: /drives/{driveId}/items/{clean_file_id} without 's' prefix
|
|
690
|
+
if item_id.startswith("s"):
|
|
663
691
|
clean_item_id = item_id[1:] # Remove 's' prefix
|
|
664
|
-
|
|
692
|
+
clean_file_id = f"{drive_id}!{clean_item_id}"
|
|
693
|
+
logger.info(
|
|
694
|
+
f"Trying drives endpoint without 's' prefix: /drives/{drive_id}/items/{clean_file_id}"
|
|
695
|
+
)
|
|
665
696
|
try:
|
|
666
|
-
url = f"{self._graph_base_url}/drives/{drive_id}/items/{
|
|
697
|
+
url = f"{self._graph_base_url}/drives/{drive_id}/items/{clean_file_id}"
|
|
667
698
|
response = await self._make_graph_request(url, params=params)
|
|
668
699
|
if response.status_code == 200:
|
|
669
700
|
return response.json()
|
|
670
701
|
except Exception as e:
|
|
671
702
|
logger.debug(f"Drives endpoint (no prefix) failed: {e}")
|
|
672
|
-
|
|
703
|
+
|
|
673
704
|
# Try: /me/drive/items/{full_id} as fallback
|
|
674
705
|
logger.info(f"Trying standard endpoint: /me/drive/items/{file_id}")
|
|
675
706
|
try:
|
|
@@ -677,42 +708,46 @@ class OneDriveConnector(BaseConnector):
|
|
|
677
708
|
response = await self._make_graph_request(url, params=params)
|
|
678
709
|
if response.status_code == 200:
|
|
679
710
|
return response.json()
|
|
711
|
+
else:
|
|
712
|
+
logger.warning(
|
|
713
|
+
f"Standard endpoint failed with status {response.status_code}: {response.text}"
|
|
714
|
+
)
|
|
680
715
|
except Exception as e:
|
|
681
|
-
logger.debug(f"Standard endpoint
|
|
716
|
+
logger.debug(f"Standard endpoint exception: {e}")
|
|
682
717
|
else:
|
|
683
718
|
# Standard item ID without '!'
|
|
684
719
|
url = f"{self._graph_base_url}/me/drive/items/{file_id}"
|
|
685
720
|
response = await self._make_graph_request(url, params=params)
|
|
686
721
|
if response.status_code == 200:
|
|
687
722
|
return response.json()
|
|
688
|
-
|
|
723
|
+
|
|
689
724
|
logger.error(f"All endpoints failed for file_id: {file_id}")
|
|
690
725
|
return None
|
|
691
726
|
|
|
692
727
|
async def _download_file_content(self, file_id: str) -> bytes:
|
|
693
728
|
"""Download file content by file ID using Graph API.
|
|
694
|
-
|
|
729
|
+
|
|
695
730
|
Handles multiple ID formats like _get_file_metadata_by_id.
|
|
696
731
|
"""
|
|
697
732
|
try:
|
|
698
733
|
token = self.oauth.get_access_token()
|
|
699
734
|
headers = {"Authorization": f"Bearer {token}"}
|
|
700
|
-
|
|
735
|
+
|
|
701
736
|
# Build URL based on ID format
|
|
702
|
-
if
|
|
703
|
-
parts = file_id.split(
|
|
737
|
+
if "!" in file_id:
|
|
738
|
+
parts = file_id.split("!", 1)
|
|
704
739
|
if len(parts) == 2:
|
|
705
740
|
drive_id = parts[0]
|
|
706
741
|
item_id = parts[1]
|
|
707
|
-
|
|
742
|
+
|
|
708
743
|
# If this looks like a sharing ID (starts with 's'), try shares endpoint first
|
|
709
|
-
if item_id.startswith(
|
|
744
|
+
if item_id.startswith("s"):
|
|
710
745
|
content = await self._download_via_shares_endpoint(file_id, headers)
|
|
711
746
|
if content is not None:
|
|
712
747
|
return content
|
|
713
748
|
|
|
714
|
-
# Try drives endpoint for driveId!itemId format
|
|
715
|
-
url = f"{self._graph_base_url}/drives/{drive_id}/items/{
|
|
749
|
+
# Try drives endpoint for driveId!itemId format
|
|
750
|
+
url = f"{self._graph_base_url}/drives/{drive_id}/items/{file_id}/content"
|
|
716
751
|
logger.info(f"Downloading via drives endpoint: {url}")
|
|
717
752
|
else:
|
|
718
753
|
url = f"{self._graph_base_url}/me/drive/items/{file_id}/content"
|
|
@@ -728,31 +763,37 @@ class OneDriveConnector(BaseConnector):
|
|
|
728
763
|
logger.error(f"Failed to download file content for {file_id}: {e}")
|
|
729
764
|
raise
|
|
730
765
|
|
|
731
|
-
async def _download_via_shares_endpoint(
|
|
766
|
+
async def _download_via_shares_endpoint(
|
|
767
|
+
self, file_id: str, headers: dict[str, str]
|
|
768
|
+
) -> bytes | None:
|
|
732
769
|
"""
|
|
733
770
|
Attempt to download content using the Graph /shares endpoint for sharing IDs.
|
|
734
771
|
"""
|
|
735
772
|
import base64
|
|
736
773
|
|
|
737
774
|
share_encodings = [
|
|
738
|
-
base64.urlsafe_b64encode(f"u!{file_id}".encode()).decode().rstrip(
|
|
739
|
-
base64.urlsafe_b64encode(f"s!{file_id}".encode()).decode().rstrip(
|
|
740
|
-
base64.urlsafe_b64encode(file_id.encode()).decode().rstrip(
|
|
775
|
+
base64.urlsafe_b64encode(f"u!{file_id}".encode()).decode().rstrip("="),
|
|
776
|
+
base64.urlsafe_b64encode(f"s!{file_id}".encode()).decode().rstrip("="),
|
|
777
|
+
base64.urlsafe_b64encode(file_id.encode()).decode().rstrip("="),
|
|
741
778
|
f"u!{file_id}",
|
|
742
779
|
]
|
|
743
780
|
|
|
744
781
|
for i, encoded in enumerate(share_encodings):
|
|
745
782
|
try:
|
|
746
783
|
url = f"{self._graph_base_url}/shares/{encoded}/driveItem/content"
|
|
747
|
-
logger.info(f"Attempting shares download (approach {i+1}): {url}")
|
|
784
|
+
logger.info(f"Attempting shares download (approach {i + 1}): {url}")
|
|
748
785
|
async with httpx.AsyncClient() as client:
|
|
749
|
-
response = await client.get(
|
|
786
|
+
response = await client.get(
|
|
787
|
+
url, headers=headers, timeout=60, follow_redirects=True
|
|
788
|
+
)
|
|
750
789
|
if response.status_code == 200:
|
|
751
790
|
return response.content
|
|
752
791
|
else:
|
|
753
|
-
logger.debug(
|
|
792
|
+
logger.debug(
|
|
793
|
+
f"Shares download approach {i + 1} failed with status {response.status_code}"
|
|
794
|
+
)
|
|
754
795
|
except Exception as e:
|
|
755
|
-
logger.debug(f"Shares download approach {i+1} failed: {e}")
|
|
796
|
+
logger.debug(f"Shares download approach {i + 1} failed: {e}")
|
|
756
797
|
|
|
757
798
|
return None
|
|
758
799
|
|
|
@@ -767,20 +808,25 @@ class OneDriveConnector(BaseConnector):
|
|
|
767
808
|
logger.error(f"Failed to download from URL {download_url}: {e}")
|
|
768
809
|
raise
|
|
769
810
|
|
|
770
|
-
def _parse_graph_date(self, date_str:
|
|
811
|
+
def _parse_graph_date(self, date_str: str | None) -> datetime:
|
|
771
812
|
"""Parse Microsoft Graph date string to datetime."""
|
|
772
813
|
if not date_str:
|
|
773
814
|
return datetime.now()
|
|
774
815
|
try:
|
|
775
|
-
if date_str.endswith(
|
|
816
|
+
if date_str.endswith("Z"):
|
|
776
817
|
return datetime.fromisoformat(date_str[:-1]).replace(tzinfo=None)
|
|
777
818
|
else:
|
|
778
|
-
return datetime.fromisoformat(date_str.replace(
|
|
819
|
+
return datetime.fromisoformat(date_str.replace("T", " "))
|
|
779
820
|
except (ValueError, AttributeError):
|
|
780
821
|
return datetime.now()
|
|
781
822
|
|
|
782
|
-
async def _make_graph_request(
|
|
783
|
-
|
|
823
|
+
async def _make_graph_request(
|
|
824
|
+
self,
|
|
825
|
+
url: str,
|
|
826
|
+
method: str = "GET",
|
|
827
|
+
data: dict | None = None,
|
|
828
|
+
params: dict | None = None,
|
|
829
|
+
) -> httpx.Response:
|
|
784
830
|
"""Make authenticated API request to Microsoft Graph."""
|
|
785
831
|
token = self.oauth.get_access_token()
|
|
786
832
|
headers = {
|
|
@@ -801,25 +847,25 @@ class OneDriveConnector(BaseConnector):
|
|
|
801
847
|
response.raise_for_status()
|
|
802
848
|
return response
|
|
803
849
|
|
|
804
|
-
async def _list_selected_files(self) ->
|
|
850
|
+
async def _list_selected_files(self) -> dict[str, Any]:
|
|
805
851
|
"""List only selected files/folders (selective sync)."""
|
|
806
|
-
files:
|
|
807
|
-
|
|
852
|
+
files: list[dict[str, Any]] = []
|
|
853
|
+
|
|
808
854
|
# Process selected file IDs
|
|
809
855
|
if self.cfg.file_ids:
|
|
810
856
|
for file_id in self.cfg.file_ids:
|
|
811
857
|
try:
|
|
812
858
|
file_meta = await self._get_file_metadata_by_id(file_id)
|
|
813
|
-
if file_meta and not file_meta.get(
|
|
859
|
+
if file_meta and not file_meta.get("isFolder", False):
|
|
814
860
|
files.append(file_meta)
|
|
815
|
-
elif file_meta and file_meta.get(
|
|
861
|
+
elif file_meta and file_meta.get("isFolder", False):
|
|
816
862
|
# If it's a folder, expand its contents
|
|
817
863
|
folder_files = await self._list_folder_contents(file_id)
|
|
818
864
|
files.extend(folder_files)
|
|
819
865
|
except Exception as e:
|
|
820
866
|
logger.warning(f"Failed to get file {file_id}: {e}")
|
|
821
867
|
continue
|
|
822
|
-
|
|
868
|
+
|
|
823
869
|
# Process selected folder IDs
|
|
824
870
|
if self.cfg.folder_ids:
|
|
825
871
|
for folder_id in self.cfg.folder_ids:
|
|
@@ -829,20 +875,20 @@ class OneDriveConnector(BaseConnector):
|
|
|
829
875
|
except Exception as e:
|
|
830
876
|
logger.warning(f"Failed to list folder {folder_id}: {e}")
|
|
831
877
|
continue
|
|
832
|
-
|
|
878
|
+
|
|
833
879
|
return {"files": files, "next_page_token": None}
|
|
834
|
-
|
|
835
|
-
async def _list_folder_contents(self, folder_id: str) ->
|
|
880
|
+
|
|
881
|
+
async def _list_folder_contents(self, folder_id: str) -> list[dict[str, Any]]:
|
|
836
882
|
"""List all files in a folder recursively."""
|
|
837
|
-
files:
|
|
838
|
-
|
|
883
|
+
files: list[dict[str, Any]] = []
|
|
884
|
+
|
|
839
885
|
try:
|
|
840
886
|
url = f"{self._graph_base_url}/me/drive/items/{folder_id}/children"
|
|
841
887
|
params = dict(self._default_params)
|
|
842
|
-
|
|
888
|
+
|
|
843
889
|
response = await self._make_graph_request(url, params=params)
|
|
844
890
|
data = response.json()
|
|
845
|
-
|
|
891
|
+
|
|
846
892
|
items = data.get("value", [])
|
|
847
893
|
for item in items:
|
|
848
894
|
if item.get("file"): # It's a file
|
|
@@ -854,35 +900,37 @@ class OneDriveConnector(BaseConnector):
|
|
|
854
900
|
files.extend(subfolder_files)
|
|
855
901
|
except Exception as e:
|
|
856
902
|
logger.error(f"Failed to list folder contents for {folder_id}: {e}")
|
|
857
|
-
|
|
903
|
+
|
|
858
904
|
return files
|
|
859
905
|
|
|
860
906
|
def _get_mime_type(self, filename: str) -> str:
|
|
861
907
|
"""Get MIME type based on file extension."""
|
|
862
908
|
import mimetypes
|
|
909
|
+
|
|
863
910
|
mime_type, _ = mimetypes.guess_type(filename)
|
|
864
911
|
return mime_type or "application/octet-stream"
|
|
865
912
|
|
|
866
913
|
# Webhook methods - BaseConnector interface
|
|
867
|
-
def handle_webhook_validation(
|
|
868
|
-
|
|
869
|
-
|
|
914
|
+
def handle_webhook_validation(
|
|
915
|
+
self, request_method: str, headers: dict[str, str], query_params: dict[str, str]
|
|
916
|
+
) -> str | None:
|
|
870
917
|
"""Handle webhook validation (Graph API specific)."""
|
|
871
918
|
if request_method == "POST" and "validationToken" in query_params:
|
|
872
919
|
return query_params["validationToken"]
|
|
873
920
|
return None
|
|
874
921
|
|
|
875
|
-
def extract_webhook_channel_id(
|
|
876
|
-
|
|
922
|
+
def extract_webhook_channel_id(
|
|
923
|
+
self, payload: dict[str, Any], headers: dict[str, str]
|
|
924
|
+
) -> str | None:
|
|
877
925
|
"""Extract channel/subscription ID from webhook payload."""
|
|
878
926
|
notifications = payload.get("value", [])
|
|
879
927
|
if notifications:
|
|
880
928
|
return notifications[0].get("subscriptionId")
|
|
881
929
|
return None
|
|
882
930
|
|
|
883
|
-
async def handle_webhook(self, payload:
|
|
931
|
+
async def handle_webhook(self, payload: dict[str, Any]) -> list[str]:
|
|
884
932
|
"""Handle webhook notification and return affected file IDs."""
|
|
885
|
-
affected_files:
|
|
933
|
+
affected_files: list[str] = []
|
|
886
934
|
notifications = payload.get("value", [])
|
|
887
935
|
for notification in notifications:
|
|
888
936
|
resource = notification.get("resource")
|
|
@@ -914,7 +962,9 @@ class OneDriveConnector(BaseConnector):
|
|
|
914
962
|
logger.info(f"OneDrive subscription {subscription_id} cleaned up successfully")
|
|
915
963
|
return True
|
|
916
964
|
else:
|
|
917
|
-
logger.warning(
|
|
965
|
+
logger.warning(
|
|
966
|
+
f"Unexpected response cleaning up subscription: {response.status_code}"
|
|
967
|
+
)
|
|
918
968
|
return False
|
|
919
969
|
|
|
920
970
|
except Exception as e:
|
connectors/onedrive/oauth.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import json
|
|
3
2
|
import logging
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
6
5
|
|
|
7
6
|
import msal
|
|
8
7
|
|
|
@@ -25,16 +24,9 @@ class OneDriveOAuth:
|
|
|
25
24
|
"Files.Read",
|
|
26
25
|
"Files.Read.All", # Access all files user can access
|
|
27
26
|
"Files.Read.Selected",
|
|
28
|
-
"
|
|
29
|
-
"offline_access"
|
|
30
|
-
]
|
|
31
|
-
RESOURCE_SCOPES = [
|
|
32
|
-
"User.Read",
|
|
33
|
-
"Files.Read",
|
|
34
|
-
"Files.Read.All",
|
|
35
|
-
"Files.Read.Selected",
|
|
36
|
-
"Sites.Read.All"
|
|
27
|
+
"offline_access",
|
|
37
28
|
]
|
|
29
|
+
RESOURCE_SCOPES = ["User.Read", "Files.Read", "Files.Read.All", "Files.Read.Selected"]
|
|
38
30
|
SCOPES = AUTH_SCOPES # Backward-compat alias if something references .SCOPES
|
|
39
31
|
|
|
40
32
|
# Kept for reference; MSAL derives endpoints from `authority`
|
|
@@ -67,7 +59,7 @@ class OneDriveOAuth:
|
|
|
67
59
|
self.authority = authority
|
|
68
60
|
self.allow_json_refresh = allow_json_refresh
|
|
69
61
|
self.token_cache = msal.SerializableTokenCache()
|
|
70
|
-
self._current_account = None
|
|
62
|
+
self._current_account: dict[str, Any] | None = None
|
|
71
63
|
|
|
72
64
|
# Initialize MSAL Confidential Client
|
|
73
65
|
self.app = msal.ConfidentialClientApplication(
|
|
@@ -81,10 +73,10 @@ class OneDriveOAuth:
|
|
|
81
73
|
"""Load existing credentials from token file (async)."""
|
|
82
74
|
try:
|
|
83
75
|
from utils.encryption import read_encrypted_file
|
|
84
|
-
|
|
76
|
+
|
|
85
77
|
logger.debug(f"OneDrive OAuth loading credentials from: {self.token_file}")
|
|
86
78
|
cache_data, needs_upgrade = await read_encrypted_file(self.token_file)
|
|
87
|
-
|
|
79
|
+
|
|
88
80
|
if cache_data and cache_data.strip():
|
|
89
81
|
# 1) Try legacy flat JSON first
|
|
90
82
|
try:
|
|
@@ -112,15 +104,20 @@ class OneDriveOAuth:
|
|
|
112
104
|
accounts = self.app.get_accounts()
|
|
113
105
|
logger.debug(f"Found {len(accounts)} accounts in MSAL cache")
|
|
114
106
|
if accounts:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
account = accounts[0]
|
|
108
|
+
self._current_account = account
|
|
109
|
+
logger.debug(f"Set current account: {account.get('username', 'no username')}")
|
|
110
|
+
|
|
118
111
|
if needs_upgrade:
|
|
119
112
|
await self.save_cache()
|
|
120
113
|
|
|
121
114
|
# Use RESOURCE_SCOPES (no reserved scopes) for silent acquisition
|
|
122
|
-
result = self.app.acquire_token_silent(
|
|
123
|
-
|
|
115
|
+
result = self.app.acquire_token_silent(
|
|
116
|
+
self.RESOURCE_SCOPES, account=self._current_account
|
|
117
|
+
)
|
|
118
|
+
logger.debug(
|
|
119
|
+
f"Silent token acquisition result keys: {list(result.keys()) if result else 'None'}"
|
|
120
|
+
)
|
|
124
121
|
if result and "access_token" in result:
|
|
125
122
|
logger.debug("Silent token acquisition successful")
|
|
126
123
|
if getattr(self.token_cache, "has_state_changed", False):
|
|
@@ -134,7 +131,7 @@ class OneDriveOAuth:
|
|
|
134
131
|
|
|
135
132
|
return False
|
|
136
133
|
|
|
137
|
-
except Exception
|
|
134
|
+
except Exception:
|
|
138
135
|
logger.exception("[CONNECTOR] OneDrive credential load failed")
|
|
139
136
|
return False
|
|
140
137
|
|
|
@@ -166,15 +163,24 @@ class OneDriveOAuth:
|
|
|
166
163
|
accounts = self.app.get_accounts()
|
|
167
164
|
logger.debug(f"After refresh, found {len(accounts)} accounts")
|
|
168
165
|
if accounts:
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
account = accounts[0]
|
|
167
|
+
self._current_account = account
|
|
168
|
+
logger.debug(
|
|
169
|
+
f"Set current account after refresh: {account.get('username', 'no username')}"
|
|
170
|
+
)
|
|
171
171
|
return True
|
|
172
172
|
|
|
173
173
|
# Error handling
|
|
174
|
-
err = (
|
|
174
|
+
err = (
|
|
175
|
+
(result or {}).get("error_description")
|
|
176
|
+
or (result or {}).get("error")
|
|
177
|
+
or "Unknown error"
|
|
178
|
+
)
|
|
175
179
|
logger.error(f"Refresh token failed: {err}")
|
|
176
180
|
|
|
177
|
-
if any(
|
|
181
|
+
if any(
|
|
182
|
+
code in err for code in ("AADSTS70000", "invalid_grant", "interaction_required")
|
|
183
|
+
):
|
|
178
184
|
logger.warning(
|
|
179
185
|
"Refresh denied due to unauthorized/expired scopes or invalid grant. "
|
|
180
186
|
"Delete the token file and perform interactive sign-in with correct scopes."
|
|
@@ -182,7 +188,7 @@ class OneDriveOAuth:
|
|
|
182
188
|
|
|
183
189
|
return False
|
|
184
190
|
|
|
185
|
-
except Exception
|
|
191
|
+
except Exception:
|
|
186
192
|
logger.exception("[CONNECTOR] OneDrive JSON token refresh failed")
|
|
187
193
|
return False
|
|
188
194
|
|
|
@@ -197,18 +203,19 @@ class OneDriveOAuth:
|
|
|
197
203
|
cache_data = self.token_cache.serialize()
|
|
198
204
|
if cache_data:
|
|
199
205
|
from utils.encryption import write_encrypted_file
|
|
206
|
+
|
|
200
207
|
await write_encrypted_file(self.token_file, cache_data)
|
|
201
|
-
|
|
208
|
+
|
|
202
209
|
logger.debug(f"Token cache saved to {self.token_file}")
|
|
203
210
|
except Exception as e:
|
|
204
211
|
logger.error(f"Failed to save token cache: {e}")
|
|
205
212
|
|
|
206
|
-
def create_authorization_url(self, redirect_uri: str, state:
|
|
213
|
+
def create_authorization_url(self, redirect_uri: str, state: str | None = None) -> str:
|
|
207
214
|
"""Create authorization URL for OAuth flow."""
|
|
208
215
|
# Store redirect URI for later use in callback
|
|
209
216
|
self._redirect_uri = redirect_uri
|
|
210
217
|
|
|
211
|
-
kwargs:
|
|
218
|
+
kwargs: dict[str, Any] = {
|
|
212
219
|
# Interactive auth includes offline_access
|
|
213
220
|
"scopes": self.AUTH_SCOPES,
|
|
214
221
|
"redirect_uri": redirect_uri,
|
|
@@ -231,7 +238,7 @@ class OneDriveOAuth:
|
|
|
231
238
|
try:
|
|
232
239
|
result = self.app.acquire_token_by_authorization_code(
|
|
233
240
|
authorization_code,
|
|
234
|
-
scopes=self.AUTH_SCOPES,
|
|
241
|
+
scopes=self.AUTH_SCOPES, # same as authorize step
|
|
235
242
|
redirect_uri=redirect_uri,
|
|
236
243
|
)
|
|
237
244
|
|
|
@@ -244,7 +251,11 @@ class OneDriveOAuth:
|
|
|
244
251
|
logger.info("OneDrive OAuth authorization successful")
|
|
245
252
|
return True
|
|
246
253
|
|
|
247
|
-
error_msg = (
|
|
254
|
+
error_msg = (
|
|
255
|
+
(result or {}).get("error_description")
|
|
256
|
+
or (result or {}).get("error")
|
|
257
|
+
or "Unknown error"
|
|
258
|
+
)
|
|
248
259
|
logger.error(f"OneDrive OAuth authorization failed: {error_msg}")
|
|
249
260
|
return False
|
|
250
261
|
|
|
@@ -262,29 +273,47 @@ class OneDriveOAuth:
|
|
|
262
273
|
load_result = await self.load_credentials()
|
|
263
274
|
logger.info(f"OneDrive is_authenticated: load_credentials returned {load_result}")
|
|
264
275
|
|
|
265
|
-
logger.info(
|
|
266
|
-
|
|
276
|
+
logger.info(
|
|
277
|
+
f"OneDrive is_authenticated: current_account={self._current_account is not None}"
|
|
278
|
+
)
|
|
279
|
+
|
|
267
280
|
# Try to get a token (MSAL will refresh if needed)
|
|
268
281
|
if self._current_account:
|
|
269
|
-
logger.info(
|
|
282
|
+
logger.info(
|
|
283
|
+
f"OneDrive is_authenticated: Trying acquire_token_silent with account {self._current_account.get('username', 'unknown')}"
|
|
284
|
+
)
|
|
270
285
|
logger.info(f"OneDrive is_authenticated: RESOURCE_SCOPES={self.RESOURCE_SCOPES}")
|
|
271
|
-
result = self.app.acquire_token_silent(
|
|
286
|
+
result = self.app.acquire_token_silent(
|
|
287
|
+
self.RESOURCE_SCOPES, account=self._current_account
|
|
288
|
+
)
|
|
272
289
|
if result and "access_token" in result:
|
|
273
|
-
logger.info(
|
|
290
|
+
logger.info(
|
|
291
|
+
"OneDrive is_authenticated: Successfully acquired token with account"
|
|
292
|
+
)
|
|
274
293
|
if getattr(self.token_cache, "has_state_changed", False):
|
|
275
294
|
await self.save_cache()
|
|
276
295
|
return True
|
|
277
296
|
else:
|
|
278
|
-
error_msg = (
|
|
279
|
-
|
|
297
|
+
error_msg = (
|
|
298
|
+
(result or {}).get("error")
|
|
299
|
+
or (result or {}).get("error_description")
|
|
300
|
+
or "No result returned"
|
|
301
|
+
)
|
|
302
|
+
logger.warning(
|
|
303
|
+
f"OneDrive is_authenticated: Token acquisition failed for current account: {error_msg}"
|
|
304
|
+
)
|
|
280
305
|
logger.warning(f"OneDrive is_authenticated: Full result: {result}")
|
|
281
306
|
|
|
282
307
|
# Fallback: try without specific account
|
|
283
|
-
logger.info(
|
|
308
|
+
logger.info(
|
|
309
|
+
"OneDrive is_authenticated: Fallback - trying acquire_token_silent without account"
|
|
310
|
+
)
|
|
284
311
|
result = self.app.acquire_token_silent(self.RESOURCE_SCOPES, account=None)
|
|
285
312
|
if result and "access_token" in result:
|
|
286
313
|
accounts = self.app.get_accounts()
|
|
287
|
-
logger.info(
|
|
314
|
+
logger.info(
|
|
315
|
+
f"OneDrive is_authenticated: Fallback succeeded, found {len(accounts)} accounts"
|
|
316
|
+
)
|
|
288
317
|
if accounts:
|
|
289
318
|
self._current_account = accounts[0]
|
|
290
319
|
if getattr(self.token_cache, "has_state_changed", False):
|
|
@@ -294,23 +323,31 @@ class OneDriveOAuth:
|
|
|
294
323
|
logger.warning(f"OneDrive is_authenticated: Fallback also failed, result: {result}")
|
|
295
324
|
return False
|
|
296
325
|
|
|
297
|
-
except Exception
|
|
326
|
+
except Exception:
|
|
298
327
|
logger.exception("[CONNECTOR] OneDrive is_authenticated failed")
|
|
299
328
|
return False
|
|
300
329
|
|
|
301
330
|
def get_access_token(self) -> str:
|
|
302
331
|
"""Get an access token for Microsoft Graph."""
|
|
303
|
-
logger.info(
|
|
332
|
+
logger.info(
|
|
333
|
+
f"OneDrive get_access_token: Starting, current_account={self._current_account is not None}"
|
|
334
|
+
)
|
|
304
335
|
try:
|
|
305
336
|
# Try with current account first
|
|
306
337
|
if self._current_account:
|
|
307
|
-
logger.info(
|
|
308
|
-
|
|
338
|
+
logger.info(
|
|
339
|
+
f"OneDrive get_access_token: Trying with account {self._current_account.get('username', 'unknown')}"
|
|
340
|
+
)
|
|
341
|
+
result = self.app.acquire_token_silent(
|
|
342
|
+
self.RESOURCE_SCOPES, account=self._current_account
|
|
343
|
+
)
|
|
309
344
|
if result and "access_token" in result:
|
|
310
345
|
logger.info("OneDrive get_access_token: Success with current account")
|
|
311
346
|
return result["access_token"]
|
|
312
347
|
else:
|
|
313
|
-
logger.warning(
|
|
348
|
+
logger.warning(
|
|
349
|
+
f"OneDrive get_access_token: Failed with account, result: {result}"
|
|
350
|
+
)
|
|
314
351
|
|
|
315
352
|
# Fallback: try without specific account
|
|
316
353
|
logger.info("OneDrive get_access_token: Fallback - trying without account")
|
|
@@ -320,11 +357,15 @@ class OneDriveOAuth:
|
|
|
320
357
|
return result["access_token"]
|
|
321
358
|
|
|
322
359
|
# If we get here, authentication has failed
|
|
323
|
-
error_msg = (
|
|
360
|
+
error_msg = (
|
|
361
|
+
(result or {}).get("error_description")
|
|
362
|
+
or (result or {}).get("error")
|
|
363
|
+
or "No valid authentication"
|
|
364
|
+
)
|
|
324
365
|
logger.error(f"OneDrive get_access_token: All attempts failed, error: {error_msg}")
|
|
325
366
|
raise ValueError(f"Failed to acquire access token: {error_msg}")
|
|
326
367
|
|
|
327
|
-
except Exception
|
|
368
|
+
except Exception:
|
|
328
369
|
logger.exception("[CONNECTOR] OneDrive get_access_token failed")
|
|
329
370
|
raise
|
|
330
371
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrag
|
|
3
|
-
Version: 0.5.0.
|
|
3
|
+
Version: 0.5.0.dev24
|
|
4
4
|
Summary: OpenRAG is a comprehensive Retrieval-Augmented Generation platform that enables intelligent document search and AI-powered conversations.
|
|
5
5
|
Classifier: Development Status :: 4 - Beta
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -61,8 +61,8 @@ connectors/ibm_cos/connector.py,sha256=HqgfpaDKl-q5gHzIiLCtCXrnaMnMX2E1HM7Xv6Yq6
|
|
|
61
61
|
connectors/ibm_cos/models.py,sha256=tQc0xSH_bFP_T-lqj010nJLqe6NWF2wFPCvuVIVzUq4,630
|
|
62
62
|
connectors/ibm_cos/support.py,sha256=0vWHSfYQiZRaPk0T0aIzTG7NYwbEdh7WWCyJAj2PhMU,2205
|
|
63
63
|
connectors/onedrive/__init__.py,sha256=YJs21dmKpdWRobGEyQex-j8Hg6y7zwpkZvOOqPGQD08,124
|
|
64
|
-
connectors/onedrive/connector.py,sha256=
|
|
65
|
-
connectors/onedrive/oauth.py,sha256=
|
|
64
|
+
connectors/onedrive/connector.py,sha256=O02zSrHhLUsB6Bj3nYiQcHz5hatj5YVzH_6N6M4q1d0,41263
|
|
65
|
+
connectors/onedrive/oauth.py,sha256=OPMeC-uaTLUMGpEWNZzgjGxkRtfxbd4ejZ4LarieFs0,17215
|
|
66
66
|
connectors/sharepoint/__init__.py,sha256=uZJE-UKNUdaugXLelLbsqpxuwWalW4306yMhwYbYiLY,132
|
|
67
67
|
connectors/sharepoint/connector.py,sha256=6aNE_1cffidmGkYoXFhJx8STegqMAqlw7__OdRLdA5s,37906
|
|
68
68
|
connectors/sharepoint/oauth.py,sha256=H7rE6DDlXfTPZTn64E_7crJQtKzuc_lC2UHo0m2FIS4,20272
|
|
@@ -73,7 +73,7 @@ models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
73
73
|
models/processors.py,sha256=e6qpstQq9DyHM4UMEUfjQqBoVO4xeLmSLkQcyt4oca0,35539
|
|
74
74
|
models/tasks.py,sha256=9iINXQZxA9-hcYzG8xC_baBfLa2w6gcRPJdQHsx_Kms,1643
|
|
75
75
|
models/url.py,sha256=92rsrLLyIqUErYipYz5QwUNTdHwJ2jBRV7PIVpncTyo,5764
|
|
76
|
-
openrag-0.5.0.
|
|
76
|
+
openrag-0.5.0.dev24.dist-info/licenses/LICENSE,sha256=jWcnPXkUxEinX60Yn-_zjsY9i84vz2QSb0fi53djOkg,11334
|
|
77
77
|
services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
78
78
|
services/api_key_service.py,sha256=NMz2ohJo0jcuHjDN3tHuzhCTVo6lsOqAkUhozzmWSv0,11720
|
|
79
79
|
services/auth_service.py,sha256=nNxKuCHh0ziHuHU18EGEUnf242h1XYsfbrbfWTmCmQE,22125
|
|
@@ -159,8 +159,8 @@ utils/telemetry/__init__.py,sha256=xoStY8hoYWnVe0B_jNthSdzRb7AxCT0rhM8ba9Q0Obg,2
|
|
|
159
159
|
utils/telemetry/category.py,sha256=wei3V5WyUaiadYjEE5s0ys9PY0TjnebIvTykQU0P_2k,1114
|
|
160
160
|
utils/telemetry/client.py,sha256=RhjW1a27w7W6gHt9X7AxdRPdZGmCEQDPskkxj5nsuiQ,12500
|
|
161
161
|
utils/telemetry/message_id.py,sha256=IAu0wBVNOGPcSlQ0bAzvqIYr1Ac_n8wSTSxXzq_tiV0,9634
|
|
162
|
-
openrag-0.5.0.
|
|
163
|
-
openrag-0.5.0.
|
|
164
|
-
openrag-0.5.0.
|
|
165
|
-
openrag-0.5.0.
|
|
166
|
-
openrag-0.5.0.
|
|
162
|
+
openrag-0.5.0.dev24.dist-info/METADATA,sha256=_I6uacsYzfvS8bzCw1kVyq1bxGtuCAwnr5GPUX9SK3E,7819
|
|
163
|
+
openrag-0.5.0.dev24.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
164
|
+
openrag-0.5.0.dev24.dist-info/entry_points.txt,sha256=ArKNHB63I3K8vynNkogvZOLmmQFpIsHDL7MvreCmju4,45
|
|
165
|
+
openrag-0.5.0.dev24.dist-info/top_level.txt,sha256=5vfLBIkzuwBJrizhCSZiXV7NTzTRIMlNLsyzYDEeCF8,125
|
|
166
|
+
openrag-0.5.0.dev24.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|