下载SharePoint office 365版本文件

Download version file of SharePoint office 365

我尝试使用 c# 下载 SharePoint 的早期版本文件。我使用 This article 作为参考。 link 是 chrome 的工作文件。现在,当我尝试在 c# 上使用 URL 逐部分下载文件时,它给我 远程服务器返回错误:(401) 未授权。 错误。

我什至使用 header 函数提供了访问令牌。

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
            WebHeaderCollection header = new WebHeaderCollection();
            request.Headers.Add(System.Net.HttpRequestHeader.Authorization, $"Bearer {token}");
            request.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f");

在这里, uri类似于地址:http://yoursite/yoursubsite/_vti_history/512/Documents/Book1.xlsx

如何使用c#下载旧版本文件?

这是我的测试代码,供大家参考。

var login = "user@xxx.onmicrosoft.com";
            var password = "Password";

            var securePassword = new SecureString();

            foreach (char c in password)
            {
                securePassword.AppendChar(c);
            }
            SharePointOnlineCredentials onlineCredentials = new SharePointOnlineCredentials(login, securePassword);

            string webUrl = "https://xxx.sharepoint.com/sites/lee";
            string requestUrl = "https://xxx.sharepoint.com/sites/lee/_vti_history/512/MyDoc2/testdata.xlsx";
            Uri uri = new Uri(requestUrl);
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
            request.Method = "GET";
            request.Credentials = onlineCredentials;
            request.Headers[HttpRequestHeader.Cookie] = onlineCredentials.GetAuthenticationCookie(new Uri(webUrl), true);  // SPO requires cookie authentication
            request.Headers["X-FORMS_BASED_AUTH_ACCEPTED"] = "f";  // disable interactive forms-based auth            
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            Stream stream = response.GetResponseStream();

以下是使用客户端 ID 和客户端密码从 SharePoint 站点下载文件的完整示例。

遇到的主要问题:

  1. 获取实际会被接受的访问令牌。获取访问令牌非常容易,但尝试使用它下载文件会导致 401 错误消息 invalid_client.

这些是无效的 authUrls。 x 尝试的值是 tenantId(例如 guid)或 tenantDomain(例如 company.com)。将返回访问令牌,但返回的资源始终用于 Microsoft Graph (00000003-0000-0000-c000-000000000000)。

String authUrl = "https://login.microsoftonline.com/" + x + "/oauth2/token";
String authUrl = "https://login.microsoftonline.com/" + x + "/oauth2/v2.0/token";
String authUrl = "https://login.windows.net/" + x + "/oauth2/token";
String authUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";

实际有效的 authUrl 是:

String authUrl = "https://accounts.accesscontrol.windows.net/" + tenantId + "/tokens/OAuth/2";
  1. 第二个问题是使用直接 URL 下载文件, 例如https://tenant.sharepoint.com/Shared Documents/Data.xlsx

显然直接网址不起作用。理论上是内部服务器重定向从请求中剥离访问令牌 headers。因此,必须使用以下任一方式下载文件:

a) https://tenant.sharepoint.com/_api/web/getfilebyserverrelativeurl('/Shared Documents/FileName.xlsx')/$value

末尾的/$value表示下载实际的二进制数据。如果省略,则会下载一个 XML 文件,其中包含有关文件的属性(created datemodified datelength 等)。

b) https://tenant.sharepoint.com/_layouts/15/download.aspx?SourceUrl=/Shared Documents/FileName.xlsx

注意:如果使用选项 a),则 'FileName' 中的任何单引号必须替换为连续的两个单引号。可以在此处阅读有关转义问题的更多信息:https://sharepoint.stackexchange.com/questions/154590/getfilebyserverrelativeurl-fails-when-the-filename-contains-a-quote

  1. 忘记设置: System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;

这会产生类似连接被强制关闭的消息。

  1. 确保 -DisableCustomAppAuthenticationfalse。这可以通过 PowerShell 设置。

    PS> install-module -name "PnP.PowerShell"

    PS> Connect-PnPOnline -Url "https://tenant.sharepoint.com"

    PS> Set-SPOTenant -DisableCustomAppAuthentication $false

