mirror of https://github.com/apache/archiva.git
MRM-781 - Removal of Archiva-Webdav implementation in favor of Jackrabbit-webdav
* Adding LockManager to DavResourceFactory * Adding locking support to DavResource * General cleanup inside of the dav resource * Adding DavSession attachement inside of DavSessionProvider * Tests NOTE: We should have a complete Class 2 locking implementation (Exclusive only) so OS X dav client should work git-svn-id: https://svn.apache.org/repos/asf/archiva/trunk@661563 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
06b0d06fde
commit
721f552dfa
|
@ -56,23 +56,27 @@ public class ArchivaDavResource
|
|||
|
||||
private final String logicalResource;
|
||||
|
||||
private static final String METHODS =
|
||||
"OPTIONS, GET, HEAD, POST, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, PUT, DELETE, MOVE";
|
||||
|
||||
private static final String COMPLIANCE_CLASS = "1";
|
||||
|
||||
private DavPropertySet properties;
|
||||
|
||||
private boolean propsInitialized = false;
|
||||
|
||||
public ArchivaDavResource( String localResource, String logicalResource, MimeTypes mimeTypes,
|
||||
ArchivaDavResourceLocator locator, DavResourceFactory factory )
|
||||
private LockManager lockManager;
|
||||
|
||||
private final DavSession session;
|
||||
|
||||
public ArchivaDavResource( String localResource,
|
||||
String logicalResource,
|
||||
MimeTypes mimeTypes,
|
||||
DavSession session,
|
||||
ArchivaDavResourceLocator locator,
|
||||
DavResourceFactory factory )
|
||||
{
|
||||
this.mimeTypes = mimeTypes;
|
||||
this.localResource = new File( localResource );
|
||||
this.logicalResource = logicalResource;
|
||||
this.locator = locator;
|
||||
this.factory = factory;
|
||||
this.session = session;
|
||||
this.properties = new DavPropertySet();
|
||||
}
|
||||
|
||||
|
@ -218,7 +222,7 @@ public class ArchivaDavResource
|
|||
DavResourceLocator parentloc = locator.getFactory().createResourceLocator( locator.getPrefix(), parentPath );
|
||||
try
|
||||
{
|
||||
parent = factory.createResource( parentloc, null );
|
||||
parent = factory.createResource( parentloc, session );
|
||||
}
|
||||
catch ( DavException e )
|
||||
{
|
||||
|
@ -285,7 +289,7 @@ public class ArchivaDavResource
|
|||
String path = locator.getResourcePath() + '/' + item;
|
||||
DavResourceLocator resourceLocator =
|
||||
locator.getFactory().createResourceLocator( locator.getPrefix(), path );
|
||||
DavResource resource = factory.createResource( resourceLocator, null );
|
||||
DavResource resource = factory.createResource( resourceLocator, session );
|
||||
if ( resource != null )
|
||||
list.add( resource );
|
||||
}
|
||||
|
@ -302,20 +306,20 @@ public class ArchivaDavResource
|
|||
public void removeMember( DavResource member )
|
||||
throws DavException
|
||||
{
|
||||
File localResource = checkDavResourceIsArchivaDavResource( member ).getLocalResource();
|
||||
File resource = checkDavResourceIsArchivaDavResource( member ).getLocalResource();
|
||||
|
||||
if ( !localResource.exists() )
|
||||
if ( !resource.exists() )
|
||||
{
|
||||
throw new DavException( HttpServletResponse.SC_NOT_FOUND, member.getResourcePath() );
|
||||
}
|
||||
|
||||
boolean suceeded = false;
|
||||
|
||||
if ( localResource.isDirectory() )
|
||||
if ( resource.isDirectory() )
|
||||
{
|
||||
try
|
||||
{
|
||||
FileUtils.deleteDirectory( localResource );
|
||||
FileUtils.deleteDirectory( resource );
|
||||
suceeded = true;
|
||||
}
|
||||
catch ( IOException e )
|
||||
|
@ -324,9 +328,9 @@ public class ArchivaDavResource
|
|||
}
|
||||
}
|
||||
|
||||
if ( !suceeded && localResource.isFile() )
|
||||
if ( !suceeded && resource.isFile() )
|
||||
{
|
||||
suceeded = localResource.delete();
|
||||
suceeded = resource.delete();
|
||||
}
|
||||
|
||||
if ( !suceeded )
|
||||
|
@ -346,14 +350,14 @@ public class ArchivaDavResource
|
|||
|
||||
try
|
||||
{
|
||||
ArchivaDavResource localResource = checkDavResourceIsArchivaDavResource( destination );
|
||||
ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( destination );
|
||||
if ( isCollection() )
|
||||
{
|
||||
FileUtils.moveDirectory( getLocalResource(), localResource.getLocalResource() );
|
||||
FileUtils.moveDirectory( getLocalResource(), resource.getLocalResource() );
|
||||
}
|
||||
else
|
||||
{
|
||||
FileUtils.moveFile( getLocalResource(), localResource.getLocalResource() );
|
||||
FileUtils.moveFile( getLocalResource(), resource.getLocalResource() );
|
||||
}
|
||||
}
|
||||
catch ( IOException e )
|
||||
|
@ -377,14 +381,14 @@ public class ArchivaDavResource
|
|||
|
||||
try
|
||||
{
|
||||
ArchivaDavResource localResource = checkDavResourceIsArchivaDavResource( destination );
|
||||
ArchivaDavResource resource = checkDavResourceIsArchivaDavResource( destination );
|
||||
if ( isCollection() )
|
||||
{
|
||||
FileUtils.copyDirectory( getLocalResource(), localResource.getLocalResource() );
|
||||
FileUtils.copyDirectory( getLocalResource(), resource.getLocalResource() );
|
||||
}
|
||||
else
|
||||
{
|
||||
FileUtils.copyFile( getLocalResource(), localResource.getLocalResource() );
|
||||
FileUtils.copyFile( getLocalResource(), resource.getLocalResource() );
|
||||
}
|
||||
}
|
||||
catch ( IOException e )
|
||||
|
@ -395,43 +399,82 @@ public class ArchivaDavResource
|
|||
|
||||
public boolean isLockable( Type type, Scope scope )
|
||||
{
|
||||
return false;
|
||||
return Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope);
|
||||
}
|
||||
|
||||
public boolean hasLock( Type type, Scope scope )
|
||||
{
|
||||
return false;
|
||||
return getLock(type, scope) != null;
|
||||
}
|
||||
|
||||
public ActiveLock getLock( Type type, Scope scope )
|
||||
{
|
||||
return null;
|
||||
ActiveLock lock = null;
|
||||
if (exists() && Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope))
|
||||
{
|
||||
lock = lockManager.getLock(type, scope, this);
|
||||
}
|
||||
return lock;
|
||||
}
|
||||
|
||||
public ActiveLock[] getLocks()
|
||||
{
|
||||
return new ActiveLock[0];
|
||||
ActiveLock writeLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
|
||||
return (writeLock != null) ? new ActiveLock[]{writeLock} : new ActiveLock[0];
|
||||
}
|
||||
|
||||
public ActiveLock lock( LockInfo reqLockInfo )
|
||||
public ActiveLock lock( LockInfo lockInfo )
|
||||
throws DavException
|
||||
{
|
||||
return null;
|
||||
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;
|
||||
}
|
||||
|
||||
public ActiveLock refreshLock( LockInfo reqLockInfo, String lockToken )
|
||||
public ActiveLock refreshLock( LockInfo lockInfo, String lockToken )
|
||||
throws DavException
|
||||
{
|
||||
return null;
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public void addLockManager( LockManager lockmgr )
|
||||
public void addLockManager( LockManager lockManager )
|
||||
{
|
||||
this.lockManager = lockManager;
|
||||
}
|
||||
|
||||
public DavResourceFactory getFactory()
|
||||
|
@ -441,7 +484,7 @@ public class ArchivaDavResource
|
|||
|
||||
public DavSession getSession()
|
||||
{
|
||||
return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -68,6 +68,8 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.io.*;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:james@atlassian.com">James William Dumay</a>
|
||||
|
@ -123,6 +125,12 @@ public class ArchivaDavResourceFactory
|
|||
*/
|
||||
private HttpAuthenticator httpAuth;
|
||||
|
||||
|
||||
/**
|
||||
* Lock Manager - use simple implementation from JackRabbit
|
||||
*/
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
|
||||
public DavResource createResource( final DavResourceLocator locator, final DavServletRequest request,
|
||||
final DavServletResponse response )
|
||||
throws DavException
|
||||
|
@ -216,7 +224,7 @@ public class ArchivaDavResourceFactory
|
|||
{
|
||||
throw new BrowserRedirectException( resource.getHref() );
|
||||
}
|
||||
|
||||
resource.addLockManager(lockManager);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
@ -243,9 +251,10 @@ public class ArchivaDavResourceFactory
|
|||
String logicalResource = RepositoryPathUtil.getLogicalResource( locator.getResourcePath() );
|
||||
File resourceFile = new File( managedRepository.getRepoRoot(), logicalResource );
|
||||
resource =
|
||||
new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource, mimeTypes, archivaLocator,
|
||||
new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource, mimeTypes, davSession, archivaLocator,
|
||||
this );
|
||||
}
|
||||
resource.addLockManager(lockManager);
|
||||
return resource;
|
||||
}
|
||||
|
||||
|
@ -255,7 +264,7 @@ public class ArchivaDavResourceFactory
|
|||
{
|
||||
File resourceFile = new File( managedRepository.getRepoRoot(), logicalResource.getPath() );
|
||||
ArchivaDavResource resource =
|
||||
new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource.getPath(), mimeTypes, locator, this );
|
||||
new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource.getPath(), mimeTypes, request.getDavSession(), locator, this );
|
||||
|
||||
if ( !resource.isCollection() )
|
||||
{
|
||||
|
@ -289,7 +298,7 @@ public class ArchivaDavResourceFactory
|
|||
resourceFile, " (proxied)" );
|
||||
}
|
||||
resource =
|
||||
new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource.getPath(), mimeTypes, locator,
|
||||
new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource.getPath(), mimeTypes, request.getDavSession(), locator,
|
||||
this );
|
||||
|
||||
if ( !resourceFile.exists() )
|
||||
|
@ -326,7 +335,7 @@ public class ArchivaDavResourceFactory
|
|||
processAuditEvents( request, locator.getRepositoryId(), logicalResource.getPath(), previouslyExisted,
|
||||
resourceFile, null );
|
||||
|
||||
return new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource.getPath(), mimeTypes, locator,
|
||||
return new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource.getPath(), mimeTypes, request.getDavSession(), locator,
|
||||
this );
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,11 @@ public class ArchivaDavResourceLocator
|
|||
this.davLocatorFactory = davLocatorFactory;
|
||||
this.resourcePath = resourcePath;
|
||||
|
||||
if (!resourcePath.startsWith("/"))
|
||||
{
|
||||
this.resourcePath = "/" + resourcePath;
|
||||
}
|
||||
|
||||
String escapedPath = Text.escapePath( resourcePath );
|
||||
String hrefPrefix = prefix;
|
||||
|
||||
|
|
|
@ -65,6 +65,9 @@ public class ArchivaDavSessionProvider
|
|||
{
|
||||
AuthenticationResult result = httpAuth.getAuthenticationResult( request, null );
|
||||
|
||||
//Create a dav session
|
||||
request.setDavSession(new ArchivaDavSession());
|
||||
|
||||
return servletAuth.isAuthenticated( request, result );
|
||||
}
|
||||
catch ( AuthenticationException e )
|
||||
|
@ -81,9 +84,13 @@ public class ArchivaDavSessionProvider
|
|||
}
|
||||
}
|
||||
|
||||
public void releaseSession( WebdavRequest webdavRequest )
|
||||
public void releaseSession( WebdavRequest request )
|
||||
{
|
||||
|
||||
//Remove DavSession
|
||||
if (request.getDavSession() != null)
|
||||
{
|
||||
request.setDavSession(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String removeContextPath( final DavServletRequest request )
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
package org.apache.maven.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 java.io.File;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
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.SimpleLockManager;
|
||||
import org.apache.jackrabbit.webdav.lock.Type;
|
||||
import org.apache.maven.archiva.webdav.util.MimeTypes;
|
||||
import org.codehaus.plexus.spring.PlexusInSpringTestCase;
|
||||
import org.codehaus.plexus.spring.PlexusToSpringUtils;
|
||||
import quicktime.std.qtcomponents.SCInfo;
|
||||
|
||||
|
||||
public class DavResourceTest extends PlexusInSpringTestCase
|
||||
{
|
||||
private DavSession session;
|
||||
|
||||
private MimeTypes mimeTypes;
|
||||
|
||||
private ArchivaDavResourceLocator resourceLocator;
|
||||
|
||||
private ArchivaDavResourceFactory factory;
|
||||
|
||||
private File baseDir;
|
||||
|
||||
private final String REPOPATH = "/myresource.jar";
|
||||
|
||||
private final File myResource = new File(baseDir, REPOPATH);
|
||||
|
||||
private DavResource resource;
|
||||
|
||||
private LockManager lockManager;
|
||||
|
||||
@Override
|
||||
protected void setUp()
|
||||
throws Exception
|
||||
{
|
||||
super.setUp();
|
||||
session = new ArchivaDavSession();
|
||||
mimeTypes = (MimeTypes)getApplicationContext().getBean(PlexusToSpringUtils.buildSpringId(MimeTypes.class));
|
||||
baseDir = new File("target/DavResourceTest");
|
||||
baseDir.mkdirs();
|
||||
myResource.createNewFile();
|
||||
resourceLocator = (ArchivaDavResourceLocator)new ArchivaDavLocatorFactory().createResourceLocator("/", REPOPATH);
|
||||
resource = getDavResource(REPOPATH, myResource);
|
||||
lockManager = new SimpleLockManager();
|
||||
resource.addLockManager(lockManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown()
|
||||
throws Exception
|
||||
{
|
||||
super.tearDown();
|
||||
release(mimeTypes);
|
||||
FileUtils.deleteDirectory(baseDir);
|
||||
}
|
||||
|
||||
private DavResource getDavResource(String logicalPath, File file)
|
||||
{
|
||||
return new ArchivaDavResource(logicalPath, file.getAbsolutePath(), mimeTypes, session, resourceLocator, null);
|
||||
}
|
||||
|
||||
public void testIsLockable()
|
||||
{
|
||||
assertTrue(resource.isLockable(Type.WRITE, Scope.EXCLUSIVE));
|
||||
assertFalse(resource.isLockable(Type.WRITE, Scope.SHARED));
|
||||
}
|
||||
|
||||
public void testLock()
|
||||
throws Exception
|
||||
{
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
|
||||
LockInfo info = new LockInfo(Scope.EXCLUSIVE, Type.WRITE, "/", 0, false);
|
||||
lockManager.createLock(info, resource);
|
||||
|
||||
assertEquals(1, resource.getLocks().length);
|
||||
}
|
||||
|
||||
public void testLockIfResourceUnlockable()
|
||||
throws Exception
|
||||
{
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
|
||||
LockInfo info = new LockInfo(Scope.SHARED, Type.WRITE, "/", 0, false);
|
||||
try
|
||||
{
|
||||
lockManager.createLock(info, resource);
|
||||
fail("Did not throw dav exception");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//Simple lock manager will die
|
||||
}
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
}
|
||||
|
||||
public void testGetLock()
|
||||
throws Exception
|
||||
{
|
||||
LockInfo info = new LockInfo(Scope.EXCLUSIVE, Type.WRITE, "/", 0, false);
|
||||
lockManager.createLock(info, resource);
|
||||
|
||||
assertEquals(1, resource.getLocks().length);
|
||||
|
||||
//Lock should exist
|
||||
assertNotNull(resource.getLock(Type.WRITE, Scope.EXCLUSIVE));
|
||||
|
||||
//Lock should not exist
|
||||
assertNull(resource.getLock(Type.WRITE, Scope.SHARED));
|
||||
}
|
||||
|
||||
|
||||
public void testRefreshLockThrowsExceptionIfNoLockIsPresent()
|
||||
throws Exception
|
||||
{
|
||||
LockInfo info = new LockInfo(Scope.EXCLUSIVE, Type.WRITE, "/", 0, false);
|
||||
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
|
||||
try
|
||||
{
|
||||
lockManager.refreshLock(info, "notoken", resource);
|
||||
fail("Did not throw dav exception");
|
||||
}
|
||||
catch (DavException e)
|
||||
{
|
||||
assertEquals(DavServletResponse.SC_PRECONDITION_FAILED, e.getErrorCode());
|
||||
}
|
||||
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
}
|
||||
|
||||
public void testRefreshLock()
|
||||
throws Exception
|
||||
{
|
||||
LockInfo info = new LockInfo(Scope.EXCLUSIVE, Type.WRITE, "/", 0, false);
|
||||
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
|
||||
lockManager.createLock(info, resource);
|
||||
|
||||
assertEquals(1, resource.getLocks().length);
|
||||
|
||||
ActiveLock lock = resource.getLocks()[0];
|
||||
|
||||
lockManager.refreshLock(info, lock.getToken(), resource);
|
||||
|
||||
assertEquals(1, resource.getLocks().length);
|
||||
}
|
||||
|
||||
public void testUnlock()
|
||||
throws Exception
|
||||
{
|
||||
LockInfo info = new LockInfo(Scope.EXCLUSIVE, Type.WRITE, "/", 0, false);
|
||||
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
|
||||
lockManager.createLock(info, resource);
|
||||
|
||||
assertEquals(1, resource.getLocks().length);
|
||||
|
||||
ActiveLock lock = resource.getLocks()[0];
|
||||
|
||||
lockManager.releaseLock(lock.getToken(), resource);
|
||||
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
}
|
||||
|
||||
public void testUnlockThrowsDavExceptionIfNotLocked()
|
||||
throws Exception
|
||||
{
|
||||
LockInfo info = new LockInfo(Scope.EXCLUSIVE, Type.WRITE, "/", 0, false);
|
||||
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
|
||||
lockManager.createLock(info, resource);
|
||||
|
||||
assertEquals(1, resource.getLocks().length);
|
||||
|
||||
try
|
||||
{
|
||||
lockManager.releaseLock("BLAH", resource);
|
||||
fail("Did not throw DavException");
|
||||
}
|
||||
catch (DavException e)
|
||||
{
|
||||
assertEquals(DavServletResponse.SC_LOCKED, e.getErrorCode());
|
||||
}
|
||||
|
||||
assertEquals(1, resource.getLocks().length);
|
||||
}
|
||||
|
||||
public void testUnlockThrowsDavExceptionIfResourceNotLocked()
|
||||
throws Exception
|
||||
{
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
|
||||
try
|
||||
{
|
||||
lockManager.releaseLock("BLAH", resource);
|
||||
fail("Did not throw DavException");
|
||||
}
|
||||
catch (DavException e)
|
||||
{
|
||||
assertEquals(DavServletResponse.SC_PRECONDITION_FAILED, e.getErrorCode());
|
||||
}
|
||||
|
||||
assertEquals(0, resource.getLocks().length);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue