Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active October 26, 2024 15:48
Show Gist options
  • 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();
}
}
@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