注意:我看到很多设置headers的代码,比如:

req.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f");
req.Headers.Add("Accept", "application/json;odata=verbose");
req.Headers.Add("cache-control", "no-cache");
req.Headers.Add("Use-Agent", "Other");
ClientId/ClientSecret 身份验证需要

None 个 headers。但是,使用旧版 SharePointOnlineCredentials 时需要 "X-FORMS_BASED_AUTH_ACCEPTED"

有用的链接:

下面的代码提供了几种获取访问令牌和下载文件的不同方法。

using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web;

namespace SharePointDemo {

public class Program {

    const String SharePointPrincipal = "00000003-0000-0ff1-ce00-000000000000";

    public static void Main(String[] args) {
        System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; // required.

        // Replace these 6 values:
        Uri url = new Uri("https://tenant.sharepoint.com"); // with or without an ending '/' both work
        String clientId = "... guid ...";
        String clientSecret = "... generated using the SharePoint AppRegNew.aspx ..."; // see useful links above
        String tenantId = "... guid ..."; // aka realm. Can be found in the SharePoint admin site settings, or by using GetTenantId(url);
        String fileRelativeUrl = "/Shared Documents/Data.xlsx";
        String filename = @"C:\temp\Data.xlsx"; // local file name
        
        String access_token_key = "access_token"; // variable name in headers to look for in authUrl's response.
        Uri fileUrl = new Uri(url.ToString() + "_layouts/15/download.aspx?SourceUrl=" + fileRelativeUrl);
        //Uri fileUrl = new Uri(url.ToString() + "_api/web/getfilebyserverrelativeurl('" + fileRelativeUrl.Replace("'", "''") + "')/$value"); // also works

        String clientIdPrincipal = clientId + "@" + tenantId;
        String resource = SharePointPrincipal + "/" + url.Host + "@" + tenantId;

        // Note: a 'scope' parameter is not required for this authUrl, but 'resource' is required.
        String authUrl = "https://accounts.accesscontrol.windows.net/" + tenantId + "/tokens/OAuth/2";
        String content = "grant_type=client_credentials&client_id=<username>&client_secret=<password>&resource=<resource>";
        content = content.Replace("<username>", clientIdPrincipal);
        content = content.Replace("<password>", clientSecret);
        content = content.Replace("<resource>", resource);

        AuthResult result = GetAuthResult(authUrl, content, access_token_key);
        String accessToken = result.access_token;
        DownloadFile1(fileUrl, accessToken, filename); // pick whichever DownloadFile method floats your boat
    }

    private static AuthResult GetAuthResult(String authUrl, String content, String access_token_key = "access_token", int timeoutSeconds = 10) {
        HttpContent data = new StringContent(content, Encoding.UTF8, "application/x-www-form-urlencoded");
        using (data) {
            using (HttpClient c = new HttpClient()) {
                c.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
                using (HttpResponseMessage res = c.PostAsync(authUrl, data).Result) {
                    HttpStatusCode code = res.StatusCode;
                    String message = res.Content.ReadAsStringAsync().Result;
                    if (code == HttpStatusCode.OK)
                        return ParseMessage(message, access_token_key);

                    throw new Exception("Auth failed. Status code: " + code + " Message: " + message);
                }
            }
        }
    }

    // alternative way using HttpWebRequest
    private static AuthResult GetAuthResult2(String authUrl, String content, String access_token_key = "access_token", int timeoutSeconds = 10) {
        HttpWebRequest req = WebRequest.CreateHttp(authUrl);
        req.AuthenticationLevel = System.Net.Security.AuthenticationLevel.None;
        req.ContentLength = content.Length;
        req.ContentType = "application/x-www-form-urlencoded";
        req.Method = "POST";
        req.Timeout = timeoutSeconds * 1000;
        using (StreamWriter sw = new StreamWriter(req.GetRequestStream(), Encoding.ASCII)) {
            sw.Write(content);
            sw.Close();
        }

        using (WebResponse res = req.GetResponse()) {
            using (StreamReader sr = new StreamReader(res.GetResponseStream(), Encoding.ASCII)) {
                String message = sr.ReadToEnd();
                return ParseMessage(message, access_token_key);
            }
        }
    }

