Microsoft Graph 실무 입문 #5 OneDrive와 SharePoint 파일 다루기: 업로드 세션과 공유
이번 편은 파일입니다. 산출물을 SharePoint에 자동 업로드하고, 매일 생기는 로그를 백업하고, 생성한 보고서의 공유 링크를 만드는 시나리오들입니다. 파일 API의 좋은 점은 OneDrive와 SharePoint가 같은 모델을 쓴다는 것입니다. 한 번 익히면 개인 드라이브든 팀 사이트든 같은 코드가 돌아갑니다.
권한은 delegated라면 Files.ReadWrite(자기 파일)로 시작하고, application이라면 Sites.ReadWrite.All이 기본입니다만, 이 권한은 조직의 모든 사이트를 열기 때문에 뒤에서 다룰 Sites.Selected로 좁히는 것이 요즘의 정석입니다.
모델: drive와 driveItem 두 개면 끝입니다 #
- drive: 파일 보관함 하나입니다. 사용자의 OneDrive도 drive, SharePoint 사이트의 문서 라이브러리도 drive, Teams 팀의 파일 탭 뒤에 있는 것도 결국 SharePoint의 drive입니다.
- driveItem: drive 안의 항목입니다. 파일도 폴더도 driveItem이고,
folder속성이 있으면 폴더,file속성이 있으면 파일이라는 식으로 구분합니다.
접근 경로만 보관함의 종류에 따라 다릅니다.
# 사용자의 OneDrive
GET /users/{user-id}/drive
# SharePoint 사이트의 기본 문서 라이브러리
GET /sites/{site-id}/drive
# 사이트 ID는 URL로 찾는 것이 실무적
GET /sites/contoso.sharepoint.com:/sites/DevTeam
→ 응답의 id가 site-id
# drive 안에서의 항목 접근: 경로 기반이 편합니다
GET /drives/{drive-id}/root:/reports/2026/weekly.xlsx마지막 줄의 경로 기반 주소(콜론 문법)가 실무의 주력입니다. root:/폴더/파일명 형태로, ID를 몰라도 사람이 아는 경로 그대로 접근할 수 있습니다. 자동화 코드에서는 “사이트 URL → site-id → drive-id"를 한 번 조회해 설정 파일에 저장해 두고, 이후는 경로 기반으로만 접근하는 패턴이 깔끔합니다.
다운로드와 단순 업로드 #
읽기와 작은 파일 쓰기는 한 호출씩입니다.
async def download(client, drive_id: str, path: str) -> bytes:
return await (
client.drives.by_drive_id(drive_id)
.root.item_with_path(path)
.content.get()
)
async def upload_small(client, drive_id: str, path: str, data: bytes):
# PUT .../root:/{path}:/content: 250MB까지 한 번에
return await (
client.drives.by_drive_id(drive_id)
.root.item_with_path(path)
.content.put(data)
)단순 업로드(PUT /content)는 250MB까지 지원합니다. 다만 공식 권고는 10MiB를 넘는 파일부터는 다음 절의 업로드 세션을 쓰라는 것입니다. 큰 파일의 단일 요청은 네트워크가 한 번 끊기면 처음부터 다시 올려야 해서, 실패 시 재개가 되는 세션 방식이 운영에서는 결국 이득이기 때문입니다.
또 하나, 같은 경로에 파일이 이미 있으면 덮어씁니다. 의도가 아니라면 업로드 시 @microsoft.graph.conflictBehavior(rename / fail / replace)를 지정해 충돌 동작을 명시하는 것이 안전합니다.
대용량 업로드: 업로드 세션 #
큰 파일의 정석은 3단계입니다. ① 업로드 세션을 만들고 ② 조각(청크)을 순서대로 PUT하고 ③ 마지막 조각이 올라가면 자동으로 완료됩니다.
import httpx
from msgraph.generated.drives.item.items.item.create_upload_session.create_upload_session_post_request_body import (
CreateUploadSessionPostRequestBody,
)
from msgraph.generated.models.drive_item_uploadable_properties import (
DriveItemUploadableProperties,
)
CHUNK = 5 * 1024 * 1024 # 5MiB: 320KiB(327,680)의 배수여야 합니다
async def upload_large(client, drive_id: str, path: str, filepath: str):
body = CreateUploadSessionPostRequestBody(
item=DriveItemUploadableProperties(
additional_data={"@microsoft.graph.conflictBehavior": "replace"},
),
)
session = await (
client.drives.by_drive_id(drive_id)
.root.item_with_path(path)
.create_upload_session.post(body)
)
# 세션 URL에는 인증 토큰이 필요 없습니다(사전 인증된 URL)
import os
total = os.path.getsize(filepath)
async with httpx.AsyncClient() as http:
with open(filepath, "rb") as f:
offset = 0
while chunk := f.read(CHUNK):
end = offset + len(chunk) - 1
resp = await http.put(
session.upload_url,
content=chunk,
headers={
"Content-Range": f"bytes {offset}-{end}/{total}",
"Content-Length": str(len(chunk)),
},
)
resp.raise_for_status()
offset = end + 1규칙이 몇 가지 있고, 어기면 마지막 조각에서 실패하는 고약한 형태로 나타나므로 미리 새겨 둡니다.
- 청크 크기는 320KiB(327,680바이트)의 배수여야 합니다. 안정적인 회선에서는 5〜10MiB가 권장 범위이고, 한 요청의 상한은 60MiB입니다.
- 조각은 순서대로 올립니다(OneDrive/SharePoint 기준).
- 5xx나 네트워크 단절을 만나면 세션 URL에 GET을 보내
nextExpectedRanges(서버가 아직 못 받은 범위)를 확인하고 거기서부터 재개합니다. 404가 오면 세션이 만료된 것이라 처음부터 다시 시작합니다. - 세션에는 만료 시각이 있지만, 조각이 올라갈 때마다 연장되므로 업로드가 진행 중인 한 계속 살아 있습니다.
참고로 각 언어 SDK에는 이 절차를 감싼 대용량 업로드 태스크 헬퍼가 있어서(예: LargeFileUploadTask), 헬퍼가 있으면 그것을 쓰는 편이 짧습니다. 위 코드는 어느 환경에서든 통하도록 절차 자체를 보인 것입니다.
공유 링크 만들기: createLink #
업로드한 보고서를 사람에게 전달하는 마지막 조각입니다. createLink로 링크를 만들고, 그 URL을 4편의 메일이나 6편의 Teams 메시지에 실으면 자동화 한 사이클이 완성됩니다.
from msgraph.generated.drives.item.items.item.create_link.create_link_post_request_body import (
CreateLinkPostRequestBody,
)
async def share_link(client, drive_id: str, item_id: str) -> str:
body = CreateLinkPostRequestBody(
type="view", # view / edit
scope="organization", # organization / anonymous / users
)
perm = await (
client.drives.by_drive_id(drive_id)
.items.by_drive_item_id(item_id)
.create_link.post(body)
)
return perm.link.web_urlscope 선택이 곧 보안 결정입니다. 사내 자동화라면 organization(조직 내부만 열람 가능)이 기본값이어야 하고, anonymous(링크만 있으면 누구나)는 조직 정책으로 막혀 있는 경우도 많으며, 막혀 있는 것이 오히려 정상입니다. 자동화가 만드는 링크는 수가 많아지므로, 넓은 scope의 습관은 그대로 정보 유출 경로가 됩니다.
application 권한 좁히기: Sites.Selected #
이 편의 마지막은 권한 이야기입니다. application의 Sites.ReadWrite.All은 조직의 모든 SharePoint 사이트를 열기 때문에, “자동화 하나 때문에 전체를 열 수는 없다"는 반대에 부딪히기 쉽습니다. 그 답이 Sites.Selected입니다.
동작 방식이 특이해서 짚어 둡니다. Sites.Selected 권한 자체는 아무 사이트도 열지 않습니다. 관리자가 사이트 단위로 “이 앱에게 이 사이트의 read/write를 준다"는 부여(site permission)를 별도로 추가해야 실제 접근이 생깁니다(이 부여 작업 자체도 Graph의 사이트 권한 API나 PnP PowerShell로 합니다). 즉 “앱 권한은 목록에 하나, 실제 범위는 사이트별 화이트리스트"라는 구조입니다. 파일 자동화의 프로덕션 구성에서는 이쪽을 기본으로 삼고, .All은 정말 전 사이트를 다뤄야 하는 경우로 한정하는 것을 권합니다.
정리 #
- 모델은 drive(보관함)와 driveItem(파일·폴더) 둘뿐이고, OneDrive와 SharePoint가 같은 모델을 씁니다. 항목 접근은 경로 기반(콜론 문법)이 실무의 주력입니다.
- 단순 업로드는 250MB까지 되지만, 10MiB를 넘으면 업로드 세션이 정석입니다. 청크는 320KiB의 배수(권장 5〜10MiB), 실패 시 nextExpectedRanges로 재개합니다.
- 업로드 충돌 동작(conflictBehavior)은 명시합니다. 기본 동작에 맡기면 어느 날 덮어쓰기 사고가 납니다.
- 공유 링크의 scope는 organization이 기본값입니다. 자동화가 만드는 anonymous 링크는 유출 경로가 됩니다.
- application 파일 자동화의 권한 정석은 Sites.Selected + 사이트별 부여입니다.
.All은 예외로 취급합니다.
다음 편은 Teams입니다. 메시지를 읽는 것과 보내는 것의 권한 지형이 크게 다른 영역이라, “알림 보내기의 세 가지 경로"를 중심으로 정리합니다.