1 // ***************************************************************************
2 // BamFtp_p.cpp (c) 2011 Derek Barnett
3 // Marth Lab, Department of Biology, Boston College
4 // ---------------------------------------------------------------------------
5 // Last modified: 10 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;
29 static const uint16_t FTP_PORT = 21;
30 static const string FTP_PREFIX = "ftp://";
31 static const size_t FTP_PREFIX_LENGTH = 6;
32 static const string FTP_NEWLINE = "\r\n";
34 static const string DEFAULT_USER = "anonymous";
35 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";
46 static const char CMD_SEPARATOR = ' ';
47 static const char HOST_SEPARATOR = '/';
48 static const char IP_SEPARATOR = '.';
50 static const char MULTILINE_CONTINUE = '-';
52 static const char PASV_REPLY_PREFIX = '(';
53 static const char PASV_REPLY_SEPARATOR = ',';
54 static const char PASV_REPLY_SUFFIX = ')';
61 vector<string> split(const string& source, const char delim) {
63 stringstream ss(source);
65 vector<string> fields;
67 while ( getline(ss, field, delim) )
68 fields.push_back(field);
73 bool startsWith(const string& source, const string& pattern) {
74 return ( source.find(pattern) == 0 );
78 string toLower(const string& s) {
80 const size_t sSize = s.size();
82 for ( size_t i = 0; i < sSize; ++i )
83 out[i] = tolower(s[i]);
87 } // namespace Internal
88 } // namespace BamTools
90 // -----------------------
91 // BamFtp implementation
92 // -----------------------
94 BamFtp::BamFtp(const string& url)
96 , m_commandSocket(new TcpSocket)
97 , m_dataSocket(new TcpSocket)
100 , m_username(DEFAULT_USER)
101 , m_password(DEFAULT_PASS)
102 , m_isUrlParsed(false)
108 BamFtp::~BamFtp(void) {
110 // close connection & clean up
112 if ( m_commandSocket )
113 delete m_commandSocket;
118 void BamFtp::Close(void) {
121 m_commandSocket->DisconnectFromHost();
122 m_dataSocket->DisconnectFromHost();
124 // reset state - necessary??
125 m_isUrlParsed = false;
127 m_username = DEFAULT_USER;
128 m_password = DEFAULT_PASS;
129 m_dataHostname.clear();
133 bool BamFtp::ConnectCommandSocket(void) {
135 BT_ASSERT_X(m_commandSocket, "null command socket?");
137 // connect to FTP server
138 if ( !m_commandSocket->ConnectToHost(m_hostname, m_port, m_mode) ) {
139 SetErrorString("BamFtp::ConnectCommandSocket", "could not connect to host");
143 // receive initial reply from host
144 if ( !ReceiveReply() ) {
150 string userCommand = USER_CMD + CMD_SEPARATOR + m_username + FTP_NEWLINE;
151 if ( !SendCommand(userCommand, true) ) {
157 string passwordCommand = PASS_CMD + CMD_SEPARATOR + m_password + FTP_NEWLINE;
158 if ( !SendCommand(passwordCommand, true) ) {
164 string typeCommand = TYPE_CMD + CMD_SEPARATOR + 'I' + FTP_NEWLINE;
165 if ( !SendCommand(typeCommand, true) ) {
174 bool BamFtp::ConnectDataSocket(void) {
176 // failure if can't connect to command socket first
177 if ( !m_commandSocket->IsConnected() ) {
178 if ( !ConnectCommandSocket() )
182 // make sure we're starting with a fresh data channel
183 if ( m_dataSocket->IsConnected() )
184 m_dataSocket->DisconnectFromHost();
186 // send passive connection command
187 const string passiveCommand = PASV_CMD + FTP_NEWLINE;
188 if ( !SendCommand(passiveCommand, true) ) {
189 // TODO: set error string
193 // retrieve passive connection port
194 if ( !ParsePassiveResponse() ) {
195 // TODO: set error string
199 // set up restart command (tell server where to start fetching bytes from)
200 if ( m_filePosition >= 0 ) {
202 stringstream fpStream("");
203 fpStream << m_filePosition;
204 string restartCommand = REST_CMD + CMD_SEPARATOR + fpStream.str() + FTP_NEWLINE;
205 if ( !SendCommand(restartCommand, true) ) {
206 // TODO: set error string
211 // main file retrieval request
212 string retrieveCommand = RETR_CMD + CMD_SEPARATOR + m_filename + FTP_NEWLINE;
213 if ( !SendCommand(retrieveCommand, false) ) {
214 // TODO: set error string
218 // make data channel connection
219 if ( !m_dataSocket->ConnectToHost(m_dataHostname, m_dataPort) ) {
220 // TODO: set error string
224 // fetch intial reply from server
225 if ( !ReceiveReply() ) {
226 // TODO: set error string
227 m_dataSocket->DisconnectFromHost();
231 // make sure we have reply code 150 (all good)
232 if ( !startsWith(m_response, "150") ) {
233 // TODO: set error string
234 m_dataSocket->DisconnectFromHost();
242 bool BamFtp::IsOpen(void) const {
243 return IBamIODevice::IsOpen() && m_isUrlParsed;
246 bool BamFtp::IsRandomAccess(void) const {
250 bool BamFtp::Open(const IBamIODevice::OpenMode mode) {
252 // BamFtp only supports read-only access
253 if ( mode != IBamIODevice::ReadOnly ) {
254 SetErrorString("BamFtp::Open", "writing on this device is not supported");
258 // initialize basic valid state
262 // attempt connection to command & data sockets
263 return ( ConnectCommandSocket() && ConnectDataSocket() );
266 bool BamFtp::ParsePassiveResponse(void) {
269 if ( m_response.empty() )
273 const size_t leftParenFound = m_response.find(PASV_REPLY_PREFIX);
274 const size_t rightParenFound = m_response.find(PASV_REPLY_SUFFIX);
275 if ( leftParenFound == string::npos || rightParenFound == string::npos )
278 // grab everything between ( should be "h1,h2,h3,h4,p1,p2" )
279 string::const_iterator responseBegin = m_response.begin();
280 const string hostAndPort(responseBegin+leftParenFound+1, responseBegin+rightParenFound);
282 // parse into string fields
283 vector<string> fields = split(hostAndPort, PASV_REPLY_SEPARATOR);
284 if ( fields.size() != 6 )
287 // fetch passive connection IP
288 m_dataHostname = fields[0] + IP_SEPARATOR +
289 fields[1] + IP_SEPARATOR +
290 fields[2] + IP_SEPARATOR +
293 // fetch passive connection port
294 const uint8_t portUpper = static_cast<uint8_t>(atoi(fields[4].c_str()));
295 const uint8_t portLower = static_cast<uint8_t>(atoi(fields[5].c_str()));
296 m_dataPort = ( portUpper<<8 ) + portLower;
302 void BamFtp::ParseUrl(const string& url) {
304 // clear flag to start
305 m_isUrlParsed = false;
307 // make sure url starts with "ftp://", case-insensitive
310 const size_t prefixFound = tempUrl.find(FTP_PREFIX);
311 if ( prefixFound == string::npos )
314 // find end of host name portion (first '/' hit after the prefix)
315 const size_t firstSlashFound = tempUrl.find(HOST_SEPARATOR, FTP_PREFIX_LENGTH);
316 if ( firstSlashFound == string::npos ) {
317 ; // no slash found... no filename given along with host?
321 string hostname = tempUrl.substr(FTP_PREFIX_LENGTH, (firstSlashFound - FTP_PREFIX_LENGTH));
322 m_hostname = hostname;
325 // store remainder of URL as filename (must be non-empty)
326 string filename = tempUrl.substr(firstSlashFound);
327 if ( filename.empty() )
329 m_filename = filename;
331 // set parsed OK flag
332 m_isUrlParsed = true;
335 int64_t BamFtp::Read(char* data, const unsigned int numBytes) {
337 // if BamHttp not in a valid state
341 // read until hit desired @numBytes
342 int64_t bytesReadSoFar = 0;
343 while ( bytesReadSoFar < numBytes ) {
345 // calculate number of bytes we're going to try to read this iteration
346 const size_t remainingBytes = ( numBytes - bytesReadSoFar );
348 // if either disconnected somehow, or (more likely) we have seeked since last read
349 if ( !m_dataSocket->IsConnected() ) {
350 if ( !ConnectDataSocket() ) {
351 // TODO: set error string
356 // read bytes from data socket
357 const int64_t socketBytesRead = ReadDataSocket(data+bytesReadSoFar, remainingBytes);
358 if ( socketBytesRead < 0 )
360 bytesReadSoFar += socketBytesRead;
361 m_filePosition += socketBytesRead;
364 // return actual number bytes successfully read
365 return bytesReadSoFar;
368 int64_t BamFtp::ReadCommandSocket(char* data, const unsigned int maxNumBytes) {
370 // try to read 'remainingBytes' from socket
371 const int64_t numBytesRead = m_commandSocket->Read(data, maxNumBytes);
372 if ( numBytesRead < 0 )
377 int64_t BamFtp::ReadDataSocket(char* data, const unsigned int maxNumBytes) {
379 // try to read 'remainingBytes' from socket
380 const int64_t numBytesRead = m_dataSocket->Read(data, maxNumBytes);
381 if ( numBytesRead < 0 )
386 bool BamFtp::ReceiveReply(void) {
388 // failure if not connected
389 if ( !m_commandSocket->IsConnected() ) {
390 SetErrorString("BamFtp::ReceiveReply()", "command socket not connected");
396 // read header data (& discard for now)
397 bool headerEnd = false;
398 while ( !headerEnd ) {
400 const string headerLine = m_commandSocket->ReadLine();
401 m_response += headerLine;
403 // if line is of form 'xyz ', quit reading lines
404 if ( (headerLine.length() >= 4 ) &&
405 isdigit(headerLine[0]) &&
406 isdigit(headerLine[1]) &&
407 isdigit(headerLine[2]) &&
408 ( headerLine[3] != MULTILINE_CONTINUE )
415 // return success, depending on response
416 if ( m_response.empty() ) {
417 SetErrorString("BamFtp::ReceiveReply", "error reading server reply");
423 bool BamFtp::Seek(const int64_t& position, const int origin) {
425 // if FTP device not in a valid state
427 // TODO: set error string
431 // ----------------------
432 // UGLY !! but works??
433 // ----------------------
434 // disconnect from server
435 m_dataSocket->DisconnectFromHost();
436 m_commandSocket->DisconnectFromHost();
438 // update file position & return success
439 if ( origin == SEEK_CUR )
440 m_filePosition += position;
441 else if ( origin == SEEK_SET)
442 m_filePosition = position;
444 // TODO: set error string
450 bool BamFtp::SendCommand(const string& command, bool waitForReply) {
452 // failure if not connected
453 if ( !m_commandSocket->IsConnected() ) {
454 SetErrorString("BamFtp::SendCommand", "command socket not connected");
458 // write command to 'command socket'
459 if ( WriteCommandSocket(command.c_str(), command.length()) == -1 ) {
460 SetErrorString("BamFtp::SendCommand", "error writing to socket");
461 // get actual error from command socket??
465 // if we sent a command that receives a response
467 return ReceiveReply();
473 int64_t BamFtp::Tell(void) const {
474 return ( IsOpen() ? m_filePosition : -1 );
477 int64_t BamFtp::Write(const char* data, const unsigned int numBytes) {
480 BT_ASSERT_X(false, "BamFtp::Write : write-mode not supported on this device");
481 SetErrorString("BamFtp::Write", "write-mode not supported on this device");
485 int64_t BamFtp::WriteCommandSocket(const char* data, const unsigned int numBytes) {
486 if ( !m_commandSocket->IsConnected() )
488 m_commandSocket->ClearBuffer();
489 return m_commandSocket->Write(data, numBytes);
492 int64_t BamFtp::WriteDataSocket(const char* data, const unsigned int numBytes) {
495 BT_ASSERT_X(false, "BamFtp::WriteDataSocket: write-mode not supported on this device");
496 SetErrorString("BamFtp::Write", "write-mode not supported on this device");