使用Python OneDrive SDK将文件上传到MS SharePoint

8

使用Python OneDrive SDK,能否将文件上传到Microsoft SharePoint网站的Shared Documents库中?

此文档(在第一句话中)表示应该可以,但我无法使其工作。

我能够进行身份验证(使用Azure AD)并上传到OneDrive文件夹,但尝试上传到SharePoint文件夹时,我不断收到以下错误:

"Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown."

这是返回带有错误的对象的代码:

(...authentication...)
client = onedrivesdk.OneDriveClient('https://{tenant}.sharepoint.com/{site}/_api/v2.0/', auth, http)
client.item(path='/drive/special/documents').children['test.xlsx'].upload('test.xlsx')

where I'd like to upload on the web

我可以成功地使用以下代码上传到https://{tenant}-my.sharepoint.com/_api/v2.0/(注意在{tenant}后面加上"-my"):
client = onedrivesdk.OneDriveClient('https://{tenant}-my.sharepoint.com/_api/v2.0/', auth, http)
returned_item = client.item(drive='me', id='root').children['test.xlsx'].upload('test.xlsx')

我该如何将同一文件上传到 SharePoint 网站?

(在 Stack Overflow 上类似问题的回答(1,2,3,4) 要么太过模糊,要么建议使用不同的 API。我的问题是是否可以使用 OneDrive Python SDK 完成,如果可以,应该如何操作。)


更新:这是我的完整代码和输出。(敏感原始数据已替换为类似格式的无意义字符。

import re
import onedrivesdk
from onedrivesdk.helpers.resource_discovery import ResourceDiscoveryRequest

# our domain (not the original)
redirect_uri = 'https://example.ourdomain.net/' 
# our client id (not the original)
client_id = "a1234567-1ab2-1234-a123-ab1234abc123"  
# our client secret (not the original)
client_secret = 'ABCaDEFGbHcd0e1I2fghJijkL3mn4M5NO67P8Qopq+r=' 
resource = 'https://api.office.com/discovery/'
auth_server_url = 'https://login.microsoftonline.com/common/oauth2/authorize'
auth_token_url = 'https://login.microsoftonline.com/common/oauth2/token'
http = onedrivesdk.HttpProvider()
auth = onedrivesdk.AuthProvider(http_provider=http, client_id=client_id, 
                                auth_server_url=auth_server_url, 
                                auth_token_url=auth_token_url)

should_authenticate_via_browser = False
try:
    # Look for a saved session. If not found, we'll have to 
    # authenticate by opening the browser.
    auth.load_session()
    auth.refresh_token()
except FileNotFoundError as e:
    should_authenticate_via_browser = True
    pass

if should_authenticate_via_browser:
    auth_url = auth.get_auth_url(redirect_uri)
    code = ''
    while not re.match(r'[a-zA-Z0-9_-]+', code):
        # Ask for the code
        print('Paste this URL into your browser, approve the app\'s access.')
        print('Copy the resulting URL and paste it below.')
        print(auth_url)
        code = input('Paste code here: ')
        # Parse code from URL if necessary
        if re.match(r'.*?code=([a-zA-Z0-9_-]+).*', code):
            code = re.sub(r'.*?code=([a-zA-Z0-9_-]*).*', r'\1', code)
    auth.authenticate(code, redirect_uri, client_secret, resource=resource)
    # If you have access to more than one service, you'll need to decide
    # which ServiceInfo to use instead of just using the first one, as below.
    service_info = ResourceDiscoveryRequest().get_service_info(auth.access_token)[0]
    auth.redeem_refresh_token(service_info.service_resource_id)
    auth.save_session()  # Save session into a local file.

# Doesn't work
client = onedrivesdk.OneDriveClient(
    'https://{tenant}.sharepoint.com/sites/{site}/_api/v2.0/', auth, http)
returned_item = client.item(path='/drive/special/documents')
                      .children['test.xlsx']
                      .upload('test.xlsx')
print(returned_item._prop_dict['error_description'])

# Works, uploads to OneDrive instead of SharePoint site
client2 = onedrivesdk.OneDriveClient(
    'https://{tenant}-my.sharepoint.com/_api/v2.0/', auth, http)
returned_item2 = client2.item(drive='me', id='root')
                        .children['test.xlsx']
                        .upload('test.xlsx')
print(returned_item2.web_url)

输出:

Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown.
https://{tenant}-my.sharepoint.com/personal/user_domain_net/_layouts/15/WopiFrame.aspx?sourcedoc=%1ABCDE2345-67F8-9012-3G45-6H78IJKL9M01%2N&file=test.xlsx&action=default

我注意到您在尝试对SharePoint库执行此操作时没有指定“驱动器”。这似乎是违反直觉的,但SharePoint文档库被视为驱动器,并且可以像访问其他OneDrive资源一样进行访问。无论您是访问OneDrive还是Sharepoint,API操作几乎总是要处理驱动器资源。请参阅驱动器资源列出驱动器 - sytech
谢谢!我意识到可以使用 onedrivesdk.OneDriveClient('https://{tenant}-my.sharepoint.com/_api/v2.0/', auth, http).drives.get() 列出驱动器。它列出了一个类型为“business”的驱动器。我认为这是OneDrive存储。 对于没有“-my”的URL,我可以执行 onedrivesdk.OneDriveClient('https://{tenant}.sharepoint.com/_api/v2.0/', auth, http).drive.get(),它返回一个具有与上面相同的错误消息(AudienceUri...)的对象。 - Attila Tanyi
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - sytech
经过查看源代码,显然SDK使用了您提供的相同资源发现。我之前检查过返回的资源列表,只有一个。现在我深入研究了SDK代码,发现发现实际上返回了4个 - OneDrive MyFiles v1.0、v2.0、SharePoint RootSite v1.0和Azure Directory v1.0。我想使用的是SharePoint RootSite v1.0。问题是SDK仅过滤v2.0,因此它甚至不会返回{tenant}.sharepoint.com/_api。 (消息格式不同 - v1为XML,v2为JSON。)我将检查是否可以进行修改。 - Attila Tanyi
@sytech - 我终于找到了一个解决方法。感谢你的帮助! - Attila Tanyi
显示剩余4条评论
1个回答

5

在(SO用户)sytech的帮助下,我终于找到了解决方案。

我的原始问题的答案是,使用原始的Python OneDrive SDK时,无法将文件上传到SharePoint Online站点的Shared Documents文件夹中(在撰写本文时):当SDK查询资源发现服务时,它会删除所有service_api_version不为v2.0的服务。然而,我用v1.0得到了SharePoint服务,所以被删除了,尽管它也可以使用API v2.0访问。

然而,通过扩展ResourceDiscoveryRequest类(在OneDrive SDK中),我们可以为此创建一个解决方法。我成功地通过这种方式上传了一个文件

import json
import re
import onedrivesdk
import requests
from onedrivesdk.helpers.resource_discovery import ResourceDiscoveryRequest, \
    ServiceInfo

# our domain (not the original)
redirect_uri = 'https://example.ourdomain.net/' 
# our client id (not the original)
client_id = "a1234567-1ab2-1234-a123-ab1234abc123"  
# our client secret (not the original)
client_secret = 'ABCaDEFGbHcd0e1I2fghJijkL3mn4M5NO67P8Qopq+r=' 
resource = 'https://api.office.com/discovery/'
auth_server_url = 'https://login.microsoftonline.com/common/oauth2/authorize'
auth_token_url = 'https://login.microsoftonline.com/common/oauth2/token'

# our sharepoint URL (not the original)
sharepoint_base_url = 'https://{tenant}.sharepoint.com/'
# our site URL (not the original)
sharepoint_site_url = sharepoint_base_url + 'sites/{site}'

file_to_upload = 'C:/test.xlsx'
target_filename = 'test.xlsx'


class AnyVersionResourceDiscoveryRequest(ResourceDiscoveryRequest):

    def get_all_service_info(self, access_token, sharepoint_base_url):
        headers = {'Authorization': 'Bearer ' + access_token}
        response = json.loads(requests.get(self._discovery_service_url,
                                           headers=headers).text)
        service_info_list = [ServiceInfo(x) for x in response['value']]
        # Get all services, not just the ones with service_api_version 'v2.0'
        # Filter only on service_resource_id
        sharepoint_services = \
            [si for si in service_info_list
             if si.service_resource_id == sharepoint_base_url]
        return sharepoint_services


http = onedrivesdk.HttpProvider()
auth = onedrivesdk.AuthProvider(http_provider=http, client_id=client_id,
                                auth_server_url=auth_server_url,
                                auth_token_url=auth_token_url)

should_authenticate_via_browser = False
try:
    # Look for a saved session. If not found, we'll have to
    # authenticate by opening the browser.
    auth.load_session()
    auth.refresh_token()
except FileNotFoundError as e:
    should_authenticate_via_browser = True
    pass

if should_authenticate_via_browser:
    auth_url = auth.get_auth_url(redirect_uri)
    code = ''
    while not re.match(r'[a-zA-Z0-9_-]+', code):
        # Ask for the code
        print('Paste this URL into your browser, approve the app\'s access.')
        print('Copy the resulting URL and paste it below.')
        print(auth_url)
        code = input('Paste code here: ')
        # Parse code from URL if necessary
        if re.match(r'.*?code=([a-zA-Z0-9_-]+).*', code):
            code = re.sub(r'.*?code=([a-zA-Z0-9_-]*).*', r'\1', code)

    auth.authenticate(code, redirect_uri, client_secret, resource=resource)
    service_info = AnyVersionResourceDiscoveryRequest().\
        get_all_service_info(auth.access_token, sharepoint_base_url)[0]
    auth.redeem_refresh_token(service_info.service_resource_id)
    auth.save_session()

client = onedrivesdk.OneDriveClient(sharepoint_site_url + '/_api/v2.0/',
                                    auth, http)
# Get the drive ID of the Documents folder.
documents_drive_id = [x['id']
                      for x
                      in client.drives.get()._prop_list
                      if x['name'] == 'Documents'][0]
items = client.item(drive=documents_drive_id, id='root')
# Upload file
uploaded_file_info = items.children[target_filename].upload(file_to_upload)

为不同服务进行认证将会获得不同的令牌。


代码看起来不错,但我正在编写的代码需要自动化,所以我不能每次都复制和粘贴浏览器生成的“代码”,这个“代码”也很快就会过期。所以,基于你的代码,我修改了我的代码,但无论是v1.0还是v2.0,我仍然得到空的response['value'].....请问,你知道我的代码有什么问题吗?这是我的代码:https://github.com/hanhanwu/Basic_But_Useful/blob/master/one_drive_buz_test.py - Cherry Wu
@CherryWu - 上面的代码是自动化的。auth.save_session() 将认证信息保存到一个名为 session.pickle 的文件中。auth.load_session() 加载这个文件。它的工作方式是,第一次运行此代码时,它将打开一个浏览器窗口,在那里您需要登录并将重定向后的 URL 复制并粘贴回 Python 控制台。然后保存此身份验证令牌,并在下一次加载时不会打开浏览器。您可以在这里的“保存和加载会话”部分了解更多信息。 - Attila Tanyi
非常感谢您,Attila。今天我没有时间尝试这个,但是有一个快速的问题:我们从浏览器获取的代码寿命非常短暂,这是否意味着使用您代码中的“保存和加载会话”功能,“代码”将不再过期? - Cherry Wu
1
我不知道,我还没有遇到过过期的情况。短生命周期令牌是_access token_,这里存储的也是_refresh token_。根据这里的答案,如果在14天内至少使用一次,这些Azure AD令牌应该在90天后过期。 - Attila Tanyi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接