001package org.apache.maven.wagon.providers.ftp; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import org.apache.commons.io.IOUtils; 023import org.apache.commons.net.ProtocolCommandEvent; 024import org.apache.commons.net.ProtocolCommandListener; 025import org.apache.commons.net.ftp.FTP; 026import org.apache.commons.net.ftp.FTPClient; 027import org.apache.commons.net.ftp.FTPFile; 028import org.apache.commons.net.ftp.FTPReply; 029import org.apache.maven.wagon.ConnectionException; 030import org.apache.maven.wagon.InputData; 031import org.apache.maven.wagon.OutputData; 032import org.apache.maven.wagon.PathUtils; 033import org.apache.maven.wagon.ResourceDoesNotExistException; 034import org.apache.maven.wagon.StreamWagon; 035import org.apache.maven.wagon.TransferFailedException; 036import org.apache.maven.wagon.WagonConstants; 037import org.apache.maven.wagon.authentication.AuthenticationException; 038import org.apache.maven.wagon.authentication.AuthenticationInfo; 039import org.apache.maven.wagon.authorization.AuthorizationException; 040import org.apache.maven.wagon.repository.RepositoryPermissions; 041import org.apache.maven.wagon.resource.Resource; 042 043import java.io.File; 044import java.io.FileInputStream; 045import java.io.IOException; 046import java.io.InputStream; 047import java.io.OutputStream; 048import java.util.ArrayList; 049import java.util.Calendar; 050import java.util.List; 051 052/** 053 * FtpWagon 054 * 055 * 056 * @plexus.component role="org.apache.maven.wagon.Wagon" 057 * role-hint="ftp" 058 * instantiation-strategy="per-lookup" 059 */ 060public class FtpWagon 061 extends StreamWagon 062{ 063 private FTPClient ftp; 064 065 /** 066 * @plexus.configuration default-value="true" 067 */ 068 private boolean passiveMode = true; 069 070 /** 071 * @plexus.configuration default-value="ISO-8859-1" 072 */ 073 private String controlEncoding = FTP.DEFAULT_CONTROL_ENCODING; 074 075 public boolean isPassiveMode() 076 { 077 return passiveMode; 078 } 079 080 public void setPassiveMode( boolean passiveMode ) 081 { 082 this.passiveMode = passiveMode; 083 } 084 085 protected void openConnectionInternal() 086 throws ConnectionException, AuthenticationException 087 { 088 AuthenticationInfo authInfo = getAuthenticationInfo(); 089 090 if ( authInfo == null ) 091 { 092 throw new IllegalArgumentException( "Authentication Credentials cannot be null for FTP protocol" ); 093 } 094 095 if ( authInfo.getUserName() == null ) 096 { 097 authInfo.setUserName( System.getProperty( "user.name" ) ); 098 } 099 100 String username = authInfo.getUserName(); 101 102 String password = authInfo.getPassword(); 103 104 if ( username == null ) 105 { 106 throw new AuthenticationException( "Username not specified for repository " + getRepository().getId() ); 107 } 108 if ( password == null ) 109 { 110 throw new AuthenticationException( "Password not specified for repository " + getRepository().getId() ); 111 } 112 113 String host = getRepository().getHost(); 114 115 ftp = new FTPClient(); 116 ftp.setDefaultTimeout( getTimeout() ); 117 ftp.setDataTimeout( getTimeout() ); 118 ftp.setControlEncoding( getControlEncoding() ); 119 120 ftp.addProtocolCommandListener( new PrintCommandListener( this ) ); 121 122 try 123 { 124 if ( getRepository().getPort() != WagonConstants.UNKNOWN_PORT ) 125 { 126 ftp.connect( host, getRepository().getPort() ); 127 } 128 else 129 { 130 ftp.connect( host ); 131 } 132 133 // After connection attempt, you should check the reply code to 134 // verify 135 // success. 136 int reply = ftp.getReplyCode(); 137 138 if ( !FTPReply.isPositiveCompletion( reply ) ) 139 { 140 ftp.disconnect(); 141 142 throw new AuthenticationException( "FTP server refused connection." ); 143 } 144 } 145 catch ( IOException e ) 146 { 147 if ( ftp.isConnected() ) 148 { 149 try 150 { 151 fireSessionError( e ); 152 153 ftp.disconnect(); 154 } 155 catch ( IOException f ) 156 { 157 // do nothing 158 } 159 } 160 161 throw new AuthenticationException( "Could not connect to server.", e ); 162 } 163 164 try 165 { 166 if ( !ftp.login( username, password ) ) 167 { 168 throw new AuthenticationException( "Cannot login to remote system" ); 169 } 170 171 fireSessionDebug( "Remote system is " + ftp.getSystemName() ); 172 173 // Set to binary mode. 174 ftp.setFileType( FTP.BINARY_FILE_TYPE ); 175 ftp.setListHiddenFiles( true ); 176 177 // Use passive mode as default because most of us are 178 // behind firewalls these days. 179 if ( isPassiveMode() ) 180 { 181 ftp.enterLocalPassiveMode(); 182 } 183 } 184 catch ( IOException e ) 185 { 186 throw new ConnectionException( "Cannot login to remote system", e ); 187 } 188 } 189 190 protected void firePutCompleted( Resource resource, File file ) 191 { 192 try 193 { 194 // TODO [BP]: verify the order is correct 195 ftp.completePendingCommand(); 196 197 RepositoryPermissions permissions = repository.getPermissions(); 198 199 if ( permissions != null && permissions.getGroup() != null ) 200 { 201 // ignore failures 202 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + resource.getName() ); 203 } 204 205 if ( permissions != null && permissions.getFileMode() != null ) 206 { 207 // ignore failures 208 ftp.sendSiteCommand( "CHMOD " + permissions.getFileMode() + " " + resource.getName() ); 209 } 210 } 211 catch ( IOException e ) 212 { 213 // TODO: handle 214 // michal I am not sure what error means in that context 215 // I think that we will be able to recover or simply we will fail later on 216 } 217 218 super.firePutCompleted( resource, file ); 219 } 220 221 protected void fireGetCompleted( Resource resource, File localFile ) 222 { 223 try 224 { 225 ftp.completePendingCommand(); 226 } 227 catch ( IOException e ) 228 { 229 // TODO: handle 230 // michal I am not sure what error means in that context 231 // actually I am not even sure why we have to invoke that command 232 // I think that we will be able to recover or simply we will fail later on 233 } 234 super.fireGetCompleted( resource, localFile ); 235 } 236 237 public void closeConnection() 238 throws ConnectionException 239 { 240 if ( ftp != null && ftp.isConnected() ) 241 { 242 try 243 { 244 // This is a NPE rethink shutting down the streams 245 ftp.disconnect(); 246 } 247 catch ( IOException e ) 248 { 249 throw new ConnectionException( "Failed to close connection to FTP repository", e ); 250 } 251 } 252 } 253 254 public void fillOutputData( OutputData outputData ) 255 throws TransferFailedException 256 { 257 OutputStream os; 258 259 Resource resource = outputData.getResource(); 260 261 RepositoryPermissions permissions = repository.getPermissions(); 262 263 try 264 { 265 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 266 { 267 throw new TransferFailedException( 268 "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" ); 269 } 270 271 String[] dirs = PathUtils.dirnames( resource.getName() ); 272 273 for ( String dir : dirs ) 274 { 275 boolean dirChanged = ftp.changeWorkingDirectory( dir ); 276 277 if ( !dirChanged ) 278 { 279 // first, try to create it 280 boolean success = ftp.makeDirectory( dir ); 281 282 if ( success ) 283 { 284 if ( permissions != null && permissions.getGroup() != null ) 285 { 286 // ignore failures 287 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + dir ); 288 } 289 290 if ( permissions != null && permissions.getDirectoryMode() != null ) 291 { 292 // ignore failures 293 ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() + " " + dir ); 294 } 295 296 dirChanged = ftp.changeWorkingDirectory( dir ); 297 } 298 } 299 300 if ( !dirChanged ) 301 { 302 throw new TransferFailedException( "Unable to create directory " + dir ); 303 } 304 } 305 306 // we come back to original basedir so 307 // FTP wagon is ready for next requests 308 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 309 { 310 throw new TransferFailedException( "Unable to return to the base directory" ); 311 } 312 313 os = ftp.storeFileStream( resource.getName() ); 314 315 if ( os == null ) 316 { 317 String msg = 318 "Cannot transfer resource: '" + resource + "'. Output stream is null. FTP Server response: " 319 + ftp.getReplyString(); 320 321 throw new TransferFailedException( msg ); 322 323 } 324 325 fireTransferDebug( "resource = " + resource ); 326 327 } 328 catch ( IOException e ) 329 { 330 throw new TransferFailedException( "Error transferring over FTP", e ); 331 } 332 333 outputData.setOutputStream( os ); 334 335 } 336 337 // ---------------------------------------------------------------------- 338 // 339 // ---------------------------------------------------------------------- 340 341 public void fillInputData( InputData inputData ) 342 throws TransferFailedException, ResourceDoesNotExistException 343 { 344 InputStream is; 345 346 Resource resource = inputData.getResource(); 347 348 try 349 { 350 ftpChangeDirectory( resource ); 351 352 String filename = PathUtils.filename( resource.getName() ); 353 FTPFile[] ftpFiles = ftp.listFiles( filename ); 354 355 if ( ftpFiles == null || ftpFiles.length <= 0 ) 356 { 357 throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" ); 358 } 359 360 long contentLength = ftpFiles[0].getSize(); 361 362 //@todo check how it works! javadoc of common login says: 363 // Returns the file timestamp. This usually the last modification time. 364 // 365 Calendar timestamp = ftpFiles[0].getTimestamp(); 366 long lastModified = timestamp != null ? timestamp.getTimeInMillis() : 0; 367 368 resource.setContentLength( contentLength ); 369 370 resource.setLastModified( lastModified ); 371 372 is = ftp.retrieveFileStream( filename ); 373 } 374 catch ( IOException e ) 375 { 376 throw new TransferFailedException( "Error transferring file via FTP", e ); 377 } 378 379 inputData.setInputStream( is ); 380 } 381 382 private void ftpChangeDirectory( Resource resource ) 383 throws IOException, TransferFailedException, ResourceDoesNotExistException 384 { 385 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 386 { 387 throw new ResourceDoesNotExistException( 388 "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" ); 389 } 390 391 String[] dirs = PathUtils.dirnames( resource.getName() ); 392 393 for ( String dir : dirs ) 394 { 395 boolean dirChanged = ftp.changeWorkingDirectory( dir ); 396 397 if ( !dirChanged ) 398 { 399 String msg = "Resource " + resource + " not found. Directory " + dir + " does not exist"; 400 401 throw new ResourceDoesNotExistException( msg ); 402 } 403 } 404 } 405 406 public class PrintCommandListener 407 implements ProtocolCommandListener 408 { 409 private FtpWagon wagon; 410 411 public PrintCommandListener( FtpWagon wagon ) 412 { 413 this.wagon = wagon; 414 } 415 416 public void protocolCommandSent( ProtocolCommandEvent event ) 417 { 418 wagon.fireSessionDebug( "Command sent: " + event.getMessage() ); 419 420 } 421 422 public void protocolReplyReceived( ProtocolCommandEvent event ) 423 { 424 wagon.fireSessionDebug( "Reply received: " + event.getMessage() ); 425 } 426 } 427 428 protected void fireSessionDebug( String msg ) 429 { 430 super.fireSessionDebug( msg ); 431 } 432 433 public List<String> getFileList( String destinationDirectory ) 434 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 435 { 436 Resource resource = new Resource( destinationDirectory ); 437 438 try 439 { 440 ftpChangeDirectory( resource ); 441 442 String filename = PathUtils.filename( resource.getName() ); 443 FTPFile[] ftpFiles = ftp.listFiles( filename ); 444 445 if ( ftpFiles == null || ftpFiles.length <= 0 ) 446 { 447 throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" ); 448 } 449 450 List<String> ret = new ArrayList<String>(); 451 for ( FTPFile file : ftpFiles ) 452 { 453 String name = file.getName(); 454 455 if ( file.isDirectory() && !name.endsWith( "/" ) ) 456 { 457 name += "/"; 458 } 459 460 ret.add( name ); 461 } 462 463 return ret; 464 } 465 catch ( IOException e ) 466 { 467 throw new TransferFailedException( "Error transferring file via FTP", e ); 468 } 469 } 470 471 public boolean resourceExists( String resourceName ) 472 throws TransferFailedException, AuthorizationException 473 { 474 Resource resource = new Resource( resourceName ); 475 476 try 477 { 478 ftpChangeDirectory( resource ); 479 480 String filename = PathUtils.filename( resource.getName() ); 481 int status = ftp.stat( filename ); 482 483 return ( ( status == FTPReply.FILE_STATUS ) || ( status == FTPReply.DIRECTORY_STATUS ) || ( status 484 == FTPReply.FILE_STATUS_OK ) // not in the RFC but used by some FTP servers 485 || ( status == FTPReply.COMMAND_OK ) // not in the RFC but used by some FTP servers 486 || ( status == FTPReply.SYSTEM_STATUS ) ); 487 } 488 catch ( IOException e ) 489 { 490 throw new TransferFailedException( "Error transferring file via FTP", e ); 491 } 492 catch ( ResourceDoesNotExistException e ) 493 { 494 return false; 495 } 496 } 497 498 public boolean supportsDirectoryCopy() 499 { 500 return true; 501 } 502 503 public void putDirectory( File sourceDirectory, String destinationDirectory ) 504 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 505 { 506 507 // Change to root. 508 try 509 { 510 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 511 { 512 RepositoryPermissions permissions = getRepository().getPermissions(); 513 if ( !makeFtpDirectoryRecursive( getRepository().getBasedir(), permissions ) ) 514 { 515 throw new TransferFailedException( 516 "Required directory: '" + getRepository().getBasedir() + "' " + "could not get created" ); 517 } 518 519 // try it again sam ... 520 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 521 { 522 throw new TransferFailedException( "Required directory: '" + getRepository().getBasedir() + "' " 523 + "is missing and could not get created" ); 524 } 525 } 526 } 527 catch ( IOException e ) 528 { 529 throw new TransferFailedException( "Cannot change to root path " + getRepository().getBasedir(), e ); 530 } 531 532 fireTransferDebug( 533 "Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as " + destinationDirectory ); 534 ftpRecursivePut( sourceDirectory, destinationDirectory ); 535 } 536 537 private void ftpRecursivePut( File sourceFile, String fileName ) 538 throws TransferFailedException 539 { 540 final RepositoryPermissions permissions = repository.getPermissions(); 541 542 fireTransferDebug( "processing = " + sourceFile.getAbsolutePath() + " as " + fileName ); 543 544 if ( sourceFile.isDirectory() ) 545 { 546 if ( !fileName.equals( "." ) ) 547 { 548 try 549 { 550 // change directory if it already exists. 551 if ( !ftp.changeWorkingDirectory( fileName ) ) 552 { 553 // first, try to create it 554 if ( makeFtpDirectoryRecursive( fileName, permissions ) ) 555 { 556 if ( !ftp.changeWorkingDirectory( fileName ) ) 557 { 558 throw new TransferFailedException( 559 "Unable to change cwd on ftp server to " + fileName + " when processing " 560 + sourceFile.getAbsolutePath() ); 561 } 562 } 563 else 564 { 565 throw new TransferFailedException( 566 "Unable to create directory " + fileName + " when processing " 567 + sourceFile.getAbsolutePath() ); 568 } 569 } 570 } 571 catch ( IOException e ) 572 { 573 throw new TransferFailedException( 574 "IOException caught while processing path at " + sourceFile.getAbsolutePath(), e ); 575 } 576 } 577 578 File[] files = sourceFile.listFiles(); 579 if ( files != null && files.length > 0 ) 580 { 581 fireTransferDebug( "listing children of = " + sourceFile.getAbsolutePath() + " found " + files.length ); 582 583 // Directories first, then files. Let's go deep early. 584 for ( File file : files ) 585 { 586 if ( file.isDirectory() ) 587 { 588 ftpRecursivePut( file, file.getName() ); 589 } 590 } 591 for ( File file : files ) 592 { 593 if ( !file.isDirectory() ) 594 { 595 ftpRecursivePut( file, file.getName() ); 596 } 597 } 598 } 599 600 // Step back up a directory once we're done with the contents of this one. 601 try 602 { 603 ftp.changeToParentDirectory(); 604 } 605 catch ( IOException e ) 606 { 607 throw new TransferFailedException( "IOException caught while attempting to step up to parent directory" 608 + " after successfully processing " 609 + sourceFile.getAbsolutePath(), e ); 610 } 611 } 612 else 613 { 614 // Oh how I hope and pray, in denial, but today I am still just a file. 615 616 FileInputStream sourceFileStream = null; 617 try 618 { 619 sourceFileStream = new FileInputStream( sourceFile ); 620 621 // It's a file. Upload it in the current directory. 622 if ( ftp.storeFile( fileName, sourceFileStream ) ) 623 { 624 if ( permissions != null ) 625 { 626 // Process permissions; note that if we get errors or exceptions here, they are ignored. 627 // This appears to be a conscious decision, based on other parts of this code. 628 String group = permissions.getGroup(); 629 if ( group != null ) 630 { 631 try 632 { 633 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() ); 634 } 635 catch ( IOException e ) 636 { 637 } 638 } 639 String mode = permissions.getFileMode(); 640 if ( mode != null ) 641 { 642 try 643 { 644 ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() ); 645 } 646 catch ( IOException e ) 647 { 648 } 649 } 650 } 651 } 652 else 653 { 654 String msg = 655 "Cannot transfer resource: '" + sourceFile.getAbsolutePath() + "' FTP Server response: " 656 + ftp.getReplyString(); 657 throw new TransferFailedException( msg ); 658 } 659 } 660 catch ( IOException e ) 661 { 662 throw new TransferFailedException( 663 "IOException caught while attempting to upload " + sourceFile.getAbsolutePath(), e ); 664 } 665 finally 666 { 667 IOUtils.closeQuietly( sourceFileStream ); 668 } 669 670 } 671 672 fireTransferDebug( "completed = " + sourceFile.getAbsolutePath() ); 673 } 674 675 /** 676 * Set the permissions (if given) for the underlying folder. 677 * Note: Since the FTP SITE command might be server dependent, we cannot 678 * rely on the functionality available on each FTP server! 679 * So we can only try and hope it works (and catch away all Exceptions). 680 * 681 * @param permissions group and directory permissions 682 */ 683 private void setPermissions( RepositoryPermissions permissions ) 684 { 685 if ( permissions != null ) 686 { 687 // Process permissions; note that if we get errors or exceptions here, they are ignored. 688 // This appears to be a conscious decision, based on other parts of this code. 689 String group = permissions.getGroup(); 690 if ( group != null ) 691 { 692 try 693 { 694 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() ); 695 } 696 catch ( IOException e ) 697 { 698 } 699 } 700 String mode = permissions.getDirectoryMode(); 701 if ( mode != null ) 702 { 703 try 704 { 705 ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() ); 706 } 707 catch ( IOException e ) 708 { 709 } 710 } 711 } 712 } 713 714 /** 715 * Recursively create directories. 716 * 717 * @param fileName the path to create (might be nested) 718 * @param permissions 719 * @return ok 720 * @throws IOException 721 */ 722 private boolean makeFtpDirectoryRecursive( String fileName, RepositoryPermissions permissions ) 723 throws IOException 724 { 725 if ( fileName == null || fileName.length() == 0 726 || fileName.replace( '/', '_' ).trim().length() == 0 ) // if a string is '/', '//' or similar 727 { 728 return false; 729 } 730 731 int slashPos = fileName.indexOf( "/" ); 732 String oldPwd = null; 733 boolean ok = true; 734 735 if ( slashPos == 0 ) 736 { 737 // this is an absolute directory 738 oldPwd = ftp.printWorkingDirectory(); 739 740 // start with the root 741 ftp.changeWorkingDirectory( "/" ); 742 fileName = fileName.substring( 1 ); 743 744 // look up the next path separator 745 slashPos = fileName.indexOf( "/" ); 746 } 747 748 if ( slashPos >= 0 && slashPos < ( fileName.length() - 1 ) ) // not only a slash at the end, like in 'newDir/' 749 { 750 if ( oldPwd == null ) 751 { 752 oldPwd = ftp.printWorkingDirectory(); 753 } 754 755 String nextDir = fileName.substring( 0, slashPos ); 756 757 // we only create the nextDir if it doesn't yet exist 758 if ( !ftp.changeWorkingDirectory( nextDir ) ) 759 { 760 ok &= ftp.makeDirectory( nextDir ); 761 } 762 763 if ( ok ) 764 { 765 // set the permissions for the freshly created directory 766 setPermissions( permissions ); 767 768 ftp.changeWorkingDirectory( nextDir ); 769 770 // now create the deeper directories 771 String remainingDirs = fileName.substring( slashPos + 1 ); 772 ok &= makeFtpDirectoryRecursive( remainingDirs, permissions ); 773 } 774 } 775 else 776 { 777 ok = ftp.makeDirectory( fileName ); 778 } 779 780 if ( oldPwd != null ) 781 { 782 // change back to the old working directory 783 ftp.changeWorkingDirectory( oldPwd ); 784 } 785 786 return ok; 787 } 788 789 public String getControlEncoding() 790 { 791 return controlEncoding; 792 } 793 794 public void setControlEncoding( String controlEncoding ) 795 { 796 this.controlEncoding = controlEncoding; 797 } 798}