diff --git a/src/app.py b/src/app.py index 8f9aa15..1798444 100644 --- a/src/app.py +++ b/src/app.py @@ -637,9 +637,9 @@ def resolve(note): item = note.get(chat_read.drive_item_path(ref["contentUrl"], "name,size,file,@microsoft.graph.downloadUrl")) if (item.get("size") or 0) > MAX_CHAT_FILE_BYTES: raise HTTPException(status_code=413, detail="The file is larger than 100 MB.") - url = item.get("@microsoft.graph.downloadUrl") + url = teams_files.download_url(note, f"shares/{chat_read.share_id(ref['contentUrl'])}/driveItem", item) if not url: - raise HTTPException(status_code=404, detail="The file has no download link.") + raise HTTPException(status_code=502, detail="Microsoft did not offer a download link for this file.") return item, url carrier = Response() @@ -886,9 +886,9 @@ def resolve(note): raise HTTPException(status_code=404, detail="File not found.") if (item.get("size") or 0) > teams_files.MAX_DOWNLOAD_BYTES: raise HTTPException(status_code=413, detail="The file is larger than 100 MB.") - url = item.get("@microsoft.graph.downloadUrl") + url = teams_files.download_url(note, teams_files.item_base(drive_id, item_id), item) if not url: - raise HTTPException(status_code=404, detail="The file has no download link.") + raise HTTPException(status_code=502, detail="Microsoft did not offer a download link for this file.") if not teams_files.is_download_url(url): raise HTTPException(status_code=502, detail="The file has an unexpected download location.") return item, url @@ -938,9 +938,9 @@ def _file_text(note, drive_id: str | None, item_id: str, offset: int, max_chars: name = posixpath.splitext(name)[0] + ".pdf" notes.append("Converted to PDF by Microsoft 365 before reading.") else: - url = item.get("@microsoft.graph.downloadUrl") + url = teams_files.download_url(note, teams_files.item_base(drive_id, item_id), item) if not url: - raise HTTPException(status_code=404, detail="The file has no download link.") + raise HTTPException(status_code=502, detail="Microsoft did not offer a download link for this file.") data = teams_files.fetch_bytes(url, file_text.MAX_SOURCE_BYTES) result = file_text.extract(name, data, offset, max_chars, sheet, row_offset, max_rows, notes) except file_text.TextError as ex: diff --git a/src/teams_files.py b/src/teams_files.py index 133b6fc..dc9f26b 100644 --- a/src/teams_files.py +++ b/src/teams_files.py @@ -76,6 +76,21 @@ def fetch_bytes(url: str, cap: int) -> bytes: return bytes(out) +def download_url(note, path: str, item: dict) -> str | None: + """The pre-authorized link of the item at `path`. Graph can leave it out of a `$select` answer, so ask again without one, then through `/content`.""" + url = item.get("@microsoft.graph.downloadUrl") + if url: + return url + full = note.get(path) + url = full.get("@microsoft.graph.downloadUrl") if isinstance(full, dict) else None + if url: + return url + try: + return note.redirect(f"{path}/content") + except GraphError: + return None + + def item_base(drive_id: str | None, item_id: str) -> str: return f"drives/{seg(drive_id)}/items/{seg(item_id)}" if drive_id else f"me/drive/items/{seg(item_id)}" diff --git a/tests/test_teams_files.py b/tests/test_teams_files.py index 28c1020..1aa31f7 100644 --- a/tests/test_teams_files.py +++ b/tests/test_teams_files.py @@ -279,3 +279,42 @@ def test_convert_text_body_cap(env, monkeypatch): monkeypatch.setattr(app_module, "MAX_TEXT_SOURCE_BYTES", 100) assert client.post("/api/convert/text?name=x.txt", content=b"a" * 101, headers=H).status_code == 413 assert client.post("/api/convert/text?name=x.txt", content=b"a" * 100, headers=H).json()["text"] == "a" * 100 + + +class LinkNote: + def __init__(self, plain=None, redirect=None): + self.plain, self.location, self.paths = plain, redirect, [] + + def get(self, path): + self.paths.append(path) + return self.plain + + def redirect(self, path): + self.paths.append(path) + if self.location is None: + raise teams_files.GraphError(404, "no content") + return self.location + + +LINK = "https://contoso.sharepoint.com/download?tempauth=x" + + +def test_download_url_uses_the_link_from_the_select_answer(): + note = LinkNote() + assert teams_files.download_url(note, "me/drive/items/I", {"@microsoft.graph.downloadUrl": LINK}) == LINK + assert note.paths == [] + + +def test_download_url_asks_again_without_select(): + note = LinkNote(plain={"@microsoft.graph.downloadUrl": LINK}) + assert teams_files.download_url(note, "me/drive/items/I", {"name": "Plan.xlsx"}) == LINK + + +def test_download_url_falls_back_to_the_content_redirect(): + note = LinkNote(plain={"name": "Plan.xlsx"}, redirect=LINK) + assert teams_files.download_url(note, "me/drive/items/I", {}) == LINK + assert note.paths == ["me/drive/items/I", "me/drive/items/I/content"] + + +def test_download_url_is_none_when_microsoft_offers_nothing(): + assert teams_files.download_url(LinkNote(plain={}), "me/drive/items/I", {}) is None