1 package org.apache.archiva.webdav;
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
22 import edu.emory.mathcs.backport.java.util.Collections;
23 import org.apache.archiva.metadata.model.facets.AuditEvent;
24 import org.apache.archiva.repository.LayoutException;
25 import org.apache.archiva.repository.content.RepositoryStorage;
26 import org.apache.archiva.repository.content.StorageAsset;
27 import org.apache.archiva.repository.events.AuditListener;
28 import org.apache.archiva.scheduler.ArchivaTaskScheduler;
29 import org.apache.archiva.scheduler.repository.model.RepositoryArchivaTaskScheduler;
30 import org.apache.archiva.scheduler.repository.model.RepositoryTask;
31 import org.apache.archiva.webdav.util.IndexWriter;
32 import org.apache.archiva.webdav.util.MimeTypes;
33 import org.apache.commons.io.IOUtils;
34 import org.apache.jackrabbit.util.Text;
35 import org.apache.jackrabbit.webdav.DavException;
36 import org.apache.jackrabbit.webdav.DavResource;
37 import org.apache.jackrabbit.webdav.DavResourceFactory;
38 import org.apache.jackrabbit.webdav.DavResourceIterator;
39 import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
40 import org.apache.jackrabbit.webdav.DavResourceLocator;
41 import org.apache.jackrabbit.webdav.DavServletResponse;
42 import org.apache.jackrabbit.webdav.DavSession;
43 import org.apache.jackrabbit.webdav.MultiStatusResponse;
44 import org.apache.jackrabbit.webdav.io.InputContext;
45 import org.apache.jackrabbit.webdav.io.OutputContext;
46 import org.apache.jackrabbit.webdav.lock.ActiveLock;
47 import org.apache.jackrabbit.webdav.lock.LockInfo;
48 import org.apache.jackrabbit.webdav.lock.LockManager;
49 import org.apache.jackrabbit.webdav.lock.Scope;
50 import org.apache.jackrabbit.webdav.lock.Type;
51 import org.apache.jackrabbit.webdav.property.DavProperty;
52 import org.apache.jackrabbit.webdav.property.DavPropertyName;
53 import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
54 import org.apache.jackrabbit.webdav.property.DavPropertySet;
55 import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
56 import org.apache.jackrabbit.webdav.property.ResourceType;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
60 import javax.servlet.http.HttpServletResponse;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.io.OutputStream;
64 import java.nio.file.Files;
65 import java.nio.file.Path;
66 import java.nio.file.StandardOpenOption;
67 import java.time.format.DateTimeFormatter;
68 import java.util.List;
69 import java.util.Objects;
70 import java.util.stream.Collectors;
74 public class ArchivaDavResource
75 implements DavResource
77 public static final String HIDDEN_PATH_PREFIX = ".";
79 private final ArchivaDavResourceLocator locator;
81 private final DavResourceFactory factory;
83 // private final Path localResource;
85 private final String logicalResource;
87 private DavPropertySet properties = null;
89 private LockManager lockManager;
91 private final DavSession session;
93 private String remoteAddr;
95 private final RepositoryStorage repositoryStorage;
97 private final MimeTypes mimeTypes;
99 private List<AuditListener> auditListeners;
101 private String principal;
103 public static final String COMPLIANCE_CLASS = "1, 2";
105 private final ArchivaTaskScheduler<RepositoryTask> scheduler;
107 private Logger log = LoggerFactory.getLogger( ArchivaDavResource.class );
109 private StorageAsset asset;
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
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;
123 // TODO: push into locator as well as moving any references out of the resource factory
124 this.repositoryStorage = repositoryStorage;
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;
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
138 this( localResource, logicalResource, repositoryStorage, session, locator, factory, mimeTypes, auditListeners,
141 this.remoteAddr = remoteAddr;
142 this.principal = principal;
147 public String getComplianceClass()
149 return COMPLIANCE_CLASS;
153 public String getSupportedMethods()
159 public boolean exists()
161 return asset.exists();
165 public boolean isCollection()
167 return asset.isContainer();
171 public String getDisplayName()
173 String resPath = getResourcePath();
174 return ( resPath != null ) ? Text.getName( resPath ) : resPath;
178 public DavResourceLocator getLocator()
184 public String getResourcePath()
186 return locator.getResourcePath();
190 public String getHref()
192 return locator.getHref( isCollection() );
196 public long getModificationTime()
198 return asset.getModificationTime().toEpochMilli();
202 public void spool( OutputContext outputContext )
205 if ( !isCollection() )
207 outputContext.setContentLength( asset.getSize());
208 outputContext.setContentType( mimeTypes.getMimeType( asset.getName() ) );
211 if ( !isCollection() && outputContext.hasStream() )
213 repositoryStorage.consumeData( asset, is -> {copyStream(is, outputContext.getOutputStream());}, true );
215 else if ( outputContext.hasStream() )
217 IndexWriter writer = new IndexWriter( asset, logicalResource );
218 writer.write( outputContext );
222 private void copyStream(InputStream is, OutputStream os) throws RuntimeException {
225 IOUtils.copy(is, os);
227 catch ( IOException e )
229 throw new RuntimeException( "Copy failed "+e.getMessage(), e );
234 public DavPropertyName[] getPropertyNames()
236 return getProperties().getPropertyNames();
240 public DavProperty getProperty( DavPropertyName name )
242 return getProperties().get( name );
246 public DavPropertySet getProperties()
248 return initProperties();
252 public void setProperty( DavProperty property )
258 public void removeProperty( DavPropertyName propertyName )
263 public MultiStatusResponse alterProperties( DavPropertySet setProperties, DavPropertyNameSet removePropertyNames )
269 @SuppressWarnings("unchecked")
271 public MultiStatusResponse alterProperties( List changeList )
278 public DavResource getCollection()
280 DavResource parent = null;
281 if ( getResourcePath() != null && !getResourcePath().equals( "/" ) )
283 String parentPath = Text.getRelativeParent( getResourcePath(), 1 );
284 if ( parentPath.equals( "" ) )
288 DavResourceLocator parentloc =
289 locator.getFactory().createResourceLocator( locator.getPrefix(), parentPath );
292 parent = factory.createResource( parentloc, session );
294 catch ( DavException e )
303 public void addMember( DavResource resource, InputContext inputContext )
306 // Path localFile = localResource.resolve( resource.getDisplayName() );
307 boolean exists = asset.exists();
308 final String newPath = asset.getPath()+"/"+resource.getDisplayName();
310 if ( isCollection() && inputContext.hasStream() ) // New File
312 Path tempFile = null;
315 tempFile = Files.createTempFile( "archiva_upload","dat" );
316 try(OutputStream os = Files.newOutputStream( tempFile, StandardOpenOption.CREATE ))
318 IOUtils.copy( inputContext.getInputStream( ), os );
320 long expectedContentLength = inputContext.getContentLength();
321 long actualContentLength = 0;
324 actualContentLength = Files.size(tempFile);
326 catch ( IOException e )
328 log.error( "Could not get length of file {}: {}", tempFile, e.getMessage(), e );
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 )
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 );
337 StorageAsset member = repositoryStorage.addAsset( newPath, false );
339 member.replaceDataFromFile( tempFile );
341 catch ( IOException e )
343 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e );
349 Files.deleteIfExists( tempFile );
351 catch ( IOException e )
353 log.error("Could not delete temporary file {}", tempFile);
358 // queueRepositoryTask( asset );
360 log.debug( "File '{}{}(current user '{}')", resource.getDisplayName(),
361 ( exists ? "' modified " : "' created " ), this.principal );
363 // triggerAuditEvent( resource, exists ? AuditEvent.MODIFY_FILE : AuditEvent.CREATE_FILE );
365 else if ( !inputContext.hasStream() && isCollection() ) // New directory
369 StorageAsset member = repositoryStorage.addAsset( newPath, true );
372 catch ( IOException e )
374 log.error("Could not create directory {}: {}", newPath, e.getMessage(), e);
377 log.debug( "Directory '{}' (current user '{}')", resource.getDisplayName(), this.principal );
379 triggerAuditEvent( resource, AuditEvent.CREATE_DIR );
383 String msg = "Could not write member " + resource.getResourcePath() + " at " + getResourcePath()
384 + " as this is not a DAV collection";
386 throw new DavException( HttpServletResponse.SC_BAD_REQUEST, msg );
390 public StorageAsset getAsset() {
395 public DavResourceIterator getMembers()
397 List<DavResource> list;
398 if ( exists() && isCollection() )
400 list = asset.list().stream().filter( m -> !m.getName().startsWith( HIDDEN_PATH_PREFIX ) )
402 String path = locator.getResourcePath( ) + '/' + m.getName();
403 DavResourceLocator resourceLocator =
404 locator.getFactory( ).createResourceLocator( locator.getPrefix( ), path );
407 return factory.createResource( resourceLocator, session );
409 catch ( DavException e )
414 }).filter( Objects::nonNull ).collect( Collectors.toList());
416 list = Collections.emptyList( );
418 return new DavResourceIteratorImpl( list );
422 public void removeMember( DavResource member )
425 StorageAsset resource = checkDavResourceIsArchivaDavResource( member ).getAsset( );
427 if ( resource.exists() )
431 if ( resource.isContainer() )
433 repositoryStorage.removeAsset( resource );
434 triggerAuditEvent( member, AuditEvent.REMOVE_DIR );
438 repositoryStorage.removeAsset( resource );
439 triggerAuditEvent( member, AuditEvent.REMOVE_FILE );
442 log.debug( "{}{}' removed (current user '{}')", ( resource.isContainer() ? "Directory '" : "File '" ),
443 member.getDisplayName(), this.principal );
446 catch ( IOException e )
448 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR );
453 throw new DavException( HttpServletResponse.SC_NOT_FOUND );
457 private void triggerAuditEvent( DavResource member, String action )
460 String path = logicalResource + "/" + member.getDisplayName();
462 ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( member );
463 AuditEvent auditEvent = new AuditEvent( locator.getRepositoryId(), resource.principal, path, action );
464 auditEvent.setRemoteIP( resource.remoteAddr );
466 for ( AuditListener listener : auditListeners )
468 listener.auditEvent( auditEvent );
473 public void move( DavResource destination )
478 throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Resource to copy does not exist." );
483 ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( destination );
484 if ( isCollection() )
486 this.asset = repositoryStorage.moveAsset( asset, destination.getResourcePath() );
487 triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.MOVE_DIRECTORY );
491 this.asset = repositoryStorage.moveAsset( asset, destination.getResourcePath() );
492 triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.MOVE_FILE );
495 log.debug( "{}{}' moved to '{}' (current user '{}')", ( isCollection() ? "Directory '" : "File '" ),
496 asset.getPath(), destination, this.principal );
499 catch ( IOException e )
501 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e );
506 public void copy( DavResource destination, boolean shallow )
511 throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Resource to copy does not exist." );
514 if ( shallow && isCollection() )
516 throw new DavException( DavServletResponse.SC_FORBIDDEN, "Unable to perform shallow copy for collection" );
521 ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( destination );
522 if ( isCollection() )
524 repositoryStorage.copyAsset( asset, destination.getResourcePath() );
526 triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.COPY_DIRECTORY );
530 repositoryStorage.copyAsset( asset, destination.getResourcePath() );
532 triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.COPY_FILE );
535 log.debug( "{}{}' copied to '{}' (current user '{}')", ( isCollection() ? "Directory '" : "File '" ),
536 asset.getPath(), destination, this.principal );
539 catch ( IOException e )
541 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e );
546 public boolean isLockable( Type type, Scope scope )
548 return Type.WRITE.equals( type ) && Scope.EXCLUSIVE.equals( scope );
552 public boolean hasLock( Type type, Scope scope )
554 return getLock( type, scope ) != null;
558 public ActiveLock getLock( Type type, Scope scope )
560 ActiveLock lock = null;
561 if ( exists() && Type.WRITE.equals( type ) && Scope.EXCLUSIVE.equals( scope ) )
563 lock = lockManager.getLock( type, scope, this );
569 public ActiveLock[] getLocks()
571 ActiveLock writeLock = getLock( Type.WRITE, Scope.EXCLUSIVE );
572 return ( writeLock != null ) ? new ActiveLock[]{ writeLock } : new ActiveLock[0];
576 public ActiveLock lock( LockInfo lockInfo )
579 ActiveLock lock = null;
580 if ( isLockable( lockInfo.getType(), lockInfo.getScope() ) )
582 lock = lockManager.createLock( lockInfo, this );
586 throw new DavException( DavServletResponse.SC_PRECONDITION_FAILED, "Unsupported lock type or scope." );
592 public ActiveLock refreshLock( LockInfo lockInfo, String lockToken )
597 throw new DavException( DavServletResponse.SC_NOT_FOUND );
599 ActiveLock lock = getLock( lockInfo.getType(), lockInfo.getScope() );
602 throw new DavException( DavServletResponse.SC_PRECONDITION_FAILED,
603 "No lock with the given type/scope present on resource " + getResourcePath() );
606 lock = lockManager.refreshLock( lockInfo, lockToken, this );
612 public void unlock( String lockToken )
615 ActiveLock lock = getLock( Type.WRITE, Scope.EXCLUSIVE );
618 throw new DavException( HttpServletResponse.SC_PRECONDITION_FAILED );
620 else if ( lock.isLockedByToken( lockToken ) )
622 lockManager.releaseLock( lockToken, this );
626 throw new DavException( DavServletResponse.SC_LOCKED );
631 public void addLockManager( LockManager lockManager )
633 this.lockManager = lockManager;
637 public DavResourceFactory getFactory()
643 public DavSession getSession()
649 * Fill the set of properties
651 protected DavPropertySet initProperties()
655 properties = new DavPropertySet();
658 if ( properties != null )
663 DavPropertySet properties = new DavPropertySet();
665 // set (or reset) fundamental properties
666 if ( getDisplayName() != null )
668 properties.add( new DefaultDavProperty<>( DavPropertyName.DISPLAYNAME, getDisplayName() ) );
670 if ( isCollection() )
672 properties.add( new ResourceType( ResourceType.COLLECTION ) );
673 // Windows XP support
674 properties.add( new DefaultDavProperty<>( DavPropertyName.ISCOLLECTION, "1" ) );
678 properties.add( new ResourceType( ResourceType.DEFAULT_RESOURCE ) );
680 // Windows XP support
681 properties.add( new DefaultDavProperty<>( DavPropertyName.ISCOLLECTION, "0" ) );
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 ) );
689 properties.add( new DefaultDavProperty<>( DavPropertyName.GETCONTENTLENGTH, asset.getSize() ) );
691 this.properties = properties;
696 private ArchivaDavResource checkDavResourceIsArchivaDavResource( DavResource resource )
699 if ( !( resource instanceof ArchivaDavResource ) )
701 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
702 "DavResource is not instance of ArchivaDavResource" );
704 return (ArchivaDavResource) resource;
707 private void triggerAuditEvent( String remoteIP, String repositoryId, String resource, String action )
709 AuditEvent event = new AuditEvent( repositoryId, principal, resource, action );
710 event.setRemoteIP( remoteIP );
712 for ( AuditListener listener : auditListeners )
714 listener.auditEvent( event );
719 private void queueRepositoryTask( Path localFile )
721 RepositoryTask task = new RepositoryTask();
722 task.setRepositoryId( repository.getId() );
723 task.setResourceFile( localFile );
724 task.setUpdateRelatedArtifacts( false );
725 task.setScanAll( false );
729 scheduler.queueTask( task );
731 catch ( TaskQueueException e )
733 log.error( "Unable to queue repository task to execute consumers on resource file ['{}"
734 + "'].", localFile.getFileName() );