-
-
Save yasirkula/d0ec0c07b138748e5feaecbd93b6223c to your computer and use it in GitHub Desktop.
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( "&", "&" ) ); | |
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(); | |
} | |
} |
Is there a way to get links specifically for the folder and not the subfolders ?
@joshkhali I've replied via e-mail.
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
@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?
Newbie alert here.
A bit more explanation throughout the code would be helpful although much of it is self explanatory.
Question.
Is there a good reason why you aren't using the Google drive api?
Thanks
I've also noticed that Drive now shows the "can't scan this file for viruses" warning for XML files for unknown reasons. As you've guessed, FileDownloader programmatically clicks "Download anyway" button to skip this dialog and start the download. Happy to hear that it was useful for you.
For this newbie what on what line does the filedownloader programmatically click download anyway?
Thankya
@BECCAKTN I'm not using Drive API because it requires importing Drive API, having an access key and being restricted by the daily quota of that access key (though the quota is usually very generous). Actually I don't know if Drive API would have any effect while downloading files from other people's Drive storage. It might be helpful only for downloading files from your own account.
ProcessDriveDownload function as a whole handles clicking the download button programmatically.
@BECCAKTN I'm not using Drive API because it requires importing Drive API, having an access key and being restricted by the daily quota of that access key (though the quota is usually very generous). Actually I don't know if Drive API would have any effect while downloading files from other people's Drive storage. It might be helpful only for downloading files from your own account.
ProcessDriveDownload function as a whole handles clicking the download button programmatically.
Thank you!!
So just so I know what's happening here.
It looks like Google wants us to use their api which requires the use of credentials and your code here is a 'work around' to be able to bypass some of their restrictions and my concern is eventually they will make changes to prevent this from happening?
Yes they can make changes to render this code obsolete but as long as we can download a public Drive file anonymously using our browser, I believe we can keep replicating that behaviour in code.
Yes they can make changes to render this code obsolete but as long as we can download a public Drive file anonymously using our browser, I believe we can keep replicating that behaviour in code.
Thank you once again.
My goodness!!
with my limited experience I was able to add your class to my spaghetti and it came out like prime rib!!!
well done my friend.
Im using a winform and once i passed the shareable link in using an app.config it was flawless. all I need to do is tie in a progessbar into the DownloadProgressChangedEventHandler and im right as rain
ok if i call Downloadfile i get everything i need but no progresschanged event is fired.
if I call Downloadfileasync i get partial file (total size is about 500mb and I get 130 ish mb) and progresschanged event works great.
obvious question, how can i call the async version an get the whole .zip file?
thx
ok if i call Downloadfile i get everything i need but no progresschanged event is fired. if I call Downloadfileasync i get partial file (total size is about 500mb and I get 130 ish mb) and progresschanged event works great. obvious question, how can i call the async version an get the whole .zip file? thx
this gives my whole file but no progress changed event..
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));
@BECCAKTN I was able to reproduce the issue with a 300 MB download. It downloaded only 50 MB for both async and sync solutions. But, I've tried downloading the file from Drive using my web browser and surprisingly, my browser was also able to download only 50 MB. So I guess it's a bug on Drive-side. Can you try downloading using your browser? I've tested on Chrome.
Hey yasirkula is it easy to implement a new feature to read a simple text file on gdrive without downloading it like httpwebrequest.create to .Get response and read it with streamreader?
I have a winform that uses a txt file and reads it for a version number and if an update is needed it goes ahead and uses what you already made to download the files
Thx!
@BECCAKTN Hi! Our web browsers can preview text files but I don't know how to accomplish it in code.
Yasirkula I want to thank you for your code here.
It has enabled me to work with Google drive from code.
Actually I now do not directly use your code at run time but I use it as a tool to get the real download able url because as the dowloadfileinternal method is done with things, like the 3rd time, I get the url that I need at run time which I just put that value in my app.config.
That download link can expire after a short time or not work at all due to missing cookies. I'd recommend attempting to download a large file (> 50MB) from one such url after 10 minutes.
@BECCAKTN Hi! Our web browsers can preview text files but I don't know how to accomplish it in code.
I found its quite easy with webclient.downloadstring
That download link can expire after a short time or not work at all due to missing cookies. I'd recommend attempting to download a large file (> 50MB) from one such url after 10 minutes.
It's been a week so far and still works with a 500mb file lol
@BECCAKTN Glad to hear it :)
Hi, great tool to download from gDrive.
Is it possible to download multiple files async and wait for all to finish before proceeding with the execution of a ButtonClick event with WPF?
Click Button -> download file 1 -> download file 2 -> ... -> proceed with the rest of the click action?
I'm just learning C# and can't figure out a good way.
@Aergernis In my opinion the simplest way to work with async operations is to use C# Tasks. You can use TaskCompletionSource to convert FileDownloader's async job to an actual Task. Then you can use all sorts of helper functions like Task.WaitAll
.
Thanks for the fast response and the hint with TaskCompletionSource.
Learned something new :)
The solution i came up with is
public Task DownloadTask(string address, string fileName)
{
var tcs = new TaskCompletionSource<object>();
FileDownloader fileDownloader = new FileDownloader();
stopwatch.Start();
fileDownloader.DownloadFileAsync(address, fileName);
fileDownloader.DownloadProgressChanged += (sender, e) =>
{
string downloadProgress = e.ProgressPercentage + "%";
string downloadSpeed = string.Format("{0} MB/s", (e.BytesReceived / 1024.0 / 1024.0 / stopwatch.Elapsed.TotalSeconds).ToString("0.00"));
string downloadedMBs = Math.Round(e.BytesReceived / 1024.0 / 1024.0) + " MB";
string totalMBs = Math.Round(e.TotalBytesToReceive / 1024.0 / 1024.0) + " MB";
string progress = $"{fileName.Split("\\").Last()}: {downloadedMBs} / {totalMBs} ({downloadProgress}) @ {downloadSpeed}";
progressBar1.Value = e.ProgressPercentage;
progressBar1.Update();
labelProgress.Invoke((MethodInvoker)(() => labelProgress.Text = progress));
};
fileDownloader.DownloadFileCompleted += (sender, e) =>
{
if (e.Cancelled)
{
stopwatch.Reset();
buttonDownload.Enabled = true;
tcs.SetResult(null);
MessageBox.Show("Download cancelled");
}
else if (e.Error != null)
{
stopwatch.Reset();
buttonDownload.Enabled = true;
tcs.SetResult(null);
MessageBox.Show("Download failed: " + e.Error);
}
else
{
stopwatch.Reset();
buttonDownload.Enabled = true;
tcs.SetResult(null);
}
};
return tcs.Task;
}
private async void button1_Click(object sender, EventArgs e)
{
await DownloadTask("https://drive.google.com/file/d/xxxx/view?usp=sharing", fil.zip));
}
Any suggestions if i can make something better?
@Aergernis Your code looks acceptable to me, well done.
Thank you! I've updated the example code accordingly.