1 // ***************************************************************************
2 // BamHttp_p.cpp (c) 2011 Derek Barnett
3 // Marth Lab, Department of Biology, Boston College
4 // ---------------------------------------------------------------------------
5 // Last modified: 8 November 2011 (DB)
6 // ---------------------------------------------------------------------------
7 // Provides reading/writing of BAM files on HTTP server
8 // ***************************************************************************
10 #include "api/BamAux.h"
11 #include "api/internal/io/BamHttp_p.h"
12 #include "api/internal/io/HttpHeader_p.h"
13 #include "api/internal/io/TcpSocket_p.h"
14 using namespace BamTools;
15 using namespace BamTools::Internal;
30 static const string HTTP_PORT = "80";
31 static const string HTTP_PREFIX = "http://";
32 static const size_t HTTP_PREFIX_LENGTH = 7;
33 static const char COLON_CHAR = ':';
34 static const char SLASH_CHAR = '/';
41 bool endsWith(const string& source, const string& pattern) {
42 return ( source.find(pattern) == (source.length() - pattern.length()) );
46 string toLower(const string& s) {
48 const size_t sSize = s.size();
50 for ( size_t i = 0; i < sSize; ++i )
51 out[i] = tolower(s[i]);
55 } // namespace Internal
56 } // namespace BamTools
58 // ------------------------
59 // BamHttp implementation
60 // ------------------------
62 BamHttp::BamHttp(const string& url)
64 , m_socket(new TcpSocket)
68 , m_isUrlParsed(false)
70 , m_endRangeFilePosition(-1)
75 BamHttp::~BamHttp(void) {
77 // close connection & clean up
83 void BamHttp::Close(void) {
86 m_socket->DisconnectFromHost();
88 // clean up request & response
98 // reset state - necessary??
99 m_isUrlParsed = false;
101 m_endRangeFilePosition = -1;
104 bool BamHttp::ConnectSocket(void) {
106 BT_ASSERT_X(m_socket, "null socket?");
108 // any state checks, etc?
109 if ( !m_socket->ConnectToHost(m_hostname, m_port, m_mode) ) {
110 // TODO: set error string
114 // attempt initial request
116 m_endRangeFilePosition = -1;
117 if ( !SendRequest() ) {
118 // TODO: set error string
123 // wait for response from server
124 if ( !ReceiveResponse() ) {
125 // TODO: set error string
134 bool BamHttp::EnsureSocketConnection(void) {
135 if ( m_socket->IsConnected() )
137 else return ConnectSocket();
140 bool BamHttp::IsOpen(void) const {
141 return IBamIODevice::IsOpen() && m_isUrlParsed;
144 bool BamHttp::IsRandomAccess(void) const {
148 bool BamHttp::Open(const IBamIODevice::OpenMode mode) {
150 // BamHttp only supports read-only access
151 if ( mode != IBamIODevice::ReadOnly ) {
152 SetErrorString("BamHttp::Open", "writing on this device is not supported");
157 // attempt connection to socket
158 if ( !ConnectSocket() ) {
159 SetErrorString("BamHttp::Open", m_socket->GetErrorString());
167 void BamHttp::ParseUrl(const string& url) {
169 // clear flag to start
170 m_isUrlParsed = false;
172 // make sure url starts with "http://", case-insensitive
175 const size_t prefixFound = tempUrl.find(HTTP_PREFIX);
176 if ( prefixFound == string::npos )
179 // find end of host name portion (first '/' hit after the prefix)
180 const size_t firstSlashFound = tempUrl.find(SLASH_CHAR, HTTP_PREFIX_LENGTH);
181 if ( firstSlashFound == string::npos ) {
182 ; // no slash found... no filename given along with host?
185 // fetch hostname (check for proxy port)
186 string hostname = tempUrl.substr(HTTP_PREFIX_LENGTH, (firstSlashFound - HTTP_PREFIX_LENGTH));
187 const size_t colonFound = hostname.find(COLON_CHAR);
188 if ( colonFound != string::npos ) {
189 ; // TODO: handle proxy port (later, just skip for now)
191 m_hostname = hostname;
195 // store remainder of URL as filename (must be non-empty)
196 string filename = tempUrl.substr(firstSlashFound);
197 if ( filename.empty() )
199 m_filename = filename;
201 // set parsed OK flag
202 m_isUrlParsed = true;
205 int64_t BamHttp::Read(char* data, const unsigned int numBytes) {
207 // if BamHttp not in a valid state
211 // read until hit desired @numBytes
212 int64_t bytesReadSoFar = 0;
213 while ( bytesReadSoFar < numBytes ) {
215 // calculate number of bytes we're going to try to read this iteration
216 const size_t remainingBytes = ( numBytes - bytesReadSoFar );
218 // if socket has access to entire file contents
219 // i.e. we received response with full data (status code == 200)
220 if ( m_endRangeFilePosition < 0 ) {
222 // try to read 'remainingBytes' from socket
223 const int64_t socketBytesRead = ReadFromSocket(data+bytesReadSoFar, remainingBytes);
224 if ( socketBytesRead < 0 )
226 bytesReadSoFar += socketBytesRead;
227 m_filePosition += socketBytesRead;
230 // socket has access to a range of data (might already be in buffer)
231 // i.e. we received response with partial data (status code == 206)
234 // there is data left from last request
235 if ( m_endRangeFilePosition > m_filePosition ) {
237 // try to read either the total 'remainingBytes' or whatever we have remaining from last request range
238 const size_t rangeRemainingBytes = m_endRangeFilePosition - m_filePosition;
239 const size_t bytesToRead = std::min(remainingBytes, rangeRemainingBytes);
240 const int64_t socketBytesRead = ReadFromSocket(data+bytesReadSoFar, bytesToRead);
241 if ( socketBytesRead < 0 )
243 bytesReadSoFar += socketBytesRead;
244 m_filePosition += socketBytesRead;
247 // otherwise, this is a 1st-time read OR we already read everything from the last GET request
250 // request for next range
251 if ( !SendRequest(remainingBytes) || !ReceiveResponse() ) {
259 // return actual number bytes successfully read
260 return bytesReadSoFar;
263 int64_t BamHttp::ReadFromSocket(char* data, const unsigned int maxNumBytes) {
265 // try to read 'remainingBytes' from socket
266 const int64_t numBytesRead = m_socket->Read(data, maxNumBytes);
267 if ( numBytesRead < 0 )
272 bool BamHttp::ReceiveResponse(void) {
274 // clear any prior response
278 // make sure we're connected
279 if ( !EnsureSocketConnection() )
282 // fetch header, up until double new line
283 string responseHeader;
284 static const string doubleNewLine = "\n\n";
286 // read line & append to full header
287 const string headerLine = m_socket->ReadLine();
288 responseHeader += headerLine;
290 } while ( !endsWith(responseHeader, doubleNewLine) );
293 if ( responseHeader.empty() ) {
294 // TODO: set error string
299 // create response from header text
300 m_response = new HttpResponseHeader(responseHeader);
301 if ( !m_response->IsValid() ) {
302 // TODO: set error string
307 // if we got range response as requested
308 if ( m_response->GetStatusCode() == 206 )
311 // if we got the full file contents instead of range
312 else if ( m_response->GetStatusCode() == 200 ) {
314 // skip up to current file position
315 RaiiBuffer tmp(0x8000);
316 int64_t numBytesRead = 0;
317 while ( numBytesRead < m_filePosition ) {
318 int64_t result = ReadFromSocket(tmp.Buffer, 0x8000);
323 numBytesRead += result;
330 // on any other reponse status
331 // TODO: set error string
336 bool BamHttp::Seek(const int64_t& position) {
338 // if HTTP device not in a valid state
340 // TODO: set error string
344 // discard socket's buffer contents, update positions, & return success
345 m_socket->ClearBuffer();
346 m_filePosition = position;
347 m_endRangeFilePosition = position;
351 bool BamHttp::SendRequest(const size_t numBytes) {
353 // remove any currently active request
357 // create range string
358 m_endRangeFilePosition = m_filePosition + numBytes;
359 stringstream range("");
360 range << "bytes=" << m_filePosition << "-" << m_endRangeFilePosition;
362 // make sure we're connected
363 if ( !EnsureSocketConnection() )
367 m_request = new HttpRequestHeader("GET", m_filename);
368 m_request->SetField("Host", m_hostname);
369 m_request->SetField("Range", range.str());
371 // write request to socket
372 const string requestHeader = m_request->ToString();
373 const size_t headerSize = requestHeader.size();
374 return ( WriteToSocket(requestHeader.c_str(), headerSize) == headerSize );
377 int64_t BamHttp::Tell(void) const {
378 return ( IsOpen() ? m_filePosition : -1 );
381 int64_t BamHttp::Write(const char* data, const unsigned int numBytes) {
384 BT_ASSERT_X(false, "BamHttp::Write : write-mode not supported on this device");
385 SetErrorString("BamHttp::Write", "write-mode not supported on this device");
389 int64_t BamHttp::WriteToSocket(const char* data, const unsigned int numBytes) {
390 if ( !m_socket->IsConnected() )
392 m_socket->ClearBuffer();
393 return m_socket->Write(data, numBytes);