001package org.apache.archiva.consumers.core.repository; 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.archiva.common.utils.VersionUtil; 023import org.apache.archiva.metadata.model.ArtifactMetadata; 024import org.apache.archiva.metadata.model.facets.AuditEvent; 025import org.apache.archiva.metadata.model.maven2.MavenArtifactFacet; 026import org.apache.archiva.metadata.repository.MetadataRepository; 027import org.apache.archiva.metadata.repository.MetadataRepositoryException; 028import org.apache.archiva.metadata.repository.MetadataResolutionException; 029import org.apache.archiva.metadata.repository.RepositorySession; 030import org.apache.archiva.model.ArtifactReference; 031import org.apache.archiva.repository.ContentNotFoundException; 032import org.apache.archiva.repository.ManagedRepositoryContent; 033import org.apache.archiva.repository.events.RepositoryListener; 034import org.apache.commons.lang.StringUtils; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import java.io.IOException; 039import java.nio.file.DirectoryStream; 040import java.nio.file.FileVisitOption; 041import java.nio.file.FileVisitResult; 042import java.nio.file.Files; 043import java.nio.file.Path; 044import java.nio.file.SimpleFileVisitor; 045import java.nio.file.attribute.BasicFileAttributes; 046import java.util.Collection; 047import java.util.HashMap; 048import java.util.HashSet; 049import java.util.List; 050import java.util.Map; 051import java.util.Set; 052 053/** 054 * Base class for all repository purge tasks. 055 */ 056public abstract class AbstractRepositoryPurge 057 implements RepositoryPurge 058{ 059 protected Logger log = LoggerFactory.getLogger( getClass( ) ); 060 061 protected final ManagedRepositoryContent repository; 062 063 protected final RepositorySession repositorySession; 064 065 protected final List<RepositoryListener> listeners; 066 067 private Logger logger = LoggerFactory.getLogger( "org.apache.archiva.AuditLog" ); 068 069 private static final char DELIM = ' '; 070 071 public AbstractRepositoryPurge( ManagedRepositoryContent repository, RepositorySession repositorySession, 072 List<RepositoryListener> listeners ) 073 { 074 this.repository = repository; 075 this.repositorySession = repositorySession; 076 this.listeners = listeners; 077 } 078 079 /* 080 * We have to track namespace, project, project version, artifact version and classifier 081 * There is no metadata class that contains all these properties. 082 */ 083 class ArtifactInfo 084 { 085 final String namespace; 086 final String name; 087 final String projectVersion; 088 String version; 089 String classifier; 090 091 ArtifactInfo( String namespace, String name, String projectVersion, String version ) 092 { 093 this.namespace = namespace; 094 this.name = name; 095 this.projectVersion = projectVersion; 096 this.version = version; 097 } 098 099 ArtifactInfo( String namespace, String name, String projectVersion ) 100 { 101 this.namespace = namespace; 102 this.name = name; 103 this.projectVersion = projectVersion; 104 } 105 106 /* 107 * Creates a info object without version and classifier 108 */ 109 ArtifactInfo projectVersionLevel( ) 110 { 111 return new ArtifactInfo( this.namespace, this.name, this.projectVersion ); 112 } 113 114 public void setClassifier( String classifier ) 115 { 116 this.classifier = classifier; 117 } 118 119 public String getNamespace( ) 120 { 121 return namespace; 122 } 123 124 public String getName( ) 125 { 126 return name; 127 } 128 129 public String getProjectVersion( ) 130 { 131 return projectVersion; 132 } 133 134 public String getVersion( ) 135 { 136 return version; 137 } 138 139 public String getClassifier( ) 140 { 141 return classifier; 142 } 143 144 public boolean hasClassifier( ) 145 { 146 return classifier != null && !"".equals( classifier ); 147 } 148 149 @Override 150 public boolean equals( Object o ) 151 { 152 if ( this == o ) return true; 153 if ( o == null || getClass( ) != o.getClass( ) ) return false; 154 155 ArtifactInfo that = (ArtifactInfo) o; 156 157 if ( !namespace.equals( that.namespace ) ) return false; 158 if ( !name.equals( that.name ) ) return false; 159 if ( !projectVersion.equals( that.projectVersion ) ) return false; 160 if ( !( version != null ? version.equals( that.version ) : that.version == null ) ) return false; 161 return classifier != null ? classifier.equals( that.classifier ) : that.classifier == null; 162 } 163 164 @Override 165 public int hashCode( ) 166 { 167 int result = namespace.hashCode( ); 168 result = 31 * result + name.hashCode( ); 169 result = 31 * result + projectVersion.hashCode( ); 170 result = 31 * result + ( version != null ? version.hashCode( ) : 0 ); 171 result = 31 * result + ( classifier != null ? classifier.hashCode( ) : 0 ); 172 return result; 173 } 174 175 @Override 176 public String toString( ) 177 { 178 final StringBuilder sb = new StringBuilder( "ArtifactInfo{" ); 179 sb.append( "namespace='" ).append( namespace ).append( '\'' ); 180 sb.append( ", name='" ).append( name ).append( '\'' ); 181 sb.append( ", projectVersion='" ).append( projectVersion ).append( '\'' ); 182 sb.append( ", version='" ).append( version ).append( '\'' ); 183 sb.append( ", classifier='" ).append( classifier ).append( '\'' ); 184 sb.append( '}' ); 185 return sb.toString( ); 186 } 187 } 188 189 /** 190 * Purge the repo. Update db and index of removed artifacts. 191 * 192 * @param references 193 */ 194 protected void purge( Set<ArtifactReference> references ) 195 { 196 if ( references != null && !references.isEmpty( ) ) 197 { 198 MetadataRepository metadataRepository = repositorySession.getRepository( ); 199 Map<ArtifactInfo, ArtifactMetadata> metaRemovalList = new HashMap<>( ); 200 Map<String, Collection<ArtifactMetadata>> metaResolved = new HashMap<>( ); 201 for ( ArtifactReference reference : references ) 202 { 203 String baseVersion = VersionUtil.getBaseVersion( reference.getVersion( ) ); 204 // Needed for tracking in the hashmap 205 String metaBaseId = reference.getGroupId( ) + "/" + reference.getArtifactId( ) + "/" + baseVersion; 206 207 if ( !metaResolved.containsKey( metaBaseId ) ) 208 { 209 try 210 { 211 metaResolved.put( metaBaseId, metadataRepository.getArtifacts( repository.getId( ), reference.getGroupId( ), 212 reference.getArtifactId( ), baseVersion ) ); 213 } 214 catch ( MetadataResolutionException e ) 215 { 216 log.error( "Error during metadata retrieval {}: {}", metaBaseId, e.getMessage( ) ); 217 } 218 } 219 Path artifactFile = repository.toFile( reference ).toPath( ); 220 221 for ( RepositoryListener listener : listeners ) 222 { 223 listener.deleteArtifact( metadataRepository, repository.getId( ), reference.getGroupId( ), 224 reference.getArtifactId( ), reference.getVersion( ), 225 artifactFile.getFileName( ).toString( ) ); 226 } 227 try 228 { 229 Files.delete( artifactFile ); 230 log.debug( "File deleted: {}", artifactFile.toAbsolutePath( ) ); 231 } 232 catch ( IOException e ) 233 { 234 log.error( "Could not delete file {}: {}", artifactFile.toAbsolutePath( ), e.getMessage( ), e ); 235 continue; 236 } 237 try 238 { 239 repository.deleteArtifact( reference ); 240 } 241 catch ( ContentNotFoundException e ) 242 { 243 log.warn( "skip error deleting artifact {}: {}", reference, e.getMessage( ) ); 244 } 245 246 boolean snapshotVersion = VersionUtil.isSnapshot( reference.getVersion( ) ); 247 248 249 // If this is a snapshot we have to search for artifacts with the same version. And remove all of them. 250 if ( snapshotVersion ) 251 { 252 Collection<ArtifactMetadata> artifacts = 253 metaResolved.get( metaBaseId ); 254 if ( artifacts != null ) 255 { 256 // cleanup snapshots metadata 257 for ( ArtifactMetadata artifactMetadata : artifacts ) 258 { 259 // Artifact metadata and reference version should match. 260 if ( artifactMetadata.getVersion( ).equals( reference.getVersion( ) ) ) 261 { 262 ArtifactInfo info = new ArtifactInfo( artifactMetadata.getNamespace( ), artifactMetadata.getProject( ), artifactMetadata.getProjectVersion( ), artifactMetadata.getVersion( ) ); 263 if ( StringUtils.isNotBlank( reference.getClassifier( ) ) ) 264 { 265 info.setClassifier( reference.getClassifier( ) ); 266 } else { 267 info.setClassifier( "" ); 268 } 269 // metadataRepository.removeArtifact( artifactMetadata, baseVersion ); 270 metaRemovalList.put( info, artifactMetadata ); 271 } 272 } 273 } 274 } 275 else // otherwise we delete the artifact version 276 { 277 ArtifactInfo info = new ArtifactInfo( reference.getGroupId( ), reference.getArtifactId( ), baseVersion, reference.getVersion( ) ); 278 for ( ArtifactMetadata metadata : metaResolved.get( metaBaseId ) ) 279 { 280 metaRemovalList.put( info, metadata ); 281 } 282 } 283 triggerAuditEvent( repository.getRepository( ).getId( ), ArtifactReference.toKey( reference ), 284 AuditEvent.PURGE_ARTIFACT ); 285 purgeSupportFiles( artifactFile ); 286 } 287 purgeMetadata( metadataRepository, metaRemovalList ); 288 repositorySession.save( ); 289 290 } 291 } 292 293 /* 294 * Purges the metadata. First removes the artifacts. After that empty versions will be removed. 295 */ 296 private void purgeMetadata( MetadataRepository metadataRepository, Map<ArtifactInfo, ArtifactMetadata> dataList ) 297 { 298 Set<ArtifactInfo> projectLevelMetadata = new HashSet<>( ); 299 for ( Map.Entry<ArtifactInfo, ArtifactMetadata> infoEntry : dataList.entrySet( ) ) 300 { 301 ArtifactInfo info = infoEntry.getKey( ); 302 try 303 { 304 removeArtifact( metadataRepository, info, infoEntry.getValue( ) ); 305 log.debug( "Removed artifact from MetadataRepository {}", info ); 306 } 307 catch ( MetadataRepositoryException e ) 308 { 309 log.error( "Could not remove artifact from MetadataRepository {}: {}", info, e.getMessage( ), e ); 310 } 311 projectLevelMetadata.add( info.projectVersionLevel( ) ); 312 } 313 metadataRepository.save( ); 314 Collection<ArtifactMetadata> artifacts = null; 315 // Get remaining artifacts and remove project if empty 316 for ( ArtifactInfo info : projectLevelMetadata ) 317 { 318 try 319 { 320 artifacts = metadataRepository.getArtifacts( repository.getId( ), info.getNamespace( ), info.getName( ), 321 info.getProjectVersion( ) ); 322 if ( artifacts.size( ) == 0 ) 323 { 324 metadataRepository.removeProjectVersion( repository.getId( ), info.getNamespace( ), 325 info.getName( ), info.getProjectVersion( ) ); 326 log.debug( "Removed project version from MetadataRepository {}", info ); 327 } 328 } 329 catch ( MetadataResolutionException | MetadataRepositoryException e ) 330 { 331 log.error( "Could not remove project version from MetadataRepository {}: {}", info, e.getMessage( ), e ); 332 } 333 } 334 metadataRepository.save( ); 335 336 } 337 338 /* 339 * Removes the artifact from the metadataRepository. If a classifier is set, the facet will be removed. 340 */ 341 private void removeArtifact( MetadataRepository metadataRepository, ArtifactInfo artifactInfo, ArtifactMetadata artifactMetadata ) throws MetadataRepositoryException 342 { 343 if ( artifactInfo.hasClassifier( ) ) 344 { 345 // cleanup facet which contains classifier information 346 MavenArtifactFacet mavenArtifactFacet = 347 (MavenArtifactFacet) artifactMetadata.getFacet( 348 MavenArtifactFacet.FACET_ID ); 349 350 if ( mavenArtifactFacet!= null && StringUtils.equals( artifactInfo.classifier, 351 mavenArtifactFacet.getClassifier( ) ) ) 352 { 353 artifactMetadata.removeFacet( MavenArtifactFacet.FACET_ID ); 354 String groupId = artifactInfo.getNamespace( ), artifactId = 355 artifactInfo.getName( ), 356 version = artifactInfo.getProjectVersion( ); 357 MavenArtifactFacet mavenArtifactFacetToCompare = new MavenArtifactFacet( ); 358 mavenArtifactFacetToCompare.setClassifier( artifactInfo.getClassifier( ) ); 359 metadataRepository.removeArtifact( repository.getId( ), groupId, artifactId, 360 version, mavenArtifactFacetToCompare ); 361 metadataRepository.save( ); 362 } 363 } 364 else 365 { 366 metadataRepository.removeArtifact( artifactMetadata, artifactInfo.getProjectVersion( ) ); 367 } 368 } 369 370 private void deleteSilently( Path path ) 371 { 372 try 373 { 374 Files.deleteIfExists( path ); 375 triggerAuditEvent( repository.getRepository( ).getId( ), path.toString( ), AuditEvent.PURGE_FILE ); 376 } 377 catch ( IOException e ) 378 { 379 log.error( "Error occured during file deletion {}: {} ", path, e.getMessage( ), e ); 380 } 381 } 382 383 /** 384 * <p> 385 * This find support files for the artifactFile and deletes them. 386 * </p> 387 * <p> 388 * Support Files are things like ".sha1", ".md5", ".asc", etc. 389 * </p> 390 * 391 * @param artifactFile the file to base off of. 392 */ 393 private void purgeSupportFiles( Path artifactFile ) 394 { 395 Path parentDir = artifactFile.getParent( ); 396 397 if ( !Files.exists( parentDir ) ) 398 { 399 return; 400 } 401 402 final String artifactName = artifactFile.getFileName( ).toString( ); 403 404 try 405 { 406 deleteArtifactFiles( parentDir, 3, artifactName ); 407 } 408 catch ( IOException e ) 409 { 410 log.error( "Purge of support files failed {}: {}", artifactFile, e.getMessage( ), e ); 411 } 412 413 } 414 415 public static void deleteArtifactFiles(final Path directory, final int maxDepth, final String artifactName) throws IOException 416 { 417 Files.walkFileTree(directory, new HashSet<FileVisitOption>( ), maxDepth, new SimpleFileVisitor<Path>() { 418 @Override 419 public FileVisitResult visitFile( Path file, BasicFileAttributes attrs) throws IOException { 420 if (file.getFileName().toString().startsWith(artifactName)) { 421 try { 422 Files.delete( file ); 423 } catch (IOException e) { 424 // We do not stop, if an error occurs. 425 } 426 } 427 return FileVisitResult.CONTINUE; 428 } 429 430 @Override 431 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 432 return FileVisitResult.CONTINUE; 433 } 434 435 }); 436 } 437 438 private void triggerAuditEvent( String repoId, String resource, String action ) 439 { 440 String msg = 441 repoId + DELIM + "<system-purge>" + DELIM + "<system>" + DELIM + '\"' + resource + '\"' + DELIM + '\"' + 442 action + '\"'; 443 444 logger.info( msg ); 445 } 446}