diff --git a/src/FileDownload.cpp b/src/FileDownload.cpp new file mode 100644 index 0000000000..852f76d32b --- /dev/null +++ b/src/FileDownload.cpp @@ -0,0 +1,348 @@ +#include "global.h" + +#if !defined(WITHOUT_NETWORKING) +#include "FileDownload.h" +#include "RageUtil.h" +#include "RageFileManager.h" +#include "SpecialFiles.h" +#include "RageLog.h" + +FileDownload::FileDownload() +{ + m_iPackagesPos = 0; + m_iLinksPos = 0; + m_bIsDownloading = false; + m_sStatus = ""; + m_fLastUpdate = 0; + m_iTotalBytes = 0; + m_iDownloaded = 0; + m_bFinished = false; + + UpdateProgress(); + + // Workaround: For some reason, the first download sometimes corrupts; + // by opening and closing the RageFile, this problem does not occur. + // Go figure? + + // XXX: This is a really dirty work around! Why does RageFile do this? + + // It's always some strange number of bytes at the end of the + // file when it corrupts. + m_fOutputFile.Open( "Packages/dummy.txt", RageFile::WRITE ); + m_fOutputFile.Close(); +} + +FileDownload::~FileDownload() +{ + m_fOutputFile.Close(); +} + +RString FileDownload::Update( float fDeltaTime ) +{ + HTTPUpdate(); + + m_fLastUpdate += fDeltaTime; + if ( m_fLastUpdate >= 1.0 ) + { + if ( m_bIsDownloading && m_bGotHeader ) + m_sStatus = ssprintf( "DL @ %d KB/s", int((m_iDownloaded-m_bytesLastUpdate)/1024) ); + + m_bytesLastUpdate = m_iDownloaded; + UpdateProgress(); + m_fLastUpdate = 0; + } + + return m_sStatus; +} + +void FileDownload::UpdateProgress() +{ + float DownloadedRatio; + + if( m_iTotalBytes < 1 ) + DownloadedRatio = 0; + else + DownloadedRatio = float(m_iDownloaded) / float(m_iTotalBytes); +} + +void FileDownload::CancelDownload( ) +{ + m_wSocket.close(); + m_bIsDownloading = false; + m_iDownloaded = 0; + m_bGotHeader = false; + m_fOutputFile.Close(); + m_sStatus = "Failed."; + m_sBUFFER = ""; + FILEMAN->Remove( "Packages/" + m_sEndName ); +} + +void FileDownload::StartDownload( const RString &sURL, const RString &sDestFile ) +{ + RString Proto; + RString Server; + int Port=80; + RString sAddress; + + if( !ParseHTTPAddress( sURL, Proto, Server, Port, sAddress ) ) + { + m_sStatus = "Invalid URL."; + UpdateProgress(); + return; + } + + //Determine if this is a website, or a package? + //Criteria: does it end with *zip? + if( sAddress.Right(3).CompareNoCase("zip") == 0 ) + m_bIsPackage=true; + else + m_bIsPackage = false; + + m_sBaseAddress = "http://" + Server; + if( Port != 80 ) + m_sBaseAddress += ssprintf( ":%d", Port ); + m_sBaseAddress += "/"; + + if( sAddress.Right(1) != "/" ) + { + m_sEndName = Basename( sAddress ); + m_sBaseAddress += Dirname( sAddress ); + } + else + { + m_sEndName = ""; + } + + //Open the file... + + //First find out if a file by this name already exists + //if so, then we gotta ditch out. + //XXX: This should be fixed by a prompt or something? + + //if we are not talking about a file, let's not worry + if( m_sEndName != "" && m_bIsPackage ) + { + vector AddTo; + GetDirListing( "Packages/"+m_sEndName, AddTo, false, false ); + if ( AddTo.size() > 0 ) + { + m_sStatus = "File Already Exists"; + UpdateProgress(); + return; + } + + if( !m_fOutputFile.Open( sDestFile, RageFile::WRITE | RageFile::STREAMED ) ) + { + m_sStatus = m_fOutputFile.GetError(); + UpdateProgress(); + return; + } + } + //Continue... + + sAddress = URLEncode( sAddress ); + + if ( sAddress != "/" ) + sAddress = "/" + sAddress; + + m_wSocket.close(); + m_wSocket.create(); + + m_wSocket.blocking = true; + + if( !m_wSocket.connect( Server, (short) Port ) ) + { + m_sStatus = "Failed to connect."; + UpdateProgress(); + return; + } + + //Produce HTTP header + + RString Header=""; + + Header = "GET "+sAddress+" HTTP/1.0\r\n"; + Header+= "Host: " + Server + "\r\n"; + Header+= "Connection: closed\r\n\r\n"; + + m_wSocket.SendData( Header.c_str(), Header.length() ); + m_sStatus = "Header Sent."; + m_wSocket.blocking = false; + m_bIsDownloading = true; + m_sBUFFER = ""; + m_bGotHeader = false; + UpdateProgress(); +} + +static size_t FindEndOfHeaders( const RString &buf ) +{ + size_t iPos1 = buf.find( "\n\n" ); + size_t iPos2 = buf.find( "\r\n\r\n" ); + LOG->Trace("end: %u, %u", unsigned(iPos1), unsigned(iPos2)); + if( iPos1 != string::npos && (iPos2 == string::npos || iPos2 > iPos1) ) + return iPos1 + 2; + else if( iPos2 != string::npos && (iPos1 == string::npos || iPos1 > iPos2) ) + return iPos2 + 4; + else + return string::npos; +} + +void FileDownload::HTTPUpdate() +{ + if( !m_bIsDownloading ) + return; + + int BytesGot=0; + //Keep this as a code block + //as there may be need to "if" it out some time. + /* If you need a conditional for a large block of code, stick it in + * a function and return. */ + while(1) + { + char Buffer[1024]; + int iSize = m_wSocket.ReadData( Buffer, 1024 ); + if( iSize <= 0 ) + break; + + m_sBUFFER.append( Buffer, iSize ); + BytesGot += iSize; + } + + if( !m_bGotHeader ) + { + m_sStatus = "Waiting for header."; + //We don't know if we are using unix-style or dos-style + size_t iHeaderEnd = FindEndOfHeaders( m_sBUFFER ); + if( iHeaderEnd == m_sBUFFER.npos ) + return; + + // "HTTP/1.1 200 OK" + size_t i = m_sBUFFER.find(" "); + size_t j = m_sBUFFER.find(" ",i+1); + size_t k = m_sBUFFER.find("\n",j+1); + if ( i == string::npos || j == string::npos || k == string::npos ) + { + m_iResponseCode = -100; + m_sResponseName = "Malformed response."; + return; + } + m_iResponseCode = atoi(m_sBUFFER.substr(i+1,j-i).c_str()); + m_sResponseName = m_sBUFFER.substr( j+1, k-j ); + + i = m_sBUFFER.find("Content-Length:"); + j = m_sBUFFER.find("\n", i+1 ); + + if( i != string::npos ) + m_iTotalBytes = atoi(m_sBUFFER.substr(i+16,j-i).c_str()); + else + m_iTotalBytes = -1; //We don't know, so go until disconnect + + m_bGotHeader = true; + m_sBUFFER.erase( 0, iHeaderEnd ); + } + + if( m_bIsPackage ) + { + m_iDownloaded += m_sBUFFER.length(); + m_fOutputFile.Write( m_sBUFFER ); + + // HACK: We're ending up with incomplete files after close() if we don't flush here. Why? + m_fOutputFile.Flush(); + + m_sBUFFER = ""; + } + else + { + m_iDownloaded = m_sBUFFER.length(); + } + + if ( ( m_iTotalBytes <= m_iDownloaded && m_iTotalBytes != -1 ) || + //We have the full doc. (And we knew how big it was) + ( m_iTotalBytes == -1 && + ( m_wSocket.state == EzSockets::skERROR || m_wSocket.state == EzSockets::skDISCONNECTED ) ) ) + //We didn't know how big it was, and were disconnected + //So that means we have it all. + { + m_wSocket.close(); + m_bIsDownloading = false; + m_bGotHeader=false; + m_sStatus = ssprintf( "Done;%dB", int(m_iDownloaded) ); + m_bFinished = true; + + if( m_iResponseCode < 200 || m_iResponseCode >= 400 ) + { + m_sStatus = ssprintf( "%ld", m_iResponseCode ) + m_sResponseName; + } + else + { + if( m_bIsPackage && m_iResponseCode < 300 ) + { + RString sZipFile = m_fOutputFile.GetRealPath(); + m_fOutputFile.Close(); + FILEMAN->FlushDirCache(); + m_iDownloaded = 0; + } + } + } +} + +bool FileDownload::ParseHTTPAddress( const RString &URL, RString &sProto, RString &sServer, int &iPort, RString &sAddress ) +{ + // [PROTO://]SERVER[:PORT][/URL] + + Regex re( + "^([A-Z]+)://" // [0]: HTTP:// + "([^/:]+)" // [1]: a.b.com + "(:([0-9]+))?" // [2], [3]: :1234 (optional, default 80) + "(/(.*))?$"); // [4], [5]: /foo.html (optional) + vector asMatches; + if( !re.Compare( URL, asMatches ) ) + return false; + ASSERT( asMatches.size() == 6 ); + + sProto = asMatches[0]; + sServer = asMatches[1]; + if( asMatches[3] != "" ) + { + iPort = atoi(asMatches[3]); + if( iPort == 0 ) + return false; + } + else + iPort = 80; + + sAddress = asMatches[5]; + + return true; +} + +bool FileDownload::IsFinished() +{ + return m_bFinished; +} + +#endif +/* + * (c) 2004 Charles Lohr, Chris Danford + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/src/FileDownload.h b/src/FileDownload.h new file mode 100644 index 0000000000..ad026309cc --- /dev/null +++ b/src/FileDownload.h @@ -0,0 +1,88 @@ +/* FileDownload - Downloads a file from the Internet. */ +#ifndef ScreenPackages_H +#define ScreenPackages_H + +#if !defined(WITHOUT_NETWORKING) + +#include "EzSockets.h" +#include "RageFile.h" + +class FileDownload +{ +public: + FileDownload(); + ~FileDownload(); + + void StartDownload( const RString &sURL, const RString &sDestFile ); + void CancelDownload( ); + RString Update(float fDeltaTime); + bool IsFinished(); +private: + int m_iPackagesPos; + int m_iLinksPos; + + int m_iDLorLST; + + // HTTP portion + void HTTPUpdate( ); + + // True if proper string, false if improper + bool ParseHTTPAddress( const RString & URL, RString & Proto, RString & Server, int & Port, RString & Addy ); + + void UpdateProgress(); + + bool m_bIsDownloading; + float m_fLastUpdate; + long m_bytesLastUpdate; + + RString m_sStatus; + + EzSockets m_wSocket; + + bool m_bGotHeader; + + RageFile m_fOutputFile; + RString m_sEndName; + bool m_bIsPackage; + + RString m_sBaseAddress; + // HTTP Header information response + long m_iTotalBytes; + long m_iDownloaded; + + long m_iResponseCode; + RString m_sResponseName; + + // Raw HTTP Buffer + RString m_sBUFFER; + + bool m_bFinished; +}; + +#endif + +#endif +/* + * (c) 2004 Charles Lohr, Chris Danford + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */