This project has retired. For details please refer to its Attic page.
Source code
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}