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