-
-
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(); | |
} | |
} |
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.
I found its quite easy with webclient.downloadstring