koltyakov / gosip

⚡️ SharePoint SDK for Go
https://go.spflow.com
MIT License
145 stars 33 forks source link

How to download a file with a sharing link? #69

Closed vekonylaszlo closed 10 months ago

vekonylaszlo commented 10 months ago

Hello,

In our setup, we manage a list wherein one of the columns contains links to uploaded files. I am retrieving and processing the items using the following Go code:

type SharepointMetadata struct {
    Type string `json:"type"`
}

type SharepointLanguageMetadata struct {
    Metadata    SharepointMetadata `json:"__metadata"`
    Description string             `json:"Description"`
    Url         string             `json:"Url"`
}

pList, err := sp.Web().Lists().GetByTitle("list-title").Items().Select("column-with-link").Get()
items := []*SharepointListElement{}
if err := json.Unmarshal(pList.Normalized(), &items); err != nil {
    log.Fatalf("unable to parse the response: %v", err)
}

// then i loop the items
for _, item := range items {
    // then download the file using the Url field
    data, err := sp.Web().GetFileByPath(item.Url).Download()
    if err != nil {
        log.Printf("error while downloading file from SharePoint %v\n", err)
        return "", errors.New("Error while downloading file")
    }
}

However, I am encountering a 400 Bad Request error, and the SharePoint API returns the following error message:

{
  "odata.error": {
    "code": "-1, Microsoft.SharePoint.Client.InvalidClientQueryException",
    "message": {
      "lang": "hu-HU",
      "value": "The „Web/GetFileByServerRelativePath(decodedUrl='/sites/{group}/https:/{tenant}.sharepoint.com/:b:/s/{group}/Ed1hWXdMvs5NoydVRb7RR2ABfErH1BuPx2nTrFx_dIXQzQ” expression is invalid. "
    }
  }
}

I have also attempted to use api.NewHTTPClient, but I am unable to make it work. Could you kindly provide assistance on how I can address this issue? Any guidance or suggestions would be greatly appreciated.

Thank you for your time and support.

koltyakov commented 10 months ago

Hi @vekonylaszlo,

.GetFileByPath(item.Url).Download() is a method to deal with a file through SharePoint API referencing it by ServerRelativeURL (like /sites/my-site/doc-lib/folder/my-file.dat).

If you know file location in SharePoint and service account has permissions to it, you can download it with the API.

An abstract URL to a "shared file" won't necessarily work as literally you could have any link in the URL field. Even if it's a SharePoint file shared - it's then outside SharePoint API context.

Having a WOPI URL like https://{tenant}.sharepoint.com/:x:/s/{sile}/EQT6Kcs-4h5LjVEaV8pLF-UBchNR-uKeiuUjJrMeE3GI2A?e=tcWSaU, theoretically it's accessible with the same FedAuth Cookie, then it contains _wopiContextJson object in server-side crafted JS fragment with identities to the original file (FileName, ParentFolderFullUrl). There should be some other way to get original URL yet I'm not sure. I've never needed reversing files share URL, usually you know original SharePoint context explicitly. UPD: Actually, this won't work as not necessarily you have an office document.

koltyakov commented 10 months ago

You can try using something like this to get SharePoint Server Relative URLs from Shared Links:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/url"
    "regexp"

    "io"

    "github.com/koltyakov/gosip"
    "github.com/koltyakov/gosip/auth"
)

func GetServerRelativeURLFromSharedLink(client *gosip.SPClient, sharedURL string) (string, error) {
    req, _ := http.NewRequest("GET", sharedURL, nil)
    resp, err := client.Execute(req)

    // None Office Formats
    if resp.StatusCode == 302 {
        parsedURL, err := url.Parse(resp.Header.Get("Location"))
        if err != nil {
            return "", fmt.Errorf("can't parse redirect location: %w", err)
        }

        return parsedURL.Query().Get("id"), nil
    }

    if err != nil {
        return "", fmt.Errorf("can't execute request: %w", err)
    }

    defer resp.Body.Close()

    body, err := io.ReadAll(io.Reader(resp.Body))

    re := regexp.MustCompile(`var _wopiContextJson =({.*?});`)
    match := re.FindStringSubmatch(string(body))

    var d struct {
        FileName            string `json:"FileName"`
        ParentFolderFullUrl string `json:"ParentFolderFullUrl"`
    }

    if len(match) < 1 {
        return "", fmt.Errorf("no match found for _wopiContextJson")
    }

    jsonStr := match[1]

    err = json.Unmarshal([]byte(jsonStr), &d)
    if err != nil {
        return "", fmt.Errorf("can't unmarshal _wopiContextJson: %w", err)
    }

    parsedURL, err := url.Parse(d.ParentFolderFullUrl + "/" + d.FileName)
    if err != nil {
        return "", fmt.Errorf("can't parse ParentFolderFullUrl: %w", err)
    }

    return parsedURL.Path, nil
}

func main() {
    sharedUrl := "https://{tenant}.sharepoint.com/:u:/s/ci/EcpaZ9PHKoFDoueaXcvy_68B4s7QLsEhB0dybIweMBXZiA?e=Iy8CXC"

    cnfg, err := auth.NewAuthFromFile("./config/private.spo-user.json")
    if err != nil {
        log.Fatal(err)
    }

    client := &gosip.SPClient{AuthCnfg: cnfg}

    serverRelativeURL, err := GetServerRelativeURLFromSharedLink(client, sharedUrl)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("ServerRelativeURL: %s\n", serverRelativeURL)
}

Checked it in Office and non-office formats.

Then serverRelativeURL is what to use in sp.Web().GetFileByPath(serverRelativeURL).

vekonylaszlo commented 10 months ago

Thank you very much for the help! Converting the SharedLink is working, I just need to fix one error (invalid URL escape "% P"), but I think it will work from here. Have a nice day!