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