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 ( String d : dirs )
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().contains( "Can't change directory" ) )
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            // ScpHelper.getResourceFilename( destinationDirectory ) - could return empty string
296            if ( !fileName.equals( "." ) && !fileName.equals( "" ) )
297            {
298                prefix = getFileName( prefix, fileName );
299                mkdir( fileName, directoryMode );
300                channel.cd( fileName );
301            }
302
303            File[] files = sourceFile.listFiles();
304            if ( files != null && files.length > 0 )
305            {
306                // Directories first, then files. Let's go deep early.
307                for ( File file : files )
308                {
309                    if ( file.isDirectory() )
310                    {
311                        ftpRecursivePut( file, prefix, file.getName(), directoryMode );
312                    }
313                }
314                for ( File file : files )
315                {
316                    if ( !file.isDirectory() )
317                    {
318                        ftpRecursivePut( file, prefix, file.getName(), directoryMode );
319                    }
320                }
321            }
322            
323            channel.cd( ".." );
324        }
325        else
326        {
327            Resource resource = ScpHelper.getResource( getFileName( prefix, fileName ) );
328
329            firePutInitiated( resource, sourceFile );
330
331            putFile( sourceFile, resource, permissions );
332        }
333    }
334
335    private String getFileName( String prefix, String fileName )
336    {
337        if ( prefix != null )
338        {
339            prefix = prefix + "/" + fileName;
340        }
341        else
342        {
343            prefix = fileName;
344        }
345        return prefix;
346    }
347    
348    public List<String> getFileList( String destinationDirectory )
349        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
350    {
351        if ( destinationDirectory.length() == 0 )
352        {
353            destinationDirectory = ".";
354        }
355        
356        String filename = ScpHelper.getResourceFilename( destinationDirectory );
357
358        String dir = ScpHelper.getResourceDirectory( destinationDirectory );
359
360        // we already setuped the root directory. Ignore beginning /
361        if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
362        {
363            dir = dir.substring( 1 );
364        }
365
366        try
367        {
368            SftpATTRS attrs = changeToRepositoryDirectory( dir, filename );
369            if ( ( attrs.getPermissions() & S_IFDIR ) == 0 )
370            {
371                throw new TransferFailedException( "Remote path is not a directory:" + dir );
372            }
373
374            @SuppressWarnings( "unchecked" )
375            List<ChannelSftp.LsEntry> fileList = channel.ls( filename );
376            List<String> files = new ArrayList<String>( fileList.size() );
377            for ( ChannelSftp.LsEntry entry : fileList )
378            {
379                String name = entry.getFilename();
380                if ( entry.getAttrs().isDir() )
381                {
382                    if ( !name.equals( "." ) && !name.equals( ".." ) )
383                    {
384                        if ( !name.endsWith( "/" ) )
385                        {
386                            name += "/";
387                        }
388                        files.add( name );
389                    }
390                }
391                else
392                {
393                    files.add( name );
394                }
395            }
396            return files;
397        }
398        catch ( SftpException e )
399        {
400            String msg =
401                "Error occurred while listing '" + destinationDirectory + "' " + "on remote repository: "
402                    + getRepository().getUrl() + ": " + e.getMessage();
403
404            throw new TransferFailedException( msg, e );
405        }
406    }
407    
408    public boolean resourceExists( String resourceName )
409        throws TransferFailedException, AuthorizationException
410    {
411        String filename = ScpHelper.getResourceFilename( resourceName );
412
413        String dir = ScpHelper.getResourceDirectory( resourceName );
414
415        // we already setuped the root directory. Ignore beginning /
416        if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
417        {
418            dir = dir.substring( 1 );
419        }
420
421        try
422        {
423            changeToRepositoryDirectory( dir, filename );
424            
425            return true;
426        }
427        catch ( ResourceDoesNotExistException e )
428        {
429            return false;
430        }
431        catch ( SftpException e )
432        {
433            String msg =
434                "Error occurred while looking for '" + resourceName + "' " + "on remote repository: "
435                    + getRepository().getUrl() + ": " + e.getMessage();
436
437            throw new TransferFailedException( msg, e );
438        }
439    }
440
441    protected void cleanupGetTransfer( Resource resource )
442    {
443        returnToParentDirectory( resource );
444    }
445    
446    protected void cleanupPutTransfer( Resource resource )
447    {
448        returnToParentDirectory( resource );
449    }
450
451    protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
452        throws TransferFailedException
453    {
454        RepositoryPermissions permissions = getRepository().getPermissions();
455
456        String filename = ScpHelper.getResourceFilename( resource.getName() );
457        if ( permissions != null && permissions.getGroup() != null )
458        {
459            setGroup( filename, permissions );
460        }
461        
462        if ( permissions != null && permissions.getFileMode() != null )
463        {
464            setFileMode( filename, permissions );
465        }
466    }
467
468    public void fillInputData( InputData inputData )
469        throws TransferFailedException, ResourceDoesNotExistException
470    {
471        Resource resource = inputData.getResource();
472        
473        String filename = ScpHelper.getResourceFilename( resource.getName() );
474
475        String dir = ScpHelper.getResourceDirectory( resource.getName() );
476
477        // we already setuped the root directory. Ignore beginning /
478        if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
479        {
480            dir = dir.substring( 1 );
481        }
482
483        try
484        {
485            SftpATTRS attrs = changeToRepositoryDirectory( dir, filename );
486
487            long lastModified = attrs.getMTime() * MILLIS_PER_SEC;
488            resource.setContentLength( attrs.getSize() );
489
490            resource.setLastModified( lastModified );
491            
492            inputData.setInputStream( channel.get( filename ) );
493        }
494        catch ( SftpException e )
495        {
496            handleGetException( resource, e );
497        }
498    }
499
500    public void fillOutputData( OutputData outputData )
501        throws TransferFailedException
502    {
503        int directoryMode = getDirectoryMode( getRepository().getPermissions() );
504
505        Resource resource = outputData.getResource();
506        
507        try
508        {
509            channel.cd( "/" );
510
511            String basedir = getRepository().getBasedir();
512            mkdirs( basedir + "/", directoryMode );
513
514            mkdirs( resource.getName(), directoryMode );
515
516            String filename = ScpHelper.getResourceFilename( resource.getName() );
517            outputData.setOutputStream( channel.put( filename ) );
518        }
519        catch ( TransferFailedException e )
520        {
521            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
522
523            throw e;
524        }
525        catch ( SftpException e )
526        {
527            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
528
529            String msg =
530                "Error occurred while deploying '" + resource.getName() + "' " + "to remote repository: "
531                    + getRepository().getUrl() + ": " + e.getMessage();
532
533            throw new TransferFailedException( msg, e );
534        }
535    }
536    
537    /**
538     * @param permissions repository's permissions
539     * @return the directory mode for the repository or <code>-1</code> if it
540     *         wasn't set
541     */
542    public int getDirectoryMode( RepositoryPermissions permissions )
543    {
544        int ret = -1;
545
546        if ( permissions != null )
547        {
548            ret = getOctalMode( permissions.getDirectoryMode() );
549        }
550
551        return ret;
552    }
553
554    public int getOctalMode( String mode )
555    {
556        int ret;
557        try
558        {
559            ret = Integer.valueOf( mode, 8 ).intValue();
560        }
561        catch ( NumberFormatException e )
562        {
563            // TODO: warning level
564            fireTransferDebug( "the file mode must be a numerical mode for SFTP" );
565            ret = -1;
566        }
567        return ret;
568    }
569}