Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active April 27, 2024 22:12
Show Gist options
  • Star 64 You must be signed in to star a gist
  • Fork 36 You must be signed in to fork a gist
  • Save yasirkula/d0ec0c07b138748e5feaecbd93b6223c to your computer and use it in GitHub Desktop.
Save yasirkula/d0ec0c07b138748e5feaecbd93b6223c to your computer and use it in GitHub Desktop.
C# Download Public File From Google Drive™ (works for large files as well)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;
/* EXAMPLE USAGE
FileDownloader fileDownloader = new FileDownloader();
// This callback is triggered for DownloadFileAsync only
fileDownloader.DownloadProgressChanged += ( sender, e ) => Console.WriteLine( "Progress changed " + e.BytesReceived + " " + e.TotalBytesToReceive );
// This callback is triggered for both DownloadFile and DownloadFileAsync
fileDownloader.DownloadFileCompleted += ( sender, e ) =>
{
if( e.Cancelled )
Console.WriteLine( "Download cancelled" );
else if( e.Error != null )
Console.WriteLine( "Download failed: " + e.Error );
else
Console.WriteLine( "Download completed" );
};
fileDownloader.DownloadFileAsync( "https://INSERT_DOWNLOAD_LINK_HERE", @"C:\downloadedFile.txt" );
*/
public class FileDownloader : IDisposable
{
private const string GOOGLE_DRIVE_DOMAIN = "drive.google.com";
private const string GOOGLE_DRIVE_DOMAIN2 = "https://drive.google.com";
// In the worst case, it is necessary to send 3 download requests to the Drive address
// 1. an NID cookie is returned instead of a download_warning cookie
// 2. download_warning cookie returned
// 3. the actual file is downloaded
private const int GOOGLE_DRIVE_MAX_DOWNLOAD_ATTEMPT = 3;
public delegate void DownloadProgressChangedEventHandler( object sender, DownloadProgress progress );
// Custom download progress reporting (needed for Google Drive)
public class DownloadProgress
{
public long BytesReceived, TotalBytesToReceive;
public object UserState;
public int ProgressPercentage
{
get
{
if( TotalBytesToReceive > 0L )
return (int) ( ( (double) BytesReceived / TotalBytesToReceive ) * 100 );
return 0;
}
}
}
// Web client that preserves cookies (needed for Google Drive)
private class CookieAwareWebClient : WebClient
{
private class CookieContainer
{
private readonly Dictionary<string, string> cookies = new Dictionary<string, string>();
public string this[Uri address]
{
get
{
string cookie;
if( cookies.TryGetValue( address.Host, out cookie ) )
return cookie;
return null;
}
set
{
cookies[address.Host] = value;
}
}
}
private readonly CookieContainer cookies = new CookieContainer();
public DownloadProgress ContentRangeTarget;
protected override WebRequest GetWebRequest( Uri address )
{
WebRequest request = base.GetWebRequest( address );
if( request is HttpWebRequest )
{
string cookie = cookies[address];
if( cookie != null )
( (HttpWebRequest) request ).Headers.Set( "cookie", cookie );
if( ContentRangeTarget != null )
( (HttpWebRequest) request ).AddRange( 0 );
}
return request;
}
protected override WebResponse GetWebResponse( WebRequest request, IAsyncResult result )
{
return ProcessResponse( base.GetWebResponse( request, result ) );
}
protected override WebResponse GetWebResponse( WebRequest request )
{
return ProcessResponse( base.GetWebResponse( request ) );
}
private WebResponse ProcessResponse( WebResponse response )
{
string[] cookies = response.Headers.GetValues( "Set-Cookie" );
if( cookies != null && cookies.Length > 0 )
{
int length = 0;
for( int i = 0; i < cookies.Length; i++ )
length += cookies[i].Length;
StringBuilder cookie = new StringBuilder( length );
for( int i = 0; i < cookies.Length; i++ )
cookie.Append( cookies[i] );
this.cookies[response.ResponseUri] = cookie.ToString();
}
if( ContentRangeTarget != null )
{
string[] rangeLengthHeader = response.Headers.GetValues( "Content-Range" );
if( rangeLengthHeader != null && rangeLengthHeader.Length > 0 )
{
int splitIndex = rangeLengthHeader[0].LastIndexOf( '/' );
if( splitIndex >= 0 && splitIndex < rangeLengthHeader[0].Length - 1 )
{
long length;
if( long.TryParse( rangeLengthHeader[0].Substring( splitIndex + 1 ), out length ) )
ContentRangeTarget.TotalBytesToReceive = length;
}
}
}
return response;
}
}
private readonly CookieAwareWebClient webClient;
private readonly DownloadProgress downloadProgress;
private Uri downloadAddress;
private string downloadPath;
private bool asyncDownload;
private object userToken;
private bool downloadingDriveFile;
private int driveDownloadAttempt;
public event DownloadProgressChangedEventHandler DownloadProgressChanged;
public event AsyncCompletedEventHandler DownloadFileCompleted;
public FileDownloader()
{
webClient = new CookieAwareWebClient();
webClient.DownloadProgressChanged += DownloadProgressChangedCallback;
webClient.DownloadFileCompleted += DownloadFileCompletedCallback;
downloadProgress = new DownloadProgress();
}
public void DownloadFile( string address, string fileName )
{
DownloadFile( address, fileName, false, null );
}
public void DownloadFileAsync( string address, string fileName, object userToken = null )
{
DownloadFile( address, fileName, true, userToken );
}
private void DownloadFile( string address, string fileName, bool asyncDownload, object userToken )
{
downloadingDriveFile = address.StartsWith( GOOGLE_DRIVE_DOMAIN ) || address.StartsWith( GOOGLE_DRIVE_DOMAIN2 );
if( downloadingDriveFile )
{
address = GetGoogleDriveDownloadAddress( address );
driveDownloadAttempt = 1;
webClient.ContentRangeTarget = downloadProgress;
}
else
webClient.ContentRangeTarget = null;
downloadAddress = new Uri( address );
downloadPath = fileName;
downloadProgress.TotalBytesToReceive = -1L;
downloadProgress.UserState = userToken;
this.asyncDownload = asyncDownload;
this.userToken = userToken;
DownloadFileInternal();
}
private void DownloadFileInternal()
{
if( !asyncDownload )
{
webClient.DownloadFile( downloadAddress, downloadPath );
// This callback isn't triggered for synchronous downloads, manually trigger it
DownloadFileCompletedCallback( webClient, new AsyncCompletedEventArgs( null, false, null ) );
}
else if( userToken == null )
webClient.DownloadFileAsync( downloadAddress, downloadPath );
else
webClient.DownloadFileAsync( downloadAddress, downloadPath, userToken );
}
private void DownloadProgressChangedCallback( object sender, DownloadProgressChangedEventArgs e )
{
if( DownloadProgressChanged != null )
{
downloadProgress.BytesReceived = e.BytesReceived;
if( e.TotalBytesToReceive > 0L )
downloadProgress.TotalBytesToReceive = e.TotalBytesToReceive;
DownloadProgressChanged( this, downloadProgress );
}
}
private void DownloadFileCompletedCallback( object sender, AsyncCompletedEventArgs e )
{
if( !downloadingDriveFile )
{
if( DownloadFileCompleted != null )
DownloadFileCompleted( this, e );
}
else
{
if( driveDownloadAttempt < GOOGLE_DRIVE_MAX_DOWNLOAD_ATTEMPT && !ProcessDriveDownload() )
{
// Try downloading the Drive file again
driveDownloadAttempt++;
DownloadFileInternal();
}
else if( DownloadFileCompleted != null )
DownloadFileCompleted( this, e );
}
}
// Downloading large files from Google Drive prompts a warning screen and requires manual confirmation
// Consider that case and try to confirm the download automatically if warning prompt occurs
// Returns true, if no more download requests are necessary
private bool ProcessDriveDownload()
{
FileInfo downloadedFile = new FileInfo( downloadPath );
if( downloadedFile == null )
return true;
// Confirmation page is around 50KB, shouldn't be larger than 60KB
if( downloadedFile.Length > 60000L )
return true;
// Downloaded file might be the confirmation page, check it
string content;
using( var reader = downloadedFile.OpenText() )
{
// Confirmation page starts with <!DOCTYPE html>, which can be preceeded by a newline
char[] header = new char[20];
int readCount = reader.ReadBlock( header, 0, 20 );
if( readCount < 20 || !( new string( header ).Contains( "<!DOCTYPE html>" ) ) )
return true;
content = reader.ReadToEnd();
}
int linkIndex = content.LastIndexOf( "href=\"/uc?" );
if( linkIndex >= 0 )
{
linkIndex += 6;
int linkEnd = content.IndexOf( '"', linkIndex );
if( linkEnd >= 0 )
{
downloadAddress = new Uri( "https://drive.google.com" + content.Substring( linkIndex, linkEnd - linkIndex ).Replace( "&amp;", "&" ) );
return false;
}
}
int formIndex = content.LastIndexOf( "<form id=\"download-form\"" );
if( formIndex >= 0 )
{
int formEndIndex = content.IndexOf( "</form>", formIndex + 10 );
int inputIndex = formIndex;
StringBuilder sb = new StringBuilder().Append( "https://drive.usercontent.google.com/download" );
bool isFirstArgument = true;
while( ( inputIndex = content.IndexOf( "<input type=\"hidden\"", inputIndex + 10 ) ) >= 0 && inputIndex < formEndIndex )
{
linkIndex = content.IndexOf( "name=", inputIndex + 10 ) + 6;
sb.Append( isFirstArgument ? '?' : '&' ).Append( content, linkIndex, content.IndexOf( '"', linkIndex ) - linkIndex ).Append( '=' );
linkIndex = content.IndexOf( "value=", linkIndex ) + 7;
sb.Append( content, linkIndex, content.IndexOf( '"', linkIndex ) - linkIndex );
isFirstArgument = false;
}
downloadAddress = new Uri( sb.ToString() );
return false;
}
return true;
}
// Handles the following formats (links can be preceeded by https://):
// - drive.google.com/open?id=FILEID&resourcekey=RESOURCEKEY
// - drive.google.com/file/d/FILEID/view?usp=sharing&resourcekey=RESOURCEKEY
// - drive.google.com/uc?id=FILEID&export=download&resourcekey=RESOURCEKEY
private string GetGoogleDriveDownloadAddress( string address )
{
int index = address.IndexOf( "id=" );
int closingIndex;
if( index > 0 )
{
index += 3;
closingIndex = address.IndexOf( '&', index );
if( closingIndex < 0 )
closingIndex = address.Length;
}
else
{
index = address.IndexOf( "file/d/" );
if( index < 0 ) // address is not in any of the supported forms
return string.Empty;
index += 7;
closingIndex = address.IndexOf( '/', index );
if( closingIndex < 0 )
{
closingIndex = address.IndexOf( '?', index );
if( closingIndex < 0 )
closingIndex = address.Length;
}
}
string fileID = address.Substring( index, closingIndex - index );
index = address.IndexOf( "resourcekey=" );
if( index > 0 )
{
index += 12;
closingIndex = address.IndexOf( '&', index );
if( closingIndex < 0 )
closingIndex = address.Length;
string resourceKey = address.Substring( index, closingIndex - index );
return string.Concat( "https://drive.google.com/uc?id=", fileID, "&export=download&resourcekey=", resourceKey, "&confirm=t" );
}
else
return string.Concat( "https://drive.google.com/uc?id=", fileID, "&export=download&confirm=t" );
}
public void Dispose()
{
webClient.Dispose();
}
}
@ebleme
Copy link

