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