1 // ***************************************************************************
2 // BamFtp_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 FTP server
8 // ***************************************************************************
10 #include "api/BamAux.h"
11 #include "api/internal/io/BamFtp_p.h"
12 #include "api/internal/io/TcpSocket_p.h"
13 using namespace BamTools;
14 using namespace BamTools::Internal;
16 #include <iostream> // debug
31 static const uint16_t FTP_PORT = 21;
32 static const string FTP_PREFIX = "ftp://";
33 static const size_t FTP_PREFIX_LENGTH = 6;
34 static const string FTP_NEWLINE = "\r\n";
35 static const string DEFAULT_USER = "anonymous";
36 static const string DEFAULT_PASS = "anonymous@";
37 static const string ABOR_CMD = "ABOR";
38 static const string USER_CMD = "USER";
39 static const string PASS_CMD = "PASS";
40 static const string PASV_CMD = "PASV";
41 static const string REIN_CMD = "REIN";
42 static const string REST_CMD = "REST";
43 static const string RETR_CMD = "RETR";
44 static const string TYPE_CMD = "TYPE";
45 static const char COLON_CHAR = ':';
46 static const char COMMA_CHAR = ',';
47 static const char DOT_CHAR = '.';
48 static const char MINUS_CHAR = '-';
49 static const char SLASH_CHAR = '/';
50 static const char SPACE_CHAR = ' ';
51 static const char LEFT_PAREN_CHAR = '(';
52 static const char RIGHT_PAREN_CHAR = ')';
59 vector<string> split(const string& source, const char delim) {
61 stringstream ss(source);
63 vector<string> fields;
65 while ( getline(ss, field, delim) )
66 fields.push_back(field);
71 bool startsWith(const string& source, const string& pattern) {
72 return ( source.find(pattern) == 0 );
76 string toLower(const string& s) {
78 const size_t sSize = s.size();
80 for ( size_t i = 0; i < sSize; ++i )
81 out[i] = tolower(s[i]);
85 } // namespace Internal
86 } // namespace BamTools
88 // -----------------------
89 // BamFtp implementation
90 // -----------------------
92 BamFtp::BamFtp(const string& url)
94 , m_commandSocket(new TcpSocket)
95 , m_dataSocket(new TcpSocket)
98 , m_username(DEFAULT_USER)
99 , m_password(DEFAULT_PASS)
100 , m_isUrlParsed(false)
106 BamFtp::~BamFtp(void) {
108 // close connection & clean up
110 if ( m_commandSocket )
111 delete m_commandSocket;
116 void BamFtp::Close(void) {
119 m_commandSocket->DisconnectFromHost();
120 m_dataSocket->DisconnectFromHost();
122 // reset state - necessary??
123 m_isUrlParsed = false;
125 m_username = DEFAULT_USER;
126 m_password = DEFAULT_PASS;
127 m_dataHostname.clear();
131 bool BamFtp::ConnectCommandSocket(void) {
133 BT_ASSERT_X(m_commandSocket, "null command socket?");
135 // connect to FTP server
136 if ( !m_commandSocket->ConnectToHost(m_hostname, m_port, m_mode) ) {
137 SetErrorString("BamFtp::ConnectCommandSocket", "could not connect to host");
141 // receive initial reply from host
142 if ( !ReceiveReply() ) {
148 string userCommand = USER_CMD + SPACE_CHAR + m_username + FTP_NEWLINE;
149 if ( !SendCommand(userCommand, true) ) {
155 string passwordCommand = PASS_CMD + SPACE_CHAR + m_password + FTP_NEWLINE;
156 if ( !SendCommand(passwordCommand, true) ) {
162 string typeCommand = TYPE_CMD + SPACE_CHAR + "I" + FTP_NEWLINE;
163 if ( !SendCommand(typeCommand, true) ) {
172 bool BamFtp::ConnectDataSocket(void) {
174 // failure if can't connect to command socket first
175 if ( !m_commandSocket->IsConnected() ) {
176 if ( !ConnectCommandSocket() )
180 // make sure we're starting with a fresh data channel
181 if ( m_dataSocket->IsConnected() )
182 m_dataSocket->DisconnectFromHost();
184 // send passive connection command
185 const string passiveCommand = PASV_CMD + FTP_NEWLINE;
186 if ( !SendCommand(passiveCommand, true) ) {
187 // TODO: set error string
191 // retrieve passive connection port
192 if ( !ParsePassiveResponse() ) {
193 // TODO: set error string
197 // set up restart command (tell server where to start fetching bytes from)
198 if ( m_filePosition >= 0 ) {
200 stringstream fpStream("");
201 fpStream << m_filePosition;
202 string restartCommand = REST_CMD + SPACE_CHAR + fpStream.str() + FTP_NEWLINE;
203 if ( !SendCommand(restartCommand, true) ) {
204 // TODO: set error string
209 // main file retrieval request
210 string retrieveCommand = RETR_CMD + SPACE_CHAR + m_filename + FTP_NEWLINE;
211 if ( !SendCommand(retrieveCommand, false) ) {
212 // TODO: set error string
216 // make data channel connection
217 if ( !m_dataSocket->ConnectToHost(m_dataHostname, m_dataPort) ) {
218 // TODO: set error string
222 // fetch intial reply from server
223 if ( !ReceiveReply() ) {
224 // TODO: set error string
225 m_dataSocket->DisconnectFromHost();
229 // make sure we have reply code 150 (all good)
230 if ( !startsWith(m_response, "150") ) {
231 // TODO: set error string
232 m_dataSocket->DisconnectFromHost();
240 bool BamFtp::IsOpen(void) const {
241 return IBamIODevice::IsOpen() && m_isUrlParsed;
244 bool BamFtp::IsRandomAccess(void) const {
248 bool BamFtp::Open(const IBamIODevice::OpenMode mode) {
250 // BamFtp only supports read-only access
251 if ( mode != IBamIODevice::ReadOnly ) {
252 SetErrorString("BamFtp::Open", "writing on this device is not supported");
256 // initialize basic valid state
260 // attempt connection to command & data sockets
261 return ( ConnectCommandSocket() && ConnectDataSocket() );
264 bool BamFtp::ParsePassiveResponse(void) {
267 if ( m_response.empty() )
271 const size_t leftParenFound = m_response.find(LEFT_PAREN_CHAR);
272 const size_t rightParenFound = m_response.find(RIGHT_PAREN_CHAR);
273 if ( leftParenFound == string::npos || rightParenFound == string::npos )
276 // grab everything between ( should be "h1,h2,h3,h4,p1,p2" )
277 string::const_iterator responseBegin = m_response.begin();
278 const string hostAndPort(responseBegin+leftParenFound+1, responseBegin+rightParenFound);
280 // parse into string fields
281 vector<string> fields = split(hostAndPort, COMMA_CHAR);
282 if ( fields.size() != 6 )
285 // fetch passive connection IP
286 m_dataHostname = fields[0] + DOT_CHAR +
287 fields[1] + DOT_CHAR +
288 fields[2] + DOT_CHAR +
291 // fetch passive connection port
292 const uint8_t portUpper = static_cast<uint8_t>(atoi(fields[4].c_str()));
293 const uint8_t portLower = static_cast<uint8_t>(atoi(fields[5].c_str()));
294 m_dataPort = ( portUpper<<8 ) + portLower;
300 void BamFtp::ParseUrl(const string& url) {
302 // clear flag to start
303 m_isUrlParsed = false;
305 // make sure url starts with "ftp://", case-insensitive
308 const size_t prefixFound = tempUrl.find(FTP_PREFIX);
309 if ( prefixFound == string::npos )
312 // find end of host name portion (first '/' hit after the prefix)
313 const size_t firstSlashFound = tempUrl.find(SLASH_CHAR, FTP_PREFIX_LENGTH);
314 if ( firstSlashFound == string::npos ) {
315 ; // no slash found... no filename given along with host?
319 string hostname = tempUrl.substr(FTP_PREFIX_LENGTH, (firstSlashFound - FTP_PREFIX_LENGTH));
320 m_hostname = hostname;
323 // store remainder of URL as filename (must be non-empty)
324 string filename = tempUrl.substr(firstSlashFound);
325 if ( filename.empty() )
327 m_filename = filename;
329 // set parsed OK flag
330 m_isUrlParsed = true;
333 int64_t BamFtp::Read(char* data, const unsigned int numBytes) {
335 // if BamHttp not in a valid state
339 // read until hit desired @numBytes
340 int64_t bytesReadSoFar = 0;
341 while ( bytesReadSoFar < numBytes ) {
343 // calculate number of bytes we're going to try to read this iteration
344 const size_t remainingBytes = ( numBytes - bytesReadSoFar );
346 // if either disconnected somehow, or (more likely) we have seeked since last read
347 if ( !m_dataSocket->IsConnected() ) {
348 if ( !ConnectDataSocket() ) {
349 // TODO: set error string
354 // read bytes from data socket
355 const int64_t socketBytesRead = ReadDataSocket(data+bytesReadSoFar, remainingBytes);
356 if ( socketBytesRead < 0 )
358 bytesReadSoFar += socketBytesRead;
359 m_filePosition += socketBytesRead;
362 // return actual number bytes successfully read
363 return bytesReadSoFar;
366 int64_t BamFtp::ReadCommandSocket(char* data, const unsigned int maxNumBytes) {
368 // try to read 'remainingBytes' from socket
369 const int64_t numBytesRead = m_commandSocket->Read(data, maxNumBytes);
370 if ( numBytesRead < 0 )
375 int64_t BamFtp::ReadDataSocket(char* data, const unsigned int maxNumBytes) {
377 // try to read 'remainingBytes' from socket
378 const int64_t numBytesRead = m_dataSocket->Read(data, maxNumBytes);
379 if ( numBytesRead < 0 )
384 bool BamFtp::ReceiveReply(void) {
386 // failure if not connected
387 if ( !m_commandSocket->IsConnected() ) {
388 SetErrorString("BamFtp::ReceiveReply()", "command socket not connected");
394 // read header data (& discard for now)
395 bool headerEnd = false;
396 while ( !headerEnd ) {
398 const string headerLine = m_commandSocket->ReadLine();
399 m_response += headerLine;
401 // if line is of form 'xyz ', quit reading lines
402 if ( (headerLine.length() >= 4 ) &&
403 isdigit(headerLine[0]) &&
404 isdigit(headerLine[1]) &&
405 isdigit(headerLine[2]) &&
406 ( headerLine[3] != MINUS_CHAR )
413 // return success, depending on response
414 if ( m_response.empty() ) {
415 SetErrorString("BamFtp::ReceiveReply", "error reading server reply");
421 bool BamFtp::Seek(const int64_t& position) {
423 // if FTP device not in a valid state
425 // TODO: set error string
429 // ----------------------
430 // UGLY !! but works??
431 // ----------------------
432 // disconnect from server
433 m_dataSocket->DisconnectFromHost();
434 m_commandSocket->DisconnectFromHost();
436 // update file position & return success
437 m_filePosition = position;
441 bool BamFtp::SendCommand(const string& command, bool waitForReply) {
443 // failure if not connected
444 if ( !m_commandSocket->IsConnected() ) {
445 SetErrorString("BamFtp::SendCommand", "command socket not connected");
449 // write command to 'command socket'
450 if ( WriteCommandSocket(command.c_str(), command.length()) == -1 ) {
451 SetErrorString("BamFtp::SendCommand", "error writing to socket");
452 // get actual error from command socket??
456 // if we sent a command that receives a response
458 return ReceiveReply();
464 int64_t BamFtp::Tell(void) const {
465 return ( IsOpen() ? m_filePosition : -1 );
468 int64_t BamFtp::Write(const char* data, const unsigned int numBytes) {
471 BT_ASSERT_X(false, "BamFtp::Write : write-mode not supported on this device");
472 SetErrorString("BamFtp::Write", "write-mode not supported on this device");
476 int64_t BamFtp::WriteCommandSocket(const char* data, const unsigned int numBytes) {
477 if ( !m_commandSocket->IsConnected() )
479 m_commandSocket->ClearBuffer();
480 return m_commandSocket->Write(data, numBytes);
483 int64_t BamFtp::WriteDataSocket(const char* data, const unsigned int numBytes) {
486 BT_ASSERT_X(false, "BamFtp::WriteDataSocket: write-mode not supported on this device");
487 SetErrorString("BamFtp::Write", "write-mode not supported on this device");