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    }