ebleme commented May 18, 2021

@yasirkula I tried it today. It was working well. But after a while, it stopped working. It gives nothing on DownloadFileCompleted event. on downloadProcess 0 bytes received. When I copy-pasted downloadAddress on the browser it downloads.

Is it a Google issue? I have 27 images at 22-220 KB size range on google drive. My app made approximately 100 calls on a short time. Is Google bans such requests ...

image

Edit:
I could download my images from google drive using the downloadAddress FileDownloader GetGoogleDriveDownloadAddress() method generated.

    FileDownloader fileDownloader = new FileDownloader();
    string downloadAddress = fileDownloader.GetGoogleDriveDownloadAddress(category.ImageUrl);

    UnityWebRequest www = UnityWebRequestTexture.GetTexture(downloadAddress);

    yield return www.SendWebRequest();

    if (www.result == UnityWebRequest.Result.Success)
    {
       // Lines below are my custom lines of codes which saves images and sets as a texture to a RawImage 
        string path = fileHandler.GetCategoryImagePath(category.Id + ".jpg"); 
        fileHandler.SaveAppImage(path, www.downloadHandler.data); 
        image.texture = fileHandler.GetCategoryImage(category.Id + ".jpg");  
    }

@yasirkula
Copy link
Author

yasirkula commented May 18, 2021