    // this could also be done using a Json library
    private static AuthResult ParseMessage(String message, String access_token_key) {
        Hashtable ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
        char[] trimChars = new [] { '{', '"', '}' };
        String[] arr = message.Split(',');
        for (int i = 0; i < arr.Length; i++) {
            String t = arr[i];
            int x = t.IndexOf(':');
            if (x < 0)
                continue;
            String varName = t.Substring(0, x).Trim(trimChars);
            String value = t.Substring(x + 1).Trim(trimChars);
            ht[varName] = value;
        }

        String accessToken = (String) ht[access_token_key];
        if (accessToken == null)
            throw new Exception(String.Format("Could not find '{0}' in response message: ", access_token_key) + message);

        AuthResult result = new AuthResult();
        result.access_token = accessToken;
        result.resource = (String) ht["resource"];
        result.token_type = (String) ht["token_type"];
        int val = 0;
        if (int.TryParse((String) ht["expires_in"], out val)) result.expires_in = val;
        if (int.TryParse((String) ht["expires_on"], out val))
            result.expires_on = val;
        else
            result.expires_on = (int) (DateTime.UtcNow.AddSeconds(result.expires_in) - AuthResult.EPOCH).TotalSeconds;

        if (int.TryParse((String) ht["not_before"], out val)) result.not_before = val;
        return result;
    }

    private class AuthResult {
        public static readonly DateTime EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        public DateTime NotBefore { get { return EPOCH.AddSeconds(not_before).ToLocalTime(); } }
        public DateTime ExpiresOn { get { return EPOCH.AddSeconds(expires_on).ToLocalTime(); } }

        public int not_before { get; set; }
        public int expires_on { get; set; }
        public String resource { get; set; }

        ///<summary>Indicates the token type value. The only type that the Microsoft identity platform supports is bearer.</summary>
        public String token_type { get; set; }
        ///<summary>The amount of time that an access token is valid (in seconds).</summary>
        public int expires_in { get; set; }
        ///<summary>The access token generated by the authentication server.</summary>
        public String access_token { get; set; }
    }

    public static void DownloadFile1(Uri fileUrl, String accessToken, String filename, int timeoutSeconds = 60) {
        using (var c = new HttpClient()) { // requires reference to System.Net.Http
            c.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
            var req = new HttpRequestMessage();
            req.Headers.Add("Authorization", "Bearer " + accessToken);
            req.Method = HttpMethod.Get;
            req.RequestUri = fileUrl;
            using (HttpResponseMessage res = c.SendAsync(req).Result) {
                if (res.StatusCode == HttpStatusCode.OK) {
                    using (Stream s = res.Content.ReadAsStreamAsync().Result) {
                        using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
                            s.CopyTo(fs);
                            fs.Flush();
                        }
                    }
                }
                else {
                    String message = "Error. Server returned status code: " + res.StatusCode + " (" + (int) res.StatusCode + "). Headers: " + res.Headers.ToString();
                    throw new Exception(message);
                }
            }
        }
    }

