Search code examples
xamarin.formsdownloadgoogle-drive-apidropboxonedrive

How to directly download files, including from Google Drive, OneDrive and Dropbox URLs, without API in C# HttpClient


Because I had a lot of trouble working out how to download files from Dropbox, Google Drive and OneDrive URLs in C# Xamarin Forms (without resorting to drive APIs), sharing my solution below.

In the case of Dropbox, there is currently a bug in Xamarin Forms, at least for Visual Studio Mac. I’ve [reported][1] it to Microsoft and provide a workaround here. Dropbox issues two redirects, the second of which has a URL ending in a ‘#’ character (which is not a valid URL character afaik). URL-encoding the ‘#’ solves the problem, at the cost of having to handle redirects yourself instead of relying on auto redirect. If you’re not using Xamarin Forms, you probably can ignore the redirect handling part. Dropbox URLs work correctly in cUrl and Fiddler.

Some transformations on URLs are involved to get direct download links, i.e. without receiving intervening HTML offering a ‘Download’ button. Also in the case of OneDrive, a transformation is required before even checking the validity of the URL. For Google Drive, there may be a file size limit of around 25MB. Sharepoint-based urls are not handled - not sure it's possible without API.


Solution

  • public static void InitialiseHttpClient ()
    // Sets up shared HttpClient in httpClient, if not already done
    // Use of a single HttpClient instance is across entire app is recommended
    {
        try
        {
            if ( httpClient != null )
                return;
    
            // Initialise httpClient
    
            httpClientHandler = new HttpClientHandler
            {
                AllowAutoRedirect = false
            };
    
            httpClient = new HttpClient ( httpClientHandler );
            httpClient.Timeout = TimeSpan.FromSeconds ( 60 );
    
            // Accept binary data
            httpClient.DefaultRequestHeaders.Accept.Add ( new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue ( "application/octet-stream" ) );
        }
        catch ( Exception ex )
        {
            // …
        }   
    }
    
    public static async Task< bool?> UrlExistsAsync ( string url )
    // Returns:
    //              true, if given URL exists
    //              false, if given URL does not exist
    //              null, if timeout
    {
        // Exclude sharepoint.com (OneDrive For Business / Office 365 or Sharepoint)
        if ( url.Contains ( "sharepoint.com" ) )
        {
            Device.BeginInvokeOnMainThread ( async () =>
            {
                await App.NavPage.DisplayAlert ( T.InvalidUrl, T.SharepointAndOneDriveForBusinessAndMicrosoft365NotSupported, T.ButtonOK );
            });
    
    
            return false;
        }
    
        // Check for OneDrive
        if ( url.StartsWith ( "https://1drv.ms/" ) )
        {
            // One Drive
                url = "https://api.onedrive.com/v1.0/shares/u!" + url.ToBase64 () + "/root/content";
        }
    
        bool? result = true;
    
        try
        {
            InitialiseHttpClient ();            // Shared across app
    
            HttpResponseMessage response = await httpClient.SendAsync ( new HttpRequestMessage ( HttpMethod.Head, url ) );
            response = await HandleAnyRedirectsAsync ( url, response );
    
            HttpStatusCode statusCode = response.StatusCode;    
            if ( response.IsSuccessStatusCode )
                return true;
            else
                return false;
        }
        catch ( OperationCanceledException )
        {
            // Timeout
            result = null;
        }
        catch ( Exception ex )
        {
            result = false;
        }
    
        return result;
    }
    
    
    public static string CheckForCloudDriveUrl ( string url )
    // A. If given url is a Google Drive url, returns an equivalent direct url, avoiding initial page with download button
    // Transforms https://drive.google.com/file/d/xxxxx/view?usp=share_link to https://drive.google.com/uc?export=download&id=xxxxx
    // Note: Probably does not work for files that Google considers too large to virus check, around 25MB, as Google will issue a prompt
    
    // B. If given url is a OneDrive url, returns an equivalent direct url
    
    // C. If given url is a Dropbox url, returns an equivalent direct url
    
    // D. Otherwise returns given url
    {
        string [] splits;
        if ( url.StartsWith ( "https://drive.google.com/file" ) )
        {
            // Google Drive
            splits = url.Split ( '/' );
            url = splits [5];
            splits = url.Split ( '/' );
            url = "https://drive.google.com/uc?export=download&id=" + splits [0];
        }
        else if ( url.StartsWith ( "https://1drv.ms/" ) )
        {
            // One Drive
            string base64Value = System.Convert.ToBase64String ( Encoding.UTF8.GetBytes ( url ) );
                    string encodedUrl = "u!" + base64Value.TrimEnd ( '=' ).Replace ( '/','_' ).Replace ( '+','-' );
            url = "https://api.onedrive.com/v1.0/shares/" + encodedUrl + "/root/content";
        }
        else if ( url.StartsWith ( "https://www.dropbox.com/" ) && url.EndsWith ( "?dl=0" ) )
        {
            // Dropbox
                    url = url.Replace ( "?dl=0", "?dl=1" );
        }
    
            return url;
        }
    
    
    public static async Task<byte []> DownloadBinaryFileToArrayAsync ( HttpClient httpClient, string webUrl )
    // Returns byte array from given file URL
    // Assumes InitialiseHttpClient has been initialised
    {
        webUrl = webUrl.Replace ( " ", " ".UrlEncode () );
        webUrl = webUrl.Replace ( "#", "#".UrlEncode () );
    
        byte [] bytes;
    
        try
        {
            // If a Cloud Drive url, transform it to a direct url
            webUrl = CheckForCloudDriveUrl ( webUrl );
    
            HttpResponseMessage response = await httpClient.GetAsync ( webUrl );
                    
            // For the sake of Dropbox, handle any redirects so as to be able to url-encode the '#' that Dropbox sends in its second redirect
            response = await HandleAnyRedirectsAsync ( webUrl, response );
            if ( response == null )
                return null;
    
            using ( MemoryStream memoryStream = new MemoryStream () )
            {
                using ( Stream stream = await response.Content.ReadAsStreamAsync () )
                {
                    stream.CopyTo ( memoryStream );
                }
                memoryStream.Seek ( 0, SeekOrigin.Begin );
                bytes = memoryStream.ToArray ();
            }
    
            return bytes;
        }
        catch ( Exception ex )
        {
            // …
        }
    }
    
    public static async Task<HttpResponseMessage> HandleAnyRedirectsAsync ( string webUrl, HttpResponseMessage response )
    // For the sake of Dropbox, handles any redirects so as to be able to url-encode the '#' that Dropbox sends in its second redirect
    // Returns possibly updated response (updated if there were redirects)
    // Returns null if error
    // Checks for unexpected status codes
    // webUrl = initial url
    // response = response to initial http get
    {
        if ( response.StatusCode == HttpStatusCode.OK )
            return response;
    
        int redirectCount = 0;
        while ( redirectCount < 20 )
        {
            if ( response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.SeeOther )
            {
                string redirectUrl;
    
                if ( response.Headers != null && response.Headers.Location != null )
                {
                    if ( response.Headers.Location.IsAbsoluteUri )
                        redirectUrl = response.Headers.Location.AbsoluteUri;
                    else
                    {
                        string[] splits = webUrl.Split ('/' );
                        redirectUrl = "https://" + splits [2] + response.Headers.Location.OriginalString;
                    }
    
                    redirectUrl = redirectUrl.Replace ( "#", "#".UrlEncode () );
                    response = await httpClient.GetAsync ( redirectUrl );
                    if ( response.StatusCode == HttpStatusCode.OK )
                        break;
    
                    if ( ! ( response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.SeeOther ) )
                    {
                        // Unexpected status code
                        await Device.InvokeOnMainThreadAsync ( async () =>
                        {
                            await App.NavPage.DisplayAlert  ( T.Error, T.FileTransferError, T.ButtonOK );
                        });
                        return null;
                    }
                }
                redirectCount++;
            }
            else
            {
                // Unexpected status code
                await Device.InvokeOnMainThreadAsync ( async () =>
                {
                    await App.NavPage.DisplayAlert  ( T.Error, T.FileTransferError, T.ButtonOK );
                });
                return null;
            }
        }
    
        return response;
    }
    
    public static string ToBase64 ( this string s )
    // Returns Base64 equivalent of given string
    {
        byte [] plainTextBytes = System.Text.Encoding.UTF8.GetBytes ( s );
        return System.Convert.ToBase64String ( plainTextBytes );
    }
    
    
    public static String UrlEncode ( this String str )
    // Returns URLencoded equivalent of given string
    {
        return HttpUtility.UrlEncode ( str );
    }
    
    public static String UrlEncode ( this String str, Encoding e )
    // Returns URL encoded equivalent of given string, using given encoding
    {
        return HttpUtility.UrlEncode ( str, e );
    }