Can you download the file in an incognito browser tab while you aren't logged in to your Google account? If so, can you check if the FileDownloader issue still persists?

@ebleme
Copy link

ebleme commented May 22, 2021

Yes, I could download the file in an incognito browser tab while I am not logged in to my Google account. I changed my approach to download the files, I decreased request count. With this approach your script continues to work flawlessly. Thank you @yasirkula

@jcasement
Copy link

@yasirkula Is it possible to get the filename as it exists on the google drive? and use that name for saving locally?
By that I mean using this address:
https://drive.google.com/file/d/1U0O-qJ7UxOts6M1eG0vW-eSNsm1OO87e/view?usp=sharing
really has this single file:
Sonata-Arctica_Victoria's-Secret_v2_p.psarc

I'd like to save it to that name locally.
BTW -- this code is fantastic

@yasirkula
Copy link
Author

You can give this solution a shot: https://stackoverflow.com/a/54616044/2373034

@jcasement
Copy link

Nope -- GetResponse headers is empty -- thanks anyway -- I'll try something else -- probably web client code

@jcasement
Copy link

@yasirkula -- use HAP to solve the named file
HtmlWeb web = new HtmlWeb();
HtmlDocument document = web.Load("https://drive.google.com/file/d/1U0O-qJ7UxOts6M1eG0vW-eSNsm1OO87e/view?usp=sharing");

            string _token = document.DocumentNode.Descendants("meta")
                .Where(node => node.Attributes["property"] != null && node.Attributes["property"].Value == "og:title")
                .Select(node => node.Attributes["content"].Value)
                .DefaultIfEmpty(string.Empty)
                .FirstOrDefault()
                .ToString();

            var namedfle = HttpUtility.HtmlDecode(_token);

