package org.apache.archiva.webdav; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import org.apache.archiva.admin.model.beans.ManagedRepository; import org.apache.archiva.metadata.model.facets.AuditEvent; import org.apache.archiva.repository.events.AuditListener; import org.apache.archiva.common.filelock.FileLockException; import org.apache.archiva.common.filelock.FileLockManager; import org.apache.archiva.common.filelock.FileLockTimeoutException; import org.apache.archiva.common.filelock.Lock; import org.apache.archiva.redback.components.taskqueue.TaskQueueException; import org.apache.archiva.scheduler.ArchivaTaskScheduler; import org.apache.archiva.scheduler.repository.model.RepositoryArchivaTaskScheduler; import org.apache.archiva.scheduler.repository.model.RepositoryTask; import org.apache.archiva.webdav.util.IndexWriter; import org.apache.archiva.webdav.util.MimeTypes; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.jackrabbit.util.Text; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceFactory; import org.apache.jackrabbit.webdav.DavResourceIterator; import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; import org.apache.jackrabbit.webdav.DavResourceLocator; import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.MultiStatusResponse; import org.apache.jackrabbit.webdav.io.InputContext; import org.apache.jackrabbit.webdav.io.OutputContext; import org.apache.jackrabbit.webdav.lock.ActiveLock; import org.apache.jackrabbit.webdav.lock.LockInfo; import org.apache.jackrabbit.webdav.lock.LockManager; import org.apache.jackrabbit.webdav.lock.Scope; import org.apache.jackrabbit.webdav.lock.Type; import org.apache.jackrabbit.webdav.property.DavProperty; import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; import org.apache.jackrabbit.webdav.property.DavPropertySet; import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import org.apache.jackrabbit.webdav.property.ResourceType; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; /** */ public class ArchivaDavResource implements DavResource { public static final String HIDDEN_PATH_PREFIX = "."; private final ArchivaDavResourceLocator locator; private final DavResourceFactory factory; private final File localResource; private final String logicalResource; private DavPropertySet properties = null; private LockManager lockManager; private final DavSession session; private String remoteAddr; private final ManagedRepository repository; private final MimeTypes mimeTypes; private List<AuditListener> auditListeners; private String principal; public static final String COMPLIANCE_CLASS = "1, 2"; private final ArchivaTaskScheduler scheduler; private final FileLockManager fileLockManager; private Logger log = LoggerFactory.getLogger( ArchivaDavResource.class ); public ArchivaDavResource( String localResource, String logicalResource, ManagedRepository repository, DavSession session, ArchivaDavResourceLocator locator, DavResourceFactory factory, MimeTypes mimeTypes, List<AuditListener> auditListeners, RepositoryArchivaTaskScheduler scheduler, FileLockManager fileLockManager ) { this.localResource = new File( localResource ); this.logicalResource = logicalResource; this.locator = locator; this.factory = factory; this.session = session; // TODO: push into locator as well as moving any references out of the resource factory this.repository = repository; // TODO: these should be pushed into the repository layer, along with the physical file operations in this class this.mimeTypes = mimeTypes; this.auditListeners = auditListeners; this.scheduler = scheduler; this.fileLockManager = fileLockManager; } public ArchivaDavResource( String localResource, String logicalResource, ManagedRepository repository, String remoteAddr, String principal, DavSession session, ArchivaDavResourceLocator locator, DavResourceFactory factory, MimeTypes mimeTypes, List<AuditListener> auditListeners, RepositoryArchivaTaskScheduler scheduler, FileLockManager fileLockManager ) { this( localResource, logicalResource, repository, session, locator, factory, mimeTypes, auditListeners, scheduler, fileLockManager ); this.remoteAddr = remoteAddr; this.principal = principal; } @Override public String getComplianceClass() { return COMPLIANCE_CLASS; } @Override public String getSupportedMethods() { return METHODS; } @Override public boolean exists() { return localResource.exists(); } @Override public boolean isCollection() { return localResource.isDirectory(); } @Override public String getDisplayName() { String resPath = getResourcePath(); return ( resPath != null ) ? Text.getName( resPath ) : resPath; } @Override public DavResourceLocator getLocator() { return locator; } public File getLocalResource() { return localResource; } @Override public String getResourcePath() { return locator.getResourcePath(); } @Override public String getHref() { return locator.getHref( isCollection() ); } @Override public long getModificationTime() { return localResource.lastModified(); } @Override public void spool( OutputContext outputContext ) throws IOException { if ( !isCollection() ) { outputContext.setContentLength( localResource.length() ); outputContext.setContentType( mimeTypes.getMimeType( localResource.getName() ) ); } try { if ( !isCollection() && outputContext.hasStream() ) { Lock lock = fileLockManager.readFileLock( localResource ); try (InputStream is = Files.newInputStream( lock.getFile().toPath() )) { IOUtils.copy( is, outputContext.getOutputStream() ); } } else if ( outputContext.hasStream() ) { IndexWriter writer = new IndexWriter( this, localResource, logicalResource ); writer.write( outputContext ); } } catch ( FileLockException e ) { throw new IOException( e.getMessage(), e ); } catch ( FileLockTimeoutException e ) { throw new IOException( e.getMessage(), e ); } } @Override public DavPropertyName[] getPropertyNames() { return getProperties().getPropertyNames(); } @Override public DavProperty getProperty( DavPropertyName name ) { return getProperties().get( name ); } @Override public DavPropertySet getProperties() { return initProperties(); } @Override public void setProperty( DavProperty property ) throws DavException { } @Override public void removeProperty( DavPropertyName propertyName ) throws DavException { } public MultiStatusResponse alterProperties( DavPropertySet setProperties, DavPropertyNameSet removePropertyNames ) throws DavException { return null; } @SuppressWarnings("unchecked") @Override public MultiStatusResponse alterProperties( List changeList ) throws DavException { return null; } @Override public DavResource getCollection() { DavResource parent = null; if ( getResourcePath() != null && !getResourcePath().equals( "/" ) ) { String parentPath = Text.getRelativeParent( getResourcePath(), 1 ); if ( parentPath.equals( "" ) ) { parentPath = "/"; } DavResourceLocator parentloc = locator.getFactory().createResourceLocator( locator.getPrefix(), parentPath ); try { parent = factory.createResource( parentloc, session ); } catch ( DavException e ) { // should not occur } } return parent; } @Override public void addMember( DavResource resource, InputContext inputContext ) throws DavException { File localFile = new File( localResource, resource.getDisplayName() ); boolean exists = localFile.exists(); if ( isCollection() && inputContext.hasStream() ) // New File { try (OutputStream stream = Files.newOutputStream( localFile.toPath() )) { IOUtils.copy( inputContext.getInputStream(), stream ); } catch ( IOException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); } // TODO: a bad deployment shouldn't delete an existing file - do we need to write to a temporary location first? long expectedContentLength = inputContext.getContentLength(); long actualContentLength = localFile.length(); // length of -1 is given for a chunked request or unknown length, in which case we accept what was uploaded if ( expectedContentLength >= 0 && expectedContentLength != actualContentLength ) { String msg = "Content Header length was " + expectedContentLength + " but was " + actualContentLength; log.debug( "Upload failed: {}", msg ); FileUtils.deleteQuietly( localFile ); throw new DavException( HttpServletResponse.SC_BAD_REQUEST, msg ); } queueRepositoryTask( localFile ); log.debug( "File '{}{}(current user '{}')", resource.getDisplayName(), ( exists ? "' modified " : "' created " ), this.principal ); triggerAuditEvent( resource, exists ? AuditEvent.MODIFY_FILE : AuditEvent.CREATE_FILE ); } else if ( !inputContext.hasStream() && isCollection() ) // New directory { localFile.mkdir(); log.debug( "Directory '{}' (current user '{}')", resource.getDisplayName(), this.principal ); triggerAuditEvent( resource, AuditEvent.CREATE_DIR ); } else { String msg = "Could not write member " + resource.getResourcePath() + " at " + getResourcePath() + " as this is not a DAV collection"; log.debug( msg ); throw new DavException( HttpServletResponse.SC_BAD_REQUEST, msg ); } } @Override public DavResourceIterator getMembers() { List<DavResource> list = new ArrayList<>(); if ( exists() && isCollection() ) { for ( String item : localResource.list() ) { try { if ( !item.startsWith( HIDDEN_PATH_PREFIX ) ) { String path = locator.getResourcePath() + '/' + item; DavResourceLocator resourceLocator = locator.getFactory().createResourceLocator( locator.getPrefix(), path ); DavResource resource = factory.createResource( resourceLocator, session ); if ( resource != null ) { list.add( resource ); } log.debug( "Resource '{}' retrieved by '{}'", item, this.principal ); } } catch ( DavException e ) { // Should not occur } } } return new DavResourceIteratorImpl( list ); } @Override public void removeMember( DavResource member ) throws DavException { File resource = checkDavResourceIsArchivaDavResource( member ).getLocalResource(); if ( resource.exists() ) { try { if ( resource.isDirectory() ) { if ( !FileUtils.deleteQuietly( resource ) ) { throw new IOException( "Could not remove directory" ); } triggerAuditEvent( member, AuditEvent.REMOVE_DIR ); } else { if ( !resource.delete() ) { throw new IOException( "Could not remove file" ); } triggerAuditEvent( member, AuditEvent.REMOVE_FILE ); } log.debug( "{}{}' removed (current user '{}')", ( resource.isDirectory() ? "Directory '" : "File '" ), member.getDisplayName(), this.principal ); } catch ( IOException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR ); } } else { throw new DavException( HttpServletResponse.SC_NOT_FOUND ); } } private void triggerAuditEvent( DavResource member, String action ) throws DavException { String path = logicalResource + "/" + member.getDisplayName(); ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( member ); AuditEvent auditEvent = new AuditEvent( locator.getRepositoryId(), resource.principal, path, action ); auditEvent.setRemoteIP( resource.remoteAddr ); for ( AuditListener listener : auditListeners ) { listener.auditEvent( auditEvent ); } } @Override public void move( DavResource destination ) throws DavException { if ( !exists() ) { throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Resource to copy does not exist." ); } try { ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( destination ); if ( isCollection() ) { FileUtils.moveDirectory( getLocalResource(), resource.getLocalResource() ); triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.MOVE_DIRECTORY ); } else { FileUtils.moveFile( getLocalResource(), resource.getLocalResource() ); triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.MOVE_FILE ); } log.debug( "{}{}' moved to '{}' (current user '{}')", ( isCollection() ? "Directory '" : "File '" ), getLocalResource().getName(), destination, this.principal ); } catch ( IOException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); } } @Override public void copy( DavResource destination, boolean shallow ) throws DavException { if ( !exists() ) { throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Resource to copy does not exist." ); } if ( shallow && isCollection() ) { throw new DavException( DavServletResponse.SC_FORBIDDEN, "Unable to perform shallow copy for collection" ); } try { ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( destination ); if ( isCollection() ) { FileUtils.copyDirectory( getLocalResource(), resource.getLocalResource() ); triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.COPY_DIRECTORY ); } else { FileUtils.copyFile( getLocalResource(), resource.getLocalResource() ); triggerAuditEvent( remoteAddr, locator.getRepositoryId(), logicalResource, AuditEvent.COPY_FILE ); } log.debug( "{}{}' copied to '{}' (current user '{)')", ( isCollection() ? "Directory '" : "File '" ), getLocalResource().getName(), destination, this.principal ); } catch ( IOException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); } } @Override public boolean isLockable( Type type, Scope scope ) { return Type.WRITE.equals( type ) && Scope.EXCLUSIVE.equals( scope ); } @Override public boolean hasLock( Type type, Scope scope ) { return getLock( type, scope ) != null; } @Override public ActiveLock getLock( Type type, Scope scope ) { ActiveLock lock = null; if ( exists() && Type.WRITE.equals( type ) && Scope.EXCLUSIVE.equals( scope ) ) { lock = lockManager.getLock( type, scope, this ); } return lock; } @Override public ActiveLock[] getLocks() { ActiveLock writeLock = getLock( Type.WRITE, Scope.EXCLUSIVE ); return ( writeLock != null ) ? new ActiveLock[]{ writeLock } : new ActiveLock[0]; } @Override public ActiveLock lock( LockInfo lockInfo ) throws DavException { ActiveLock lock = null; if ( isLockable( lockInfo.getType(), lockInfo.getScope() ) ) { lock = lockManager.createLock( lockInfo, this ); } else { throw new DavException( DavServletResponse.SC_PRECONDITION_FAILED, "Unsupported lock type or scope." ); } return lock; } @Override public ActiveLock refreshLock( LockInfo lockInfo, String lockToken ) throws DavException { if ( !exists() ) { throw new DavException( DavServletResponse.SC_NOT_FOUND ); } ActiveLock lock = getLock( lockInfo.getType(), lockInfo.getScope() ); if ( lock == null ) { throw new DavException( DavServletResponse.SC_PRECONDITION_FAILED, "No lock with the given type/scope present on resource " + getResourcePath() ); } lock = lockManager.refreshLock( lockInfo, lockToken, this ); return lock; } @Override public void unlock( String lockToken ) throws DavException { ActiveLock lock = getLock( Type.WRITE, Scope.EXCLUSIVE ); if ( lock == null ) { throw new DavException( HttpServletResponse.SC_PRECONDITION_FAILED ); } else if ( lock.isLockedByToken( lockToken ) ) { lockManager.releaseLock( lockToken, this ); } else { throw new DavException( DavServletResponse.SC_LOCKED ); } } @Override public void addLockManager( LockManager lockManager ) { this.lockManager = lockManager; } @Override public DavResourceFactory getFactory() { return factory; } @Override public DavSession getSession() { return session; } /** * Fill the set of properties */ protected DavPropertySet initProperties() { if ( !exists() ) { properties = new DavPropertySet(); } if ( properties != null ) { return properties; } DavPropertySet properties = new DavPropertySet(); // set (or reset) fundamental properties if ( getDisplayName() != null ) { properties.add( new DefaultDavProperty( DavPropertyName.DISPLAYNAME, getDisplayName() ) ); } if ( isCollection() ) { properties.add( new ResourceType( ResourceType.COLLECTION ) ); // Windows XP support properties.add( new DefaultDavProperty( DavPropertyName.ISCOLLECTION, "1" ) ); } else { properties.add( new ResourceType( ResourceType.DEFAULT_RESOURCE ) ); // Windows XP support properties.add( new DefaultDavProperty( DavPropertyName.ISCOLLECTION, "0" ) ); } // Need to get the ISO8601 date for properties DateTime dt = new DateTime( localResource.lastModified() ); DateTimeFormatter fmt = ISODateTimeFormat.dateTime(); String modifiedDate = fmt.print( dt ); properties.add( new DefaultDavProperty( DavPropertyName.GETLASTMODIFIED, modifiedDate ) ); properties.add( new DefaultDavProperty( DavPropertyName.CREATIONDATE, modifiedDate ) ); properties.add( new DefaultDavProperty( DavPropertyName.GETCONTENTLENGTH, localResource.length() ) ); this.properties = properties; return properties; } private ArchivaDavResource checkDavResourceIsArchivaDavResource( DavResource resource ) throws DavException { if ( !( resource instanceof ArchivaDavResource ) ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "DavResource is not instance of ArchivaDavResource" ); } return (ArchivaDavResource) resource; } private void triggerAuditEvent( String remoteIP, String repositoryId, String resource, String action ) { AuditEvent event = new AuditEvent( repositoryId, principal, resource, action ); event.setRemoteIP( remoteIP ); for ( AuditListener listener : auditListeners ) { listener.auditEvent( event ); } } private void queueRepositoryTask( File localFile ) { RepositoryTask task = new RepositoryTask(); task.setRepositoryId( repository.getId() ); task.setResourceFile( localFile ); task.setUpdateRelatedArtifacts( false ); task.setScanAll( false ); try { scheduler.queueTask( task ); } catch ( TaskQueueException e ) { log.error( "Unable to queue repository task to execute consumers on resource file ['" + localFile.getName() + "']." ); } } }