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