    // slight variation to DownloadFile1, but basically the same
    public static void DownloadFile2(Uri fileUrl, String accessToken, String filename, int timeoutSeconds = 60) {
        using (var c = new HttpClient()) {
            c.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
            c.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
            using (HttpResponseMessage res = c.GetAsync(fileUrl).Result) {
                if (res.StatusCode == HttpStatusCode.OK) {
                    using (Stream s = res.Content.ReadAsStreamAsync().Result) {
                        using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
                            s.CopyTo(fs);
                            fs.Flush();
                        }
                    }
                }
                else {
                    String message = "Error. Server returned status code: " + res.StatusCode + " (" + (int) res.StatusCode + "). Headers: " + res.Headers.ToString();
                    throw new Exception(message);
                }
            }
        }
    }

    public static void DownloadFile3(Uri fileUrl, String accessToken, String filename, int timeoutSeconds = 60) {
        HttpWebRequest req = WebRequest.CreateHttp(fileUrl);
        //req.ContinueTimeout = ...;
        //req.ReadWriteTimeout = ...;
        req.Timeout = timeoutSeconds * 1000;
        req.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + accessToken);
        req.Method = "GET";
        try {
            using (var res = (HttpWebResponse) req.GetResponse()) {
                using (Stream s = res.GetResponseStream()) {
                    using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
                        s.CopyTo(fs);
                        fs.Flush();
                    }
                }
            }
        } catch (Exception ex) {
            if (ex is WebException) {
                var we = (WebException) ex;
                String headers = we.Response.Headers.ToString();
                throw new WebException(we.Message + " headers: " + headers, we);
            }
            throw;
        }
    }

    public static void DownloadFile4(Uri fileUrl, String accessToken, String filename) {
        using (WebClient c = new WebClient()) {
            c.Headers.Add("Authorization", "Bearer " + accessToken);
            using (Stream s = c.OpenRead(fileUrl)) {
                using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
                    s.CopyTo(fs);
                    fs.Flush();
                }
            }
        }
    }

    // Helper methods, not used:

    ///<summary>
    ///Makes an http request to the site url in order to read the GUID tenant-id (also called the realm) from the response headers.
    ///</summary>
    public static Guid? GetTenantId(Uri siteUrl, int timeoutSeconds = 10) {
        // the code: url = url.TrimEnd('/') + "/_vti_bin/client.svc"; is not needed
        String url = siteUrl.GetLeftPart(UriPartial.Authority);

        // use HttpClient to avoid Exception when using HttpWebRequest
        using (var c = new HttpClient()) { // requires reference to System.Net.Http
            c.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
            var req = new HttpRequestMessage();

            // "Bearer " without an access token results in StatusCode = Unauthorized (401) and the response headers contain the tenant-id
            req.Headers.Add("Authorization", "Bearer ");
            req.Method = HttpMethod.Get;
            req.RequestUri = new Uri(url);
            using (HttpResponseMessage res = c.SendAsync(req).Result) {
                // HttpStatusCode code = res.StatusCode; // typically Unauthorized
                System.Net.Http.Headers.HttpResponseHeaders h = res.Headers;
                foreach (String s in h.GetValues("WWW-Authenticate")) { // should only have one
                    Guid? g = TryGetGuid(s);
                    if (g.HasValue)
                        return g;
                }
            }
        }
        return null;
    }

    public static Guid? GetTenantId_old(Uri siteUrl, int timeoutSeconds = 10) {
        String url = siteUrl.GetLeftPart(UriPartial.Authority);

        HttpWebRequest req = WebRequest.CreateHttp(url);
        req.Timeout = timeoutSeconds * 1000;
        req.Headers["Authorization"] = "Bearer ";

        String header = null;
        try {
            using (req.GetResponse()) {}
        } catch (WebException e) {
            if (e.Response != null)
                header = e.Response.Headers["WWW-Authenticate"];
        }
        return TryGetGuid(header);
    }

    private static Guid? TryGetGuid(String header) {
        if (String.IsNullOrEmpty(header))
            return null;

        const String bearer = "Bearer realm=\"";
        int bearerIndex = header.IndexOf(bearer, StringComparison.OrdinalIgnoreCase);
        if (bearerIndex < 0)
            return null;

        int x1 = bearerIndex + bearer.Length;
        int x2 = header.IndexOf('"', x1 + 1);
        String realm = (x2 < 0 ? header.Substring(x1) : header.Substring(x1, x2 - x1));

        Guid guid;
        if (Guid.TryParse(realm, out guid))
            return guid;

        return null;
    }
}
}