| package org.apache.maven.plugins.stage; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import org.apache.maven.artifact.manager.WagonManager; |
| import org.apache.maven.artifact.repository.ArtifactRepository; |
| import org.apache.maven.artifact.repository.metadata.Metadata; |
| import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader; |
| import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer; |
| import org.apache.maven.wagon.ConnectionException; |
| import org.apache.maven.wagon.ResourceDoesNotExistException; |
| import org.apache.maven.wagon.TransferFailedException; |
| import org.apache.maven.wagon.UnsupportedProtocolException; |
| import org.apache.maven.wagon.Wagon; |
| import org.apache.maven.wagon.WagonException; |
| import org.apache.maven.wagon.authentication.AuthenticationException; |
| import org.apache.maven.wagon.authentication.AuthenticationInfo; |
| import org.apache.maven.wagon.authorization.AuthorizationException; |
| import org.apache.maven.wagon.providers.ssh.jsch.ScpWagon; |
| import org.apache.maven.wagon.repository.Repository; |
| import org.codehaus.plexus.logging.LogEnabled; |
| import org.codehaus.plexus.logging.Logger; |
| import org.codehaus.plexus.util.FileUtils; |
| import org.codehaus.plexus.util.IOUtil; |
| import org.codehaus.plexus.util.StringUtils; |
| import org.codehaus.plexus.util.xml.pull.XmlPullParserException; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.io.Reader; |
| import java.io.Writer; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipOutputStream; |
| |
| /** |
| * @author Jason van Zyl |
| * @plexus.component |
| */ |
| public class DefaultRepositoryCopier |
| implements LogEnabled, RepositoryCopier |
| { |
| private MetadataXpp3Reader reader = new MetadataXpp3Reader(); |
| |
| private MetadataXpp3Writer writer = new MetadataXpp3Writer(); |
| |
| /** @plexus.requirement */ |
| private WagonManager wagonManager; |
| |
| private Logger logger; |
| |
| public void copy( Repository sourceRepository, Repository targetRepository, String version ) |
| throws WagonException, IOException |
| { |
| String prefix = "staging-plugin"; |
| |
| String fileName = prefix + "-" + version + ".zip"; |
| |
| String tempdir = System.getProperty( "java.io.tmpdir" ); |
| |
| logger.debug( "Writing all output to " + tempdir ); |
| |
| // Create the renameScript script |
| |
| String renameScriptName = prefix + "-" + version + "-rename.sh"; |
| |
| File renameScript = new File( tempdir, renameScriptName ); |
| |
| // Work directory |
| |
| File basedir = new File( tempdir, prefix + "-" + version ); |
| |
| FileUtils.deleteDirectory( basedir ); |
| |
| basedir.mkdirs(); |
| |
| String protocol = sourceRepository.getProtocol(); |
| |
| Wagon sourceWagon = wagonManager.getWagon( sourceRepository ); |
| AuthenticationInfo sourceAuth = wagonManager.getAuthenticationInfo( sourceRepository.getId() ); |
| |
| sourceWagon.connect( sourceRepository, sourceAuth ); |
| |
| logger.info( "Looking for files in the source repository." ); |
| |
| List files = new ArrayList(); |
| |
| scan( sourceWagon, "", files ); |
| |
| logger.info( "Downloading files from the source repository to: " + basedir ); |
| |
| for ( Iterator i = files.iterator(); i.hasNext(); ) |
| { |
| String s = (String) i.next(); |
| |
| if ( s.indexOf( ".svn" ) >= 0 ) |
| { |
| continue; |
| } |
| |
| File f = new File( basedir, s ); |
| |
| FileUtils.mkdir( f.getParentFile().getAbsolutePath() ); |
| |
| logger.info( "Downloading file from the source repository: " + s ); |
| |
| sourceWagon.get( s, f ); |
| } |
| |
| // ---------------------------------------------------------------------------- |
| // Now all the files are present locally and now we are going to grab the |
| // metadata files from the targetRepositoryUrl and pull those down locally |
| // so that we can merge the metadata. |
| // ---------------------------------------------------------------------------- |
| |
| logger.info( "Downloading metadata from the target repository." ); |
| |
| Wagon targetWagon = wagonManager.getWagon( targetRepository ); |
| AuthenticationInfo targetAuth = wagonManager.getAuthenticationInfo( targetRepository.getId() ); |
| |
| targetWagon.connect( targetRepository, targetAuth ); |
| |
| PrintWriter rw = new PrintWriter( new FileWriter( renameScript ) ); |
| |
| File archive = new File( tempdir, fileName ); |
| |
| for ( Iterator i = files.iterator(); i.hasNext(); ) |
| { |
| String s = (String) i.next(); |
| |
| if ( s.startsWith( "/" ) ) |
| { |
| s = s.substring( 1 ); |
| } |
| |
| if ( s.endsWith( MAVEN_METADATA ) ) |
| { |
| File emf = new File( basedir, s + IN_PROCESS_MARKER ); |
| |
| try |
| { |
| targetWagon.get( s, emf ); |
| } |
| catch ( ResourceDoesNotExistException e ) |
| { |
| // We don't have an equivalent on the targetRepositoryUrl side because we have something |
| // new on the sourceRepositoryUrl side so just skip the metadata merging. |
| |
| continue; |
| } |
| |
| try |
| { |
| mergeMetadata( emf ); |
| } |
| catch ( XmlPullParserException e ) |
| { |
| throw new IOException( "Metadata file is corrupt " + s + " Reason: " + e.getMessage() ); |
| } |
| } |
| } |
| |
| Set moveCommands = new TreeSet(); |
| |
| // ---------------------------------------------------------------------------- |
| // Create the Zip file that we will deploy to the targetRepositoryUrl stage |
| // ---------------------------------------------------------------------------- |
| |
| logger.info( "Creating zip file." ); |
| |
| OutputStream os = new FileOutputStream( archive ); |
| |
| ZipOutputStream zos = new ZipOutputStream( os ); |
| |
| scanDirectory( basedir, basedir, zos, version, moveCommands ); |
| |
| // ---------------------------------------------------------------------------- |
| // Create the renameScript script. This is as atomic as we can |
| // ---------------------------------------------------------------------------- |
| |
| logger.info( "Creating rename script." ); |
| |
| for ( Iterator i = moveCommands.iterator(); i.hasNext(); ) |
| { |
| String s = (String) i.next(); |
| |
| // We use an explicit unix '\n' line-ending here instead of using the println() method. |
| // Using println() will cause files and folders to have a '\r' at the end if the plugin is run on Windows. |
| rw.print( s + "\n" ); |
| } |
| |
| IOUtil.close( rw ); |
| |
| ZipEntry e = new ZipEntry( renameScript.getName() ); |
| |
| zos.putNextEntry( e ); |
| |
| InputStream is = new FileInputStream( renameScript ); |
| |
| IOUtil.copy( is, zos ); |
| |
| IOUtil.close( is ); |
| |
| IOUtil.close( zos ); |
| |
| sourceWagon.disconnect(); |
| |
| // Push the Zip to the target system |
| |
| logger.info( "Uploading zip file to the target repository." ); |
| |
| targetWagon.put( archive, fileName ); |
| |
| logger.info( "Unpacking zip file on the target machine." ); |
| |
| String targetRepoBaseDirectory = targetRepository.getBasedir(); |
| |
| // We use the super quiet option here as all the noise seems to kill/stall the connection |
| |
| String command = "unzip -o -qq -d " + targetRepoBaseDirectory + " " + targetRepoBaseDirectory + "/" + fileName; |
| |
| ( (ScpWagon) targetWagon ).executeCommand( command ); |
| |
| logger.info( "Deleting zip file from the target repository." ); |
| |
| command = "rm -f " + targetRepoBaseDirectory + "/" + fileName; |
| |
| ( (ScpWagon) targetWagon ).executeCommand( command ); |
| |
| logger.info( "Running rename script on the target machine." ); |
| |
| command = "cd " + targetRepoBaseDirectory + "; sh " + renameScriptName; |
| |
| ( (ScpWagon) targetWagon ).executeCommand( command ); |
| |
| logger.info( "Deleting rename script from the target repository." ); |
| |
| command = "rm -f " + targetRepoBaseDirectory + "/" + renameScriptName; |
| |
| ( (ScpWagon) targetWagon ).executeCommand( command ); |
| |
| targetWagon.disconnect(); |
| } |
| |
| private void scanDirectory( File basedir, File dir, ZipOutputStream zos, String version, Set moveCommands ) |
| throws IOException |
| { |
| if ( dir == null ) |
| { |
| return; |
| } |
| |
| File[] files = dir.listFiles(); |
| |
| for ( int i = 0; i < files.length; i++ ) |
| { |
| File f = files[i]; |
| |
| if ( f.isDirectory() ) |
| { |
| if ( f.getName().equals( ".svn" ) ) |
| { |
| continue; |
| } |
| |
| if ( f.getName().endsWith( version ) ) |
| { |
| String s = f.getAbsolutePath().substring( basedir.getAbsolutePath().length() + 1 ); |
| s = StringUtils.replace( s, "\\", "/" ); |
| |
| moveCommands.add( "mv " + s + IN_PROCESS_MARKER + " " + s ); |
| } |
| |
| scanDirectory( basedir, f, zos, version, moveCommands ); |
| } |
| else |
| { |
| InputStream is = new FileInputStream( f ); |
| |
| String s = f.getAbsolutePath().substring( basedir.getAbsolutePath().length() + 1 ); |
| s = StringUtils.replace( s, "\\", "/" ); |
| |
| // We are marking any version directories with the in-process flag so that |
| // anything being unpacked on the target side will not be recogized by Maven |
| // and so users cannot download partially uploaded files. |
| |
| String vtag = "/" + version; |
| |
| s = StringUtils.replace( s, vtag + "/", vtag + IN_PROCESS_MARKER + "/" ); |
| |
| ZipEntry e = new ZipEntry( s ); |
| |
| zos.putNextEntry( e ); |
| |
| IOUtil.copy( is, zos ); |
| |
| IOUtil.close( is ); |
| |
| int idx = s.indexOf( IN_PROCESS_MARKER ); |
| |
| if ( idx > 0 ) |
| { |
| String d = s.substring( 0, idx ); |
| |
| moveCommands.add( "mv " + d + IN_PROCESS_MARKER + " " + d ); |
| } |
| } |
| } |
| } |
| |
| private void mergeMetadata( File existingMetadata ) |
| throws IOException, XmlPullParserException |
| { |
| // Existing Metadata in target stage |
| |
| Reader existingMetadataReader = new FileReader( existingMetadata ); |
| |
| Metadata existing = reader.read( existingMetadataReader ); |
| |
| // Staged Metadata |
| |
| File stagedMetadataFile = new File( existingMetadata.getParentFile(), MAVEN_METADATA ); |
| |
| Reader stagedMetadataReader = new FileReader( stagedMetadataFile ); |
| |
| Metadata staged = reader.read( stagedMetadataReader ); |
| |
| // Merge |
| |
| existing.merge( staged ); |
| |
| Writer writer = new FileWriter( existingMetadata ); |
| |
| this.writer.write( writer, existing ); |
| |
| IOUtil.close( writer ); |
| |
| IOUtil.close( stagedMetadataReader ); |
| |
| IOUtil.close( existingMetadataReader ); |
| |
| // Mark all metadata as in-process and regenerate the checksums as they will be different |
| // after the merger |
| |
| try |
| { |
| File newMd5 = new File( existingMetadata.getParentFile(), MAVEN_METADATA + ".md5" + IN_PROCESS_MARKER ); |
| |
| FileUtils.fileWrite( newMd5.getAbsolutePath(), checksum( existingMetadata, MD5 ) ); |
| |
| File oldMd5 = new File( existingMetadata.getParentFile(), MAVEN_METADATA + ".md5" ); |
| |
| oldMd5.delete(); |
| |
| File newSha1 = new File( existingMetadata.getParentFile(), MAVEN_METADATA + ".sha1" + IN_PROCESS_MARKER ); |
| |
| FileUtils.fileWrite( newSha1.getAbsolutePath(), checksum( existingMetadata, SHA1 ) ); |
| |
| File oldSha1 = new File( existingMetadata.getParentFile(), MAVEN_METADATA + ".sha1" ); |
| |
| oldSha1.delete(); |
| } |
| catch ( NoSuchAlgorithmException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| |
| // We have the new merged copy so we're good |
| |
| stagedMetadataFile.delete(); |
| } |
| |
| private String checksum( File file, |
| String type ) |
| throws IOException, NoSuchAlgorithmException |
| { |
| MessageDigest md5 = MessageDigest.getInstance( type ); |
| |
| InputStream is = new FileInputStream( file ); |
| |
| byte[] buf = new byte[8192]; |
| |
| int i; |
| |
| while ( ( i = is.read( buf ) ) > 0 ) |
| { |
| md5.update( buf, 0, i ); |
| } |
| |
| IOUtil.close( is ); |
| |
| return encode( md5.digest() ); |
| } |
| |
| protected String encode( byte[] binaryData ) |
| { |
| if ( binaryData.length != 16 && binaryData.length != 20 ) |
| { |
| int bitLength = binaryData.length * 8; |
| throw new IllegalArgumentException( "Unrecognised length for binary data: " + bitLength + " bits" ); |
| } |
| |
| String retValue = ""; |
| |
| for ( int i = 0; i < binaryData.length; i++ ) |
| { |
| String t = Integer.toHexString( binaryData[i] & 0xff ); |
| |
| if ( t.length() == 1 ) |
| { |
| retValue += ( "0" + t ); |
| } |
| else |
| { |
| retValue += t; |
| } |
| } |
| |
| return retValue.trim(); |
| } |
| |
| private void scan( Wagon wagon, |
| String basePath, |
| List collected ) |
| { |
| try |
| { |
| List files = wagon.getFileList( basePath ); |
| |
| if ( files.isEmpty() ) |
| { |
| collected.add( basePath ); |
| } |
| else |
| { |
| basePath = basePath + "/"; |
| for ( Iterator iterator = files.iterator(); iterator.hasNext(); ) |
| { |
| String file = (String) iterator.next(); |
| logger.info( "Found file in the source repository: " + file ); |
| scan( wagon, basePath + file, collected ); |
| } |
| } |
| } |
| catch ( TransferFailedException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| catch ( ResourceDoesNotExistException e ) |
| { |
| // is thrown when calling getFileList on a file |
| collected.add( basePath ); |
| } |
| catch ( AuthorizationException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| |
| } |
| |
| protected List scanForArtifactPaths( ArtifactRepository repository ) |
| { |
| List collected; |
| try |
| { |
| Wagon wagon = wagonManager.getWagon( repository.getProtocol() ); |
| Repository artifactRepository = new Repository( repository.getId(), repository.getUrl() ); |
| wagon.connect( artifactRepository ); |
| collected = new ArrayList(); |
| scan( wagon, "/", collected ); |
| wagon.disconnect(); |
| |
| return collected; |
| |
| } |
| catch ( UnsupportedProtocolException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| catch ( ConnectionException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| catch ( AuthenticationException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| } |
| |
| public void enableLogging( Logger logger ) |
| { |
| this.logger = logger; |
| } |
| } |