001package org.apache.maven.wagon.providers.ssh.jsch; 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 java.io.File; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.util.ArrayList; 026import java.util.List; 027 028import org.apache.maven.wagon.InputData; 029import org.apache.maven.wagon.OutputData; 030import org.apache.maven.wagon.PathUtils; 031import org.apache.maven.wagon.ResourceDoesNotExistException; 032import org.apache.maven.wagon.TransferFailedException; 033import org.apache.maven.wagon.authentication.AuthenticationException; 034import org.apache.maven.wagon.authorization.AuthorizationException; 035import org.apache.maven.wagon.events.TransferEvent; 036import org.apache.maven.wagon.providers.ssh.ScpHelper; 037import org.apache.maven.wagon.repository.RepositoryPermissions; 038import org.apache.maven.wagon.resource.Resource; 039 040import com.jcraft.jsch.ChannelSftp; 041import com.jcraft.jsch.JSchException; 042import com.jcraft.jsch.SftpATTRS; 043import com.jcraft.jsch.SftpException; 044 045/** 046 * SFTP protocol wagon. 047 * 048 * @author <a href="mailto:brett@apache.org">Brett Porter</a> 049 * 050 * @todo [BP] add compression flag 051 * @todo see if SftpProgressMonitor allows us to do streaming (without it, we can't do checksums as the input stream is lost) 052 * 053 * @plexus.component role="org.apache.maven.wagon.Wagon" 054 * role-hint="sftp" 055 * instantiation-strategy="per-lookup" 056 */ 057public class SftpWagon 058 extends AbstractJschWagon 059{ 060 private static final String SFTP_CHANNEL = "sftp"; 061 062 private static final int S_IFDIR = 0x4000; 063 064 private static final long MILLIS_PER_SEC = 1000L; 065 066 private ChannelSftp channel; 067 068 public void closeConnection() 069 { 070 if ( channel != null ) 071 { 072 channel.disconnect(); 073 } 074 super.closeConnection(); 075 } 076 077 public void openConnectionInternal() 078 throws AuthenticationException 079 { 080 super.openConnectionInternal(); 081 082 try 083 { 084 channel = (ChannelSftp) session.openChannel( SFTP_CHANNEL ); 085 086 channel.connect(); 087 } 088 catch ( JSchException e ) 089 { 090 throw new AuthenticationException( "Error connecting to remote repository: " + getRepository().getUrl(), 091 e ); 092 } 093 } 094 095 private void returnToParentDirectory( Resource resource ) 096 { 097 try 098 { 099 String dir = ScpHelper.getResourceDirectory( resource.getName() ); 100 String[] dirs = PathUtils.dirnames( dir ); 101 for ( int i = 0; i < dirs.length; i++ ) 102 { 103 channel.cd( ".." ); 104 } 105 } 106 catch ( SftpException e ) 107 { 108 fireTransferDebug( "Error returning to parent directory: " + e.getMessage() ); 109 } 110 } 111 112 private void putFile( File source, Resource resource, RepositoryPermissions permissions ) 113 throws SftpException, TransferFailedException 114 { 115 resource.setContentLength( source.length() ); 116 117 resource.setLastModified( source.lastModified() ); 118 119 String filename = ScpHelper.getResourceFilename( resource.getName() ); 120 121 firePutStarted( resource, source ); 122 123 channel.put( source.getAbsolutePath(), filename ); 124 125 postProcessListeners( resource, source, TransferEvent.REQUEST_PUT ); 126 127 if ( permissions != null && permissions.getGroup() != null ) 128 { 129 setGroup( filename, permissions ); 130 } 131 132 if ( permissions != null && permissions.getFileMode() != null ) 133 { 134 setFileMode( filename, permissions ); 135 } 136 137 firePutCompleted( resource, source ); 138 } 139 140 private void setGroup( String filename, RepositoryPermissions permissions ) 141 { 142 try 143 { 144 int group = Integer.valueOf( permissions.getGroup() ).intValue(); 145 channel.chgrp( group, filename ); 146 } 147 catch ( NumberFormatException e ) 148 { 149 // TODO: warning level 150 fireTransferDebug( "Not setting group: must be a numerical GID for SFTP" ); 151 } 152 catch ( SftpException e ) 153 { 154 fireTransferDebug( "Not setting group: " + e.getMessage() ); 155 } 156 } 157 158 private void setFileMode( String filename, RepositoryPermissions permissions ) 159 { 160 try 161 { 162 int mode = getOctalMode( permissions.getFileMode() ); 163 channel.chmod( mode, filename ); 164 } 165 catch ( NumberFormatException e ) 166 { 167 // TODO: warning level 168 fireTransferDebug( "Not setting mode: must be a numerical mode for SFTP" ); 169 } 170 catch ( SftpException e ) 171 { 172 fireTransferDebug( "Not setting mode: " + e.getMessage() ); 173 } 174 } 175 176 private void mkdirs( String resourceName, int mode ) 177 throws SftpException, TransferFailedException 178 { 179 String[] dirs = PathUtils.dirnames( resourceName ); 180 for ( String dir : dirs ) 181 { 182 mkdir( dir, mode ); 183 184 channel.cd( dir ); 185 } 186 } 187 188 private void mkdir( String dir, int mode ) 189 throws TransferFailedException, SftpException 190 { 191 try 192 { 193 SftpATTRS attrs = channel.stat( dir ); 194 if ( ( attrs.getPermissions() & S_IFDIR ) == 0 ) 195 { 196 throw new TransferFailedException( "Remote path is not a directory: " + dir ); 197 } 198 } 199 catch ( SftpException e ) 200 { 201 // doesn't exist, make it and try again 202 channel.mkdir( dir ); 203 if ( mode != -1 ) 204 { 205 try 206 { 207 channel.chmod( mode, dir ); 208 } 209 catch ( SftpException e1 ) 210 { 211 // for some extrange reason we recive this exception, 212 // even when chmod success 213 } 214 } 215 } 216 } 217 218 private SftpATTRS changeToRepositoryDirectory( String dir, String filename ) 219 throws ResourceDoesNotExistException, SftpException 220 { 221 // This must be called first to ensure that if the file doesn't exist it throws an exception 222 SftpATTRS attrs; 223 try 224 { 225 channel.cd( repository.getBasedir() ); 226 227 if ( dir.length() > 0 ) 228 { 229 channel.cd( dir ); 230 } 231 232 if ( filename.length() == 0 ) 233 { 234 filename = "."; 235 } 236 237 attrs = channel.stat( filename ); 238 } 239 catch ( SftpException e ) 240 { 241 if ( e.toString().trim().endsWith( "No such file" ) ) 242 { 243 throw new ResourceDoesNotExistException( e.toString(), e ); 244 } 245 else if ( e.toString().trim().indexOf( "Can't change directory" ) != -1 ) 246 { 247 throw new ResourceDoesNotExistException( e.toString(), e ); 248 } 249 else 250 { 251 throw e; 252 } 253 } 254 return attrs; 255 } 256 257 public void putDirectory( File sourceDirectory, String destinationDirectory ) 258 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 259 { 260 final RepositoryPermissions permissions = repository.getPermissions(); 261 262 try 263 { 264 channel.cd( "/" ); 265 266 String basedir = getRepository().getBasedir(); 267 int directoryMode = getDirectoryMode( permissions ); 268 269 mkdirs( basedir + "/", directoryMode ); 270 271 fireTransferDebug( "Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as " 272 + destinationDirectory ); 273 274 mkdirs( destinationDirectory, directoryMode ); 275 ftpRecursivePut( sourceDirectory, null, ScpHelper.getResourceFilename( destinationDirectory ), 276 directoryMode ); 277 } 278 catch ( SftpException e ) 279 { 280 String msg = 281 "Error occurred while deploying '" + sourceDirectory.getAbsolutePath() + "' " + "to remote repository: " 282 + getRepository().getUrl() + ": " + e.getMessage(); 283 284 throw new TransferFailedException( msg, e ); 285 } 286 } 287 288 private void ftpRecursivePut( File sourceFile, String prefix, String fileName, int directoryMode ) 289 throws TransferFailedException, SftpException 290 { 291 final RepositoryPermissions permissions = repository.getPermissions(); 292 293 if ( sourceFile.isDirectory() ) 294 { 295 if ( !fileName.equals( "." ) ) 296 { 297 prefix = getFileName( prefix, fileName ); 298 mkdir( fileName, directoryMode ); 299 channel.cd( fileName ); 300 } 301 302 File[] files = sourceFile.listFiles(); 303 if ( files != null && files.length > 0 ) 304 { 305 // Directories first, then files. Let's go deep early. 306 for ( File file : files ) 307 { 308 if ( file.isDirectory() ) 309 { 310 ftpRecursivePut( file, prefix, file.getName(), directoryMode ); 311 } 312 } 313 for ( File file : files ) 314 { 315 if ( !file.isDirectory() ) 316 { 317 ftpRecursivePut( file, prefix, file.getName(), directoryMode ); 318 } 319 } 320 } 321 322 channel.cd( ".." ); 323 } 324 else 325 { 326 Resource resource = ScpHelper.getResource( getFileName( prefix, fileName ) ); 327 328 firePutInitiated( resource, sourceFile ); 329 330 putFile( sourceFile, resource, permissions ); 331 } 332 } 333 334 private String getFileName( String prefix, String fileName ) 335 { 336 if ( prefix != null ) 337 { 338 prefix = prefix + "/" + fileName; 339 } 340 else 341 { 342 prefix = fileName; 343 } 344 return prefix; 345 } 346 347 public List<String> getFileList( String destinationDirectory ) 348 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 349 { 350 if ( destinationDirectory.length() == 0 ) 351 { 352 destinationDirectory = "."; 353 } 354 355 String filename = ScpHelper.getResourceFilename( destinationDirectory ); 356 357 String dir = ScpHelper.getResourceDirectory( destinationDirectory ); 358 359 // we already setuped the root directory. Ignore beginning / 360 if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR ) 361 { 362 dir = dir.substring( 1 ); 363 } 364 365 try 366 { 367 SftpATTRS attrs = changeToRepositoryDirectory( dir, filename ); 368 if ( ( attrs.getPermissions() & S_IFDIR ) == 0 ) 369 { 370 throw new TransferFailedException( "Remote path is not a directory:" + dir ); 371 } 372 373 @SuppressWarnings( "unchecked" ) 374 List<ChannelSftp.LsEntry> fileList = channel.ls( filename ); 375 List<String> files = new ArrayList<String>( fileList.size() ); 376 for ( ChannelSftp.LsEntry entry : fileList ) 377 { 378 String name = entry.getFilename(); 379 if ( entry.getAttrs().isDir() ) 380 { 381 if ( !name.equals( "." ) && !name.equals( ".." ) ) 382 { 383 if ( !name.endsWith( "/" ) ) 384 { 385 name += "/"; 386 } 387 files.add( name ); 388 } 389 } 390 else 391 { 392 files.add( name ); 393 } 394 } 395 return files; 396 } 397 catch ( SftpException e ) 398 { 399 String msg = 400 "Error occurred while listing '" + destinationDirectory + "' " + "on remote repository: " 401 + getRepository().getUrl() + ": " + e.getMessage(); 402 403 throw new TransferFailedException( msg, e ); 404 } 405 } 406 407 public boolean resourceExists( String resourceName ) 408 throws TransferFailedException, AuthorizationException 409 { 410 String filename = ScpHelper.getResourceFilename( resourceName ); 411 412 String dir = ScpHelper.getResourceDirectory( resourceName ); 413 414 // we already setuped the root directory. Ignore beginning / 415 if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR ) 416 { 417 dir = dir.substring( 1 ); 418 } 419 420 try 421 { 422 changeToRepositoryDirectory( dir, filename ); 423 424 return true; 425 } 426 catch ( ResourceDoesNotExistException e ) 427 { 428 return false; 429 } 430 catch ( SftpException e ) 431 { 432 String msg = 433 "Error occurred while looking for '" + resourceName + "' " + "on remote repository: " 434 + getRepository().getUrl() + ": " + e.getMessage(); 435 436 throw new TransferFailedException( msg, e ); 437 } 438 } 439 440 protected void cleanupGetTransfer( Resource resource ) 441 { 442 returnToParentDirectory( resource ); 443 } 444 445 protected void cleanupPutTransfer( Resource resource ) 446 { 447 returnToParentDirectory( resource ); 448 } 449 450 protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output ) 451 throws TransferFailedException 452 { 453 RepositoryPermissions permissions = getRepository().getPermissions(); 454 455 String filename = ScpHelper.getResourceFilename( resource.getName() ); 456 if ( permissions != null && permissions.getGroup() != null ) 457 { 458 setGroup( filename, permissions ); 459 } 460 461 if ( permissions != null && permissions.getFileMode() != null ) 462 { 463 setFileMode( filename, permissions ); 464 } 465 } 466 467 public void fillInputData( InputData inputData ) 468 throws TransferFailedException, ResourceDoesNotExistException 469 { 470 Resource resource = inputData.getResource(); 471 472 String filename = ScpHelper.getResourceFilename( resource.getName() ); 473 474 String dir = ScpHelper.getResourceDirectory( resource.getName() ); 475 476 // we already setuped the root directory. Ignore beginning / 477 if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR ) 478 { 479 dir = dir.substring( 1 ); 480 } 481 482 try 483 { 484 SftpATTRS attrs = changeToRepositoryDirectory( dir, filename ); 485 486 long lastModified = attrs.getMTime() * MILLIS_PER_SEC; 487 resource.setContentLength( attrs.getSize() ); 488 489 resource.setLastModified( lastModified ); 490 491 inputData.setInputStream( channel.get( filename ) ); 492 } 493 catch ( SftpException e ) 494 { 495 handleGetException( resource, e ); 496 } 497 } 498 499 public void fillOutputData( OutputData outputData ) 500 throws TransferFailedException 501 { 502 int directoryMode = getDirectoryMode( getRepository().getPermissions() ); 503 504 Resource resource = outputData.getResource(); 505 506 try 507 { 508 channel.cd( "/" ); 509 510 String basedir = getRepository().getBasedir(); 511 mkdirs( basedir + "/", directoryMode ); 512 513 mkdirs( resource.getName(), directoryMode ); 514 515 String filename = ScpHelper.getResourceFilename( resource.getName() ); 516 outputData.setOutputStream( channel.put( filename ) ); 517 } 518 catch ( TransferFailedException e ) 519 { 520 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 521 522 throw e; 523 } 524 catch ( SftpException e ) 525 { 526 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 527 528 String msg = 529 "Error occurred while deploying '" + resource.getName() + "' " + "to remote repository: " 530 + getRepository().getUrl() + ": " + e.getMessage(); 531 532 throw new TransferFailedException( msg, e ); 533 } 534 } 535 536 /** 537 * @param permissions repository's permissions 538 * @return the directory mode for the repository or <code>-1</code> if it 539 * wasn't set 540 */ 541 public int getDirectoryMode( RepositoryPermissions permissions ) 542 { 543 int ret = -1; 544 545 if ( permissions != null ) 546 { 547 ret = getOctalMode( permissions.getDirectoryMode() ); 548 } 549 550 return ret; 551 } 552 553 public int getOctalMode( String mode ) 554 { 555 int ret; 556 try 557 { 558 ret = Integer.valueOf( mode, 8 ).intValue(); 559 } 560 catch ( NumberFormatException e ) 561 { 562 // TODO: warning level 563 fireTransferDebug( "the file mode must be a numerical mode for SFTP" ); 564 ret = -1; 565 } 566 return ret; 567 } 568}