@jcasement
Copy link

@yasirkula
For those of you getting the 403 error, yes, as stated, there is a bandwidth limit as far as I can tell.
I'm doing a large number of files downloads with files that average 10MB each.
I'm getting either 62 files downloaded or a max of just over 500MB, whichever comes first.
So did the ridiculous, when I get the 403 I use a
await Task.Delay(ConvertMinutesToMilliseconds(30));

and resume and loop the process.
Yes -- this will take forever but I don't see a way out.

@derekantrican
Copy link

derekantrican commented Feb 15, 2022

Looks like this script stopped working a couple days ago. Now the contents of the file downloaded is just the html of the original download warning page

Looks like the issue is content.LastIndexOf("href=\"/uc?") is returning -1. Maybe the html page from Google Drive has changed? Here is the response I am getting back (formatted): https://pastebin.com/raw/bvRMQ8kU

@yasirkula looking at the download page for my link and at the response in the code, my guess is that the <form id="downloadForm"... gets replaced by the <a id="uc-download-link"... by some javascript that isn't being triggered by the C# WebClient. I don't know if there's a way around that without switching to something like System.Windows.Forms.WebBrowser or Selenium

@yasirkula
Copy link
Author

@derekantrican I'll test it soon, thanks for bringing it to my attention.

@yasirkula
Copy link
Author

@derekantrican Can you give the latest version a try? The link you pass to it must now contain &resourcekey=RESOURCEKEY, there's unfortunately no other way. So if you were using old share links which didn't contain &resourcekey=RESOURCEKEY, you'll have to refresh your links.

@derekantrican
Copy link

@yasirkula Yes, the latest version works! I think by resourcekey you're referring to the security update here: https://support.google.com/a/answer/10685032. I couldn't find a way to get my file's link with a resourcekey attached, but with the latest revision you uploaded the old url works just fine. Thanks!

@yasirkula
Copy link
Author

@derekantrican You're welcome! The file's link comes with resourceKey when you right click it and select Get Link or via some Drive extensions. How do you get your file's link?

@derekantrican
Copy link

I use "Get Link" like you were suggesting (or going through the sharing menu) but neither gave me a "resourcekey link". Maybe it's only for newly-created files?

@yasirkula
Copy link
Author

It generates links with resourcekey for very old files, as well. Perhaps it's not the case for files in shared drives, I'm testing with files in my personal Drive.

@hnsnls
Copy link

hnsnls commented Feb 21, 2022

@yasirkula,
Thank you very much for your adjustment! You are helping me out again with it.

@Tony91ans
Copy link

Thanks for the fix

Copy link

ghost commented Mar 3, 2022

the function
public void DownloadFileAsync(string address, string fileName, object userToken = null)

does not downloads asynchronously as you've just called the DownloadFile method inside its body. My program fails to await for its completion, it just calls this method and moves ahead.

@yasirkula
Copy link
Author

@Usama-AZIZ You need to register to its DownloadFileCompleted event as shown in the example code (line 8).

@derekantrican
Copy link

derekantrican commented Mar 3, 2022

@Usama-AZIZ you're correct that this function does not follow the standard async-await pattern. This is using the WebClient.DownloadFileAsync method from Microsoft which does not follow the standard conventions: https://docs.microsoft.com/en-us/dotnet/api/system.net.webclient.downloadfileasync?view=net-6.0.

If you want an awaitable method you can change the method signature from void to async Task and the method await webClient.DownloadFileTaskAsync (note "Task" in the name) is awaitable. Of course, there's some other hookups/changes that will have to be done in between those two methods (eg DownloadFile, DownloadFileInternal, etc) but those are the two main things that would need to change.

@ActionX
Copy link

ActionX commented Feb 17, 2023

Not quite the right way to work with Web requests. It is necessary to focus on the response codes and headers. In this case, it will be possible to get a file with the extension if we do not know what we are downloading.

@steve-pence
Copy link

steve-pence commented May 17, 2023

In my testing, the callback DownloadFileCompleted is being called when the download is NOT completed. If I put my client in airplane mode before starting the download, I wind up with a 0 byte file and a call to DownloadFileCompleted.

Sorry, I see now that the callback always gets made, and I need to check e.Error and e.Cancelled and handle accordingly. You might consider updating (or at least commenting) your usage example. Thanks!

@yasirkula
Copy link
Author

Thank you! I've updated the example code accordingly.

@joshkhali
Copy link

Is there a way to get links specifically for the folder and not the subfolders ?

@lhuclkabay2110
Copy link

lhuclkabay2110 commented Jun 19, 2023 via email

@yasirkula
Copy link
Author

@joshkhali I've replied via e-mail.

@steve-pence
Copy link

I've been using the downloader as part of a project for a number of months with great results and no issues. We are downloading a small xml file from Google drive to a number of client installs. Very simple implementation. The middle of this month suddenly all our clients are unable to parse the downloaded xml. Turns out that instead of the contents of our file being downloaded, Google is replacing "our" contents with some html containing a virus warning and a scary message about how this kind of file can cause harm. So the file is downloaded just fine, but has unexpected contents. We are also now seeing, as new behaivior, that when the same file is downloaded interactively, e.g. from chrome, Google now is putting up a danger dialog asking for user confirmation.
I realize that officially this is not your problem. The file is being downloaded as requested. But do you have a suggesstion? E.g. is there a url tweak we can use to bypass this new warning and tell Google that we really do know what we are doing?

Thanks much,
Steve

@yasirkula
Copy link
Author

@steve-pence I've pushed an update 3 weeks ago to resolve a similar issue. If you didn't update the code during that time, could you give it a shot?

@steve-pence
Copy link

steve-pence commented Jan 31, 2024 via email

@steve-pence
Copy link

steve-pence commented Feb 1, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment