001package org.apache.archiva.webdav; 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.metadata.model.facets.AuditEvent; 023import org.apache.archiva.repository.LayoutException; 024import org.apache.archiva.repository.storage.RepositoryStorage; 025import org.apache.archiva.repository.storage.StorageAsset; 026import org.apache.archiva.metadata.audit.AuditListener; 027import org.apache.archiva.scheduler.ArchivaTaskScheduler; 028import org.apache.archiva.scheduler.repository.model.RepositoryArchivaTaskScheduler; 029import org.apache.archiva.scheduler.repository.model.RepositoryTask; 030import org.apache.archiva.webdav.util.IndexWriter; 031import org.apache.archiva.webdav.util.MimeTypes; 032import org.apache.commons.io.IOUtils; 033import org.apache.jackrabbit.util.Text; 034import org.apache.jackrabbit.webdav.DavException; 035import org.apache.jackrabbit.webdav.DavResource; 036import org.apache.jackrabbit.webdav.DavResourceFactory; 037import org.apache.jackrabbit.webdav.DavResourceIterator; 038import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; 039import org.apache.jackrabbit.webdav.DavResourceLocator; 040import org.apache.jackrabbit.webdav.DavServletResponse; 041import org.apache.jackrabbit.webdav.DavSession; 042import org.apache.jackrabbit.webdav.MultiStatusResponse; 043import org.apache.jackrabbit.webdav.io.InputContext; 044import org.apache.jackrabbit.webdav.io.OutputContext; 045import org.apache.jackrabbit.webdav.lock.ActiveLock; 046import org.apache.jackrabbit.webdav.lock.LockInfo; 047import org.apache.jackrabbit.webdav.lock.LockManager; 048import org.apache.jackrabbit.webdav.lock.Scope; 049import org.apache.jackrabbit.webdav.lock.Type; 050import org.apache.jackrabbit.webdav.property.DavProperty; 051import org.apache.jackrabbit.webdav.property.DavPropertyName; 052import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; 053import org.apache.jackrabbit.webdav.property.DavPropertySet; 054import org.apache.jackrabbit.webdav.property.DefaultDavProperty; 055import org.apache.jackrabbit.webdav.property.ResourceType; 056import org.slf4j.Logger; 057import org.slf4j.LoggerFactory; 058 059import javax.servlet.http.HttpServletResponse; 060import java.io.IOException; 061import java.io.InputStream; 062import java.io.OutputStream; 063import java.nio.file.Files; 064import java.nio.file.Path; 065import java.nio.file.StandardOpenOption; 066import java.time.format.DateTimeFormatter; 067import java.util.Collections; 068import java.util.List; 069import java.util.Objects; 070import java.util.stream.Collectors; 071 072/** 073 */ 074public class ArchivaDavResource 075 implements DavResource 076{ 077 public static final String HIDDEN_PATH_PREFIX = "."; 078 079 private final ArchivaDavResourceLocator locator; 080 081 private final DavResourceFactory factory; 082 083 // private final Path localResource; 084 085 private final String logicalResource; 086 087 private DavPropertySet properties = null; 088 089 private LockManager lockManager; 090 091 private final DavSession session; 092 093 private String remoteAddr; 094 095 private final RepositoryStorage repositoryStorage; 096 097 private final MimeTypes mimeTypes; 098 099 private List<AuditListener> auditListeners; 100 101 private String principal; 102 103 public static final String COMPLIANCE_CLASS = "1, 2"; 104 105 private final ArchivaTaskScheduler<RepositoryTask> scheduler; 106 107 private Logger log = LoggerFactory.getLogger( ArchivaDavResource.class ); 108 109 private StorageAsset asset; 110 111 public ArchivaDavResource( StorageAsset localResource, String logicalResource, RepositoryStorage repositoryStorage, 112 DavSession session, ArchivaDavResourceLocator locator, DavResourceFactory factory, 113 MimeTypes mimeTypes, List<AuditListener> auditListeners, 114 RepositoryArchivaTaskScheduler scheduler) throws LayoutException 115 { 116 // this.localResource = Paths.get( localResource ); 117 this.asset = localResource; 118 this.logicalResource = logicalResource; 119 this.locator = locator; 120 this.factory = factory; 121 this.session = session; 122 123 // TODO: push into locator as well as moving any references out of the resource factory 124 this.repositoryStorage = repositoryStorage; 125 126 // TODO: these should be pushed into the repository layer, along with the physical file operations in this class 127 this.mimeTypes = mimeTypes; 128 this.auditListeners = auditListeners; 129 this.scheduler = scheduler; 130 131 } 132 133 public ArchivaDavResource( StorageAsset localResource, String logicalResource, RepositoryStorage repositoryStorage, 134 String remoteAddr, String principal, DavSession session, 135 ArchivaDavResourceLocator locator, DavResourceFactory factory, MimeTypes mimeTypes, 136 List<AuditListener> auditListeners, RepositoryArchivaTaskScheduler scheduler) throws LayoutException 137 { 138 this( localResource, logicalResource, repositoryStorage, session, locator, factory, mimeTypes, auditListeners, 139 scheduler ); 140 141 this.remoteAddr = remoteAddr; 142 this.principal = principal; 143 } 144 145 146 @Override 147 public String getComplianceClass() 148 { 149 return COMPLIANCE_CLASS; 150 } 151 152 @Override 153 public String getSupportedMethods() 154 { 155 return METHODS; 156 } 157 158 @Override 159 public boolean exists() 160 { 161 return asset.exists(); 162 } 163 164 @Override 165 public boolean isCollection() 166 { 167 return asset.isContainer(); 168 } 169 170 @Override 171 public String getDisplayName() 172 { 173 String resPath = getResourcePath(); 174 return ( resPath != null ) ? Text.getName( resPath ) : resPath; 175 } 176 177 @Override 178 public DavResourceLocator getLocator() 179 { 180 return locator; 181 } 182 183 @Override 184 public String getResourcePath() 185 { 186 return locator.getResourcePath(); 187 } 188 189 @Override 190 public String getHref() 191 { 192 return locator.getHref( isCollection() ); 193 } 194 195 @Override 196 public long getModificationTime() 197 { 198 return asset.getModificationTime().toEpochMilli(); 199 } 200 201 @Override 202 public void spool( OutputContext outputContext ) 203 throws IOException 204 { 205 if ( !isCollection() ) 206 { 207 outputContext.setContentLength( asset.getSize()); 208 outputContext.setContentType( mimeTypes.getMimeType( asset.getName() ) ); 209 } 210 211 if ( !isCollection() && outputContext.hasStream() ) 212 { 213 repositoryStorage.consumeData( asset, is -> {copyStream(is, outputContext.getOutputStream());}, true ); 214 } 215 else if ( outputContext.hasStream() ) 216 { 217 IndexWriter writer = new IndexWriter( asset, logicalResource ); 218 writer.write( outputContext ); 219 } 220 } 221 222 private void copyStream(InputStream is, OutputStream os) throws RuntimeException { 223 try 224 { 225 IOUtils.copy(is, os); 226 } 227 catch ( IOException e ) 228 { 229 throw new RuntimeException( "Copy failed "+e.getMessage(), e ); 230 } 231 } 232 233 @Override 234 public DavPropertyName[] getPropertyNames() 235 { 236 return getProperties().getPropertyNames(); 237 } 238 239 @Override 240 public DavProperty getProperty( DavPropertyName name ) 241 { 242 return getProperties().get( name ); 243 } 244 245 @Override 246 public DavPropertySet getProperties() 247 { 248 return initProperties(); 249 } 250 251 @Override 252 public void setProperty( DavProperty property ) 253 throws DavException 254 { 255 } 256 257 @Override 258 public void removeProperty( DavPropertyName propertyName ) 259 throws DavException 260 { 261 } 262 263 public MultiStatusResponse alterProperties( DavPropertySet setProperties, DavPropertyNameSet removePropertyNames ) 264 throws DavException 265 { 266 return null; 267 } 268 269 @SuppressWarnings("unchecked") 270 @Override 271 public MultiStatusResponse alterProperties( List changeList ) 272 throws DavException 273 { 274 return null; 275 } 276 277 @Override 278 public DavResource getCollection() 279 { 280 DavResource parent = null; 281 if ( getResourcePath() != null && !getResourcePath().equals( "/" ) ) 282 { 283 String parentPath = Text.getRelativeParent( getResourcePath(), 1 ); 284 if ( parentPath.equals( "" ) ) 285 { 286 parentPath = "/"; 287 } 288 DavResourceLocator parentloc = 289 locator.getFactory().createResourceLocator( locator.getPrefix(), parentPath ); 290 try 291 { 292 parent = factory.createResource( parentloc, session ); 293 } 294 catch ( DavException e ) 295 { 296 // should not occur 297 } 298 } 299 return parent; 300 } 301 302 @Override 303 public void addMember( DavResource resource, InputContext inputContext ) 304 throws DavException 305 { 306 // Path localFile = localResource.resolve( resource.getDisplayName() ); 307 boolean exists = asset.exists(); 308 final String newPath = asset.getPath()+"/"+resource.getDisplayName(); 309 310 if ( isCollection() && inputContext.hasStream() ) // New File 311 { 312 Path tempFile = null; 313 try 314 { 315 tempFile = Files.createTempFile( "archiva_upload","dat" ); 316 try(OutputStream os = Files.newOutputStream( tempFile, StandardOpenOption.CREATE )) 317 { 318 IOUtils.copy( inputContext.getInputStream( ), os ); 319 } 320 long expectedContentLength = inputContext.getContentLength(); 321 long actualContentLength = 0; 322 try 323 { 324 actualContentLength = Files.size(tempFile); 325 } 326 catch ( IOException e ) 327 { 328 log.error( "Could not get length of file {}: {}", tempFile, e.getMessage(), e ); 329 } 330 // length of -1 is given for a chunked request or unknown length, in which case we accept what was uploaded 331 if ( expectedContentLength >= 0 && expectedContentLength != actualContentLength ) 332 { 333 String msg = "Content Header length was " + expectedContentLength + " but was " + actualContentLength; 334 log.debug( "Upload failed: {}", msg ); 335 throw new DavException( HttpServletResponse.SC_BAD_REQUEST, msg ); 336 } 337 StorageAsset member = repositoryStorage.addAsset( newPath, false ); 338 member.create(); 339 member.replaceDataFromFile( tempFile ); 340 } 341 catch ( IOException e ) 342 { 343 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); 344 } finally { 345 if (tempFile!=null) 346 { 347 try 348 { 349 Files.deleteIfExists( tempFile ); 350 } 351 catch ( IOException e ) 352 { 353 log.error("Could not delete temporary file {}", tempFile); 354 } 355 } 356 } 357 358 // queueRepositoryTask( asset ); 359 360 log.debug( "File '{}{}(current user '{}')", resource.getDisplayName(), 361 ( exists ? "' modified " : "' created " ), this.principal ); 362 363 // triggerAuditEvent( resource, exists ? AuditEvent.MODIFY_FILE : AuditEvent.CREATE_FILE ); 364 } 365 else if ( !inputContext.hasStream() && isCollection() ) // New directory 366 { 367 try 368 { 369 StorageAsset member = repositoryStorage.addAsset( newPath, true ); 370 member.create(); 371 } 372 catch ( IOException e ) 373 { 374 log.error("Could not create directory {}: {}", newPath, e.getMessage(), e); 375 } 376 377 log.debug( "Directory '{}' (current user '{}')", resource.getDisplayName(), this.principal ); 378 379 triggerAuditEvent( resource, AuditEvent.CREATE_DIR ); 380 } 381 else 382 { 383 String msg = "Could not write member " + resource.getResourcePath() + " at " + getResourcePath() 384 + " as this is not a DAV collection"; 385 log.debug( msg ); 386 throw new DavException( HttpServletResponse.SC_BAD_REQUEST, msg ); 387 } 388 } 389 390 public StorageAsset getAsset() { 391 return asset; 392 } 393 394 @Override 395 public DavResourceIterator getMembers() 396 { 397 List<DavResource> list; 398 if ( exists() && isCollection() ) 399 { 400 list = asset.list().stream().filter( m -> !m.getName().startsWith( HIDDEN_PATH_PREFIX ) ) 401 .map(m -> { 402 String path = locator.getResourcePath( ) + '/' + m.getName(); 403 DavResourceLocator resourceLocator = 404 locator.getFactory( ).createResourceLocator( locator.getPrefix( ), path ); 405 try 406 { 407 return factory.createResource( resourceLocator, session ); 408 } 409 catch ( DavException e ) 410 { 411 return null; 412 } 413 414 }).filter( Objects::nonNull ).collect( Collectors.toList()); 415 } else { 416 list = Collections.emptyList( ); 417 } 418 return new DavResourceIteratorImpl( list ); 419 } 420 421 @Override 422 public void removeMember( DavResource member ) 423 throws DavException 424 { 425 StorageAsset resource = checkDavResourceIsArchivaDavResource( member ).getAsset( ); 426 427 if ( resource.exists() ) 428 { 429 try 430 { 431 if ( resource.isContainer() ) 432 { 433 repositoryStorage.removeAsset( resource ); 434 triggerAuditEvent( member, AuditEvent.REMOVE_DIR ); 435 } 436 else 437 { 438 repositoryStorage.removeAsset( resource ); 439 triggerAuditEvent( member, AuditEvent.REMOVE_FILE ); 440 } 441 442 log.debug( "{}{}' removed (current user '{}')", ( resource.isContainer() ? "Directory '" : "File '" ), 443 member.getDisplayName(), this.principal ); 444 445 } 446 catch ( IOException e ) 447 { 448 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR ); 449 } 450 } 451 else 452 { 453 throw new DavException( HttpServletResponse.SC_NOT_FOUND ); 454 } 455 } 456 457 private void triggerAuditEvent( DavResource member, String action ) 458 throws DavException 459 { 460 String path = logicalResource + "/" + member.getDisplayName(); 461 462 ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( member ); 463 AuditEvent auditEvent = new AuditEvent( locator.getRepositoryId(), resource.principal, path, action ); 464 auditEvent.setRemoteIP( resource.remoteAddr ); 465 466 for ( AuditListener listener : auditListeners ) 467 { 468 listener.auditEvent( auditEvent ); 469 } 470 } 471 472 @Override 473 public void move( DavResource destination ) 474 throws DavException 475 { 476 if ( !exists() ) 477 { 478 throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Resource to copy does not exist." ); 479 } 480 481 try 482 { 483 ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( destination ); 484 if ( isCollection() ) 485 { 486 this.asset = repositoryStorage.moveAsset( asset, destination.getResourcePath() ); 487 triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.MOVE_DIRECTORY ); 488 } 489 else 490 { 491 this.asset = repositoryStorage.moveAsset( asset, destination.getResourcePath() ); 492 triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.MOVE_FILE ); 493 } 494 495 log.debug( "{}{}' moved to '{}' (current user '{}')", ( isCollection() ? "Directory '" : "File '" ), 496 asset.getPath(), destination, this.principal ); 497 498 } 499 catch ( IOException e ) 500 { 501 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); 502 } 503 } 504 505 @Override 506 public void copy( DavResource destination, boolean shallow ) 507 throws DavException 508 { 509 if ( !exists() ) 510 { 511 throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Resource to copy does not exist." ); 512 } 513 514 if ( shallow && isCollection() ) 515 { 516 throw new DavException( DavServletResponse.SC_FORBIDDEN, "Unable to perform shallow copy for collection" ); 517 } 518 519 try 520 { 521 ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( destination ); 522 if ( isCollection() ) 523 { 524 repositoryStorage.copyAsset( asset, destination.getResourcePath() ); 525 526 triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.COPY_DIRECTORY ); 527 } 528 else 529 { 530 repositoryStorage.copyAsset( asset, destination.getResourcePath() ); 531 532 triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.COPY_FILE ); 533 } 534 535 log.debug( "{}{}' copied to '{}' (current user '{}')", ( isCollection() ? "Directory '" : "File '" ), 536 asset.getPath(), destination, this.principal ); 537 538 } 539 catch ( IOException e ) 540 { 541 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); 542 } 543 } 544 545 @Override 546 public boolean isLockable( Type type, Scope scope ) 547 { 548 return Type.WRITE.equals( type ) && Scope.EXCLUSIVE.equals( scope ); 549 } 550 551 @Override 552 public boolean hasLock( Type type, Scope scope ) 553 { 554 return getLock( type, scope ) != null; 555 } 556 557 @Override 558 public ActiveLock getLock( Type type, Scope scope ) 559 { 560 ActiveLock lock = null; 561 if ( exists() && Type.WRITE.equals( type ) && Scope.EXCLUSIVE.equals( scope ) ) 562 { 563 lock = lockManager.getLock( type, scope, this ); 564 } 565 return lock; 566 } 567 568 @Override 569 public ActiveLock[] getLocks() 570 { 571 ActiveLock writeLock = getLock( Type.WRITE, Scope.EXCLUSIVE ); 572 return ( writeLock != null ) ? new ActiveLock[]{ writeLock } : new ActiveLock[0]; 573 } 574 575 @Override 576 public ActiveLock lock( LockInfo lockInfo ) 577 throws DavException 578 { 579 ActiveLock lock = null; 580 if ( isLockable( lockInfo.getType(), lockInfo.getScope() ) ) 581 { 582 lock = lockManager.createLock( lockInfo, this ); 583 } 584 else 585 { 586 throw new DavException( DavServletResponse.SC_PRECONDITION_FAILED, "Unsupported lock type or scope." ); 587 } 588 return lock; 589 } 590 591 @Override 592 public ActiveLock refreshLock( LockInfo lockInfo, String lockToken ) 593 throws DavException 594 { 595 if ( !exists() ) 596 { 597 throw new DavException( DavServletResponse.SC_NOT_FOUND ); 598 } 599 ActiveLock lock = getLock( lockInfo.getType(), lockInfo.getScope() ); 600 if ( lock == null ) 601 { 602 throw new DavException( DavServletResponse.SC_PRECONDITION_FAILED, 603 "No lock with the given type/scope present on resource " + getResourcePath() ); 604 } 605 606 lock = lockManager.refreshLock( lockInfo, lockToken, this ); 607 608 return lock; 609 } 610 611 @Override 612 public void unlock( String lockToken ) 613 throws DavException 614 { 615 ActiveLock lock = getLock( Type.WRITE, Scope.EXCLUSIVE ); 616 if ( lock == null ) 617 { 618 throw new DavException( HttpServletResponse.SC_PRECONDITION_FAILED ); 619 } 620 else if ( lock.isLockedByToken( lockToken ) ) 621 { 622 lockManager.releaseLock( lockToken, this ); 623 } 624 else 625 { 626 throw new DavException( DavServletResponse.SC_LOCKED ); 627 } 628 } 629 630 @Override 631 public void addLockManager( LockManager lockManager ) 632 { 633 this.lockManager = lockManager; 634 } 635 636 @Override 637 public DavResourceFactory getFactory() 638 { 639 return factory; 640 } 641 642 @Override 643 public DavSession getSession() 644 { 645 return session; 646 } 647 648 /** 649 * Fill the set of properties 650 */ 651 protected DavPropertySet initProperties() 652 { 653 if ( !exists() ) 654 { 655 properties = new DavPropertySet(); 656 } 657 658 if ( properties != null ) 659 { 660 return properties; 661 } 662 663 DavPropertySet properties = new DavPropertySet(); 664 665 // set (or reset) fundamental properties 666 if ( getDisplayName() != null ) 667 { 668 properties.add( new DefaultDavProperty<>( DavPropertyName.DISPLAYNAME, getDisplayName() ) ); 669 } 670 if ( isCollection() ) 671 { 672 properties.add( new ResourceType( ResourceType.COLLECTION ) ); 673 // Windows XP support 674 properties.add( new DefaultDavProperty<>( DavPropertyName.ISCOLLECTION, "1" ) ); 675 } 676 else 677 { 678 properties.add( new ResourceType( ResourceType.DEFAULT_RESOURCE ) ); 679 680 // Windows XP support 681 properties.add( new DefaultDavProperty<>( DavPropertyName.ISCOLLECTION, "0" ) ); 682 } 683 684 // Need to get the ISO8601 date for properties 685 String modifiedDate = DateTimeFormatter.ISO_INSTANT.format( asset.getModificationTime() ); 686 properties.add( new DefaultDavProperty<>( DavPropertyName.GETLASTMODIFIED, modifiedDate ) ); 687 properties.add( new DefaultDavProperty<>( DavPropertyName.CREATIONDATE, modifiedDate ) ); 688 689 properties.add( new DefaultDavProperty<>( DavPropertyName.GETCONTENTLENGTH, asset.getSize() ) ); 690 691 this.properties = properties; 692 693 return properties; 694 } 695 696 private ArchivaDavResource checkDavResourceIsArchivaDavResource( DavResource resource ) 697 throws DavException 698 { 699 if ( !( resource instanceof ArchivaDavResource ) ) 700 { 701 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 702 "DavResource is not instance of ArchivaDavResource" ); 703 } 704 return (ArchivaDavResource) resource; 705 } 706 707 private void triggerAuditEvent( String remoteIP, String repositoryId, String resource, String action ) 708 { 709 AuditEvent event = new AuditEvent( repositoryId, principal, resource, action ); 710 event.setRemoteIP( remoteIP ); 711 712 for ( AuditListener listener : auditListeners ) 713 { 714 listener.auditEvent( event ); 715 } 716 } 717 718 /** 719 private void queueRepositoryTask( Path localFile ) 720 { 721 RepositoryTask task = new RepositoryTask(); 722 task.setRepositoryId( repository.getId() ); 723 task.setResourceFile( localFile ); 724 task.setUpdateRelatedArtifacts( false ); 725 task.setScanAll( false ); 726 727 try 728 { 729 scheduler.queueTask( task ); 730 } 731 catch ( TaskQueueException e ) 732 { 733 log.error( "Unable to queue repository task to execute consumers on resource file ['{}" 734 + "'].", localFile.getFileName() ); 735 } 736 } 737 **/ 738}