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