Why is my code to download files producing a 404 error?

1.1k views Asked by At

I have created an app, generated client credentials, and trusted the app for my SharePoint online site.

I have created a file test.txt and it is placed under -https://company.sharepoint.com/sites/testsite/Shared%20Documents/General/test.txt

Additionally, I have installed the latest version of the module

pip freeze | grep Office
Office365-REST-Python-Client==2.3.11
class SharePoint:

    def __init__(self):
        context_auth = AuthenticationContext(Configs.SITE_URL) --->   SITE_URL='https://company.sharepoint.com/sites/testsite/'
        context_auth.acquire_token_for_app(client_id=Configs.OAUTH_CLIENT_ID, client_secret=Configs.OAUTH_CLIENT_SECRET)
        self.ctx = ClientContext(Configs.SITE_URL, context_auth)

    def download_files(self):
        file_url = "/sites/testsite/Shared%20Documents/General/test.txt"
        download_path = os.path.join(tempfile.mkdtemp(), os.path.basename(file_url))
        print(download_path)
        with open(download_path, "wb") as local_file:
            file = self.ctx.web.get_file_by_server_relative_url(file_url).download(local_file).execute_query()
        print("[Ok] file has been downloaded into: {0}".format(download_path))

if __name__ == '__main__':
    s = SharePoint()
    s.download_files()

However, it throws an error, not able to get my head around this.

office365.runtime.client_request_exception.ClientRequestException: ('-2130575338, Microsoft.SharePoint.SPException', 'The file /sites/testsite/Shared%20Documents/General/test.txt does not exist.', "404 Client Error: Not Found for url: https://company.sharepoint.com/sites/testsite/_api/Web/getFileByServerRelativeUrl('%2Fsites%2Ftestsite%2FShared%2520Documents%2FGeneral%2Ftest.txt')?$select=ServerRelativePath")
1

There are 1 answers

0
ChaddRobertson On

You seem to be basing this off of the example shown here. I was having similar issues at first, until I made all function inputs be absolute paths, inclusive of url scheme and site. This just removes a lot of room for error.

My current script is similar to this:

from urllib.parse import urlparse
from office365.runtime.auth.authentication_context import AuthenticationContext
from office365.sharepoint.client_context import ClientContext

def download_file(local_absolute_path:str, global_absolute_path:str, client_context:ClientContext) -> None:

        print(f"The file {global_absolute_path} is being prepared for download.")

        download_location = urlparse(global_absolute_path)

        file_to_download = client_context.web.get_file_by_server_relative_url(download_location)

        with open(local_absolute_path, "wb") as local_file:
            file_to_download.download_session(local_file).execute_query()
        
        print(f"──► Download successful. The file has been saved as {local_absolute_path}\n")

Note that self.ctx in your code is equivalent to client_context in mine.

I recommend writing a bunch of helper functions to convert the paths back and forth between absolute, relative and the file name. The ones I currently use can be found below:

import os
from urllib.parse import urlparse


class PathHandler(object):

    def __init__(self, absolute_path:str) -> None:
        self.absolute_path = absolute_path

    
    def get_filename_from_absolute(self) -> str:

        parsed_url = urlparse(self.absolute_path)

        return os.path.basename(parsed_url.path)

    
    def get_relative_from_absolute(self) -> str:

        parsed_url = urlparse(self.absolute_path)

        return parsed_url.path

    
    def get_parent_folder_from_absolute(self) -> str:

        parsed_url = urlparse(self.absolute_path)

        return os.path.dirname(parsed_url.path)

    
    def get_scheme_and_root_from_absolute(self) -> str:

        parsed_url = urlparse(self.absolute_path)

        return f"{parsed_url.scheme}//{parsed_url.netloc}"
        
    
    def convert_to_absolute_local(self, local_root:str, global_root:str) -> str:

        return local_root + os.sep + self.absolute_path[len(global_root):].replace("/", os.sep)

    
    def convert_to_absolute_global(self, local_root:str, global_root:str) -> str:

        return global_root + "/" + self.absolute_path[len(local_root):].replace(os.sep, "/")