diff options
author | Martin Stockhammer <martin_s@apache.org> | 2019-05-13 22:23:01 +0200 |
---|---|---|
committer | Martin Stockhammer <martin_s@apache.org> | 2019-05-13 22:23:01 +0200 |
commit | f3aa14f3e2445e5404b9d254c9d3c3dd90221f6a (patch) | |
tree | 32f8015ad868ce6a372af859520bb8cdf3598f1e /archiva-modules/archiva-base | |
parent | 7adddbe141186225bc33e918727ceb3bed6646f3 (diff) | |
download | archiva-f3aa14f3e2445e5404b9d254c9d3c3dd90221f6a.tar.gz archiva-f3aa14f3e2445e5404b9d254c9d3c3dd90221f6a.zip |
Refactoring storage access and webdav
Diffstat (limited to 'archiva-modules/archiva-base')
6 files changed, 631 insertions, 3 deletions
diff --git a/archiva-modules/archiva-base/archiva-converter/src/main/java/org/apache/archiva/converter/legacy/LegacyConverterArtifactConsumer.java b/archiva-modules/archiva-base/archiva-converter/src/main/java/org/apache/archiva/converter/legacy/LegacyConverterArtifactConsumer.java index 13a79dd64..bddec98e5 100644 --- a/archiva-modules/archiva-base/archiva-converter/src/main/java/org/apache/archiva/converter/legacy/LegacyConverterArtifactConsumer.java +++ b/archiva-modules/archiva-base/archiva-converter/src/main/java/org/apache/archiva/converter/legacy/LegacyConverterArtifactConsumer.java @@ -19,6 +19,7 @@ package org.apache.archiva.converter.legacy; * under the License. */ +import org.apache.archiva.common.filelock.FileLockManager; import org.apache.archiva.common.plexusbridge.PlexusSisuBridge; import org.apache.archiva.common.plexusbridge.PlexusSisuBridgeException; import org.apache.archiva.configuration.FileTypes; @@ -71,6 +72,9 @@ public class LegacyConverterArtifactConsumer @Inject private FileTypes fileTypes; + @Inject + private FileLockManager fileLockManager; + private ArtifactFactory artifactFactory; private ManagedRepositoryContent managedRepository; @@ -93,11 +97,10 @@ public class LegacyConverterArtifactConsumer } @Override - public void beginScan( org.apache.archiva.repository.ManagedRepository repository, Date whenGathered ) + public void beginScan( ManagedRepository repository, Date whenGathered ) throws ConsumerException { - this.managedRepository = new ManagedDefaultRepositoryContent(artifactMappingProviders, fileTypes); - this.managedRepository.setRepository( repository ); + this.managedRepository = new ManagedDefaultRepositoryContent(repository, artifactMappingProviders, fileTypes, fileLockManager); } @Override diff --git a/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/ManagedRepositoryContent.java b/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/ManagedRepositoryContent.java index 816b57766..37ba9cf94 100644 --- a/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/ManagedRepositoryContent.java +++ b/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/ManagedRepositoryContent.java @@ -23,9 +23,17 @@ import org.apache.archiva.model.ArchivaArtifact; import org.apache.archiva.model.ArtifactReference; import org.apache.archiva.model.ProjectReference; import org.apache.archiva.model.VersionedReference; +import org.apache.archiva.repository.content.StorageAsset; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Path; +import java.time.Instant; +import java.util.List; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; /** * ManagedRepositoryContent interface for interacting with a managed repository in an abstract way, @@ -224,4 +232,57 @@ public interface ManagedRepositoryContent extends RepositoryContent * @return the relative path to the artifact. */ String toPath( ArchivaArtifact reference ); + + /** + * Returns information about a specific storage asset. + * @param path + * @return + */ + StorageAsset getAsset(String path); + + /** + * Consumes the data and sets a lock for the file during the operation. + * + * @param asset + * @param consumerFunction + * @param readLock + * @throws IOException + */ + void consumeData( StorageAsset asset, Consumer<InputStream> consumerFunction, boolean readLock ) throws IOException; + + /** + * Adds a new asset to the underlying storage. + * @param path The path to the asset. + * @param container True, if the asset should be a container, false, if it is a file. + * @return + */ + StorageAsset addAsset(String path, boolean container); + + /** + * Removes the given asset from the storage. + * + * @param asset + * @throws IOException + */ + void removeAsset(StorageAsset asset) throws IOException; + + /** + * Moves the asset to the given location and returns the asset object for the destination. + * + * @param origin + * @param destination + * @return + */ + StorageAsset moveAsset(StorageAsset origin, String destination) throws IOException; + + + /** + * Copies the given asset to the new destination. + * + * @param origin + * @param destination + * @return + * @throws IOException + */ + StorageAsset copyAsset(StorageAsset origin, String destination) throws IOException; } diff --git a/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/RelocatablePath.java b/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/RelocatablePath.java new file mode 100644 index 000000000..055abe48b --- /dev/null +++ b/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/RelocatablePath.java @@ -0,0 +1,41 @@ +package org.apache.archiva.repository; + +/** + * @author Martin Stockhammer <martin_s@apache.org> + */ +public class RelocatablePath +{ + + private final String path; + private final String originPath; + private final boolean relocated; + + RelocatablePath(String path, String originPath) { + this.path = path; + this.originPath = originPath; + this.relocated = !path.equals(originPath); + } + + RelocatablePath(String path) { + this.path = path; + this.originPath = path; + this.relocated = false; + } + + public String getPath( ) + { + return path; + } + + public String getOriginPath( ) + { + return originPath; + } + + public boolean isRelocated( ) + { + return relocated; + } + + +} diff --git a/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/RequestPathMapper.java b/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/RequestPathMapper.java new file mode 100644 index 000000000..d06615fba --- /dev/null +++ b/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/RequestPathMapper.java @@ -0,0 +1,35 @@ +package org.apache.archiva.repository; + +/** + * + * Maps request paths to native repository paths. Normally HTTP requests and the path in the repository + * storage should be identically. + * + * @author Martin Stockhammer <martin_s@apache.org> + */ +public interface RequestPathMapper +{ + /** + * Maps a request path to a repository path. The request path should be relative + * to the repository. The resulting path should always start with a '/'. + * The returned object contains additional information, if this request + * + * @param requestPath + * @return + */ + RelocatablePath relocatableRequestToRepository(String requestPath); + + + String requestToRepository(String requestPath); + + + /** + * Maps a repository path to a request path. The repository path is relative to the + * repository. The resulting path should always start with a '/'. + * + * @param repositoryPath + * @return + */ + String repositoryToRequest(String repositoryPath); + +} diff --git a/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/content/StorageAsset.java b/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/content/StorageAsset.java new file mode 100644 index 000000000..008f096a3 --- /dev/null +++ b/archiva-modules/archiva-base/archiva-repository-api/src/main/java/org/apache/archiva/repository/content/StorageAsset.java @@ -0,0 +1,131 @@ +package org.apache.archiva.repository.content; + +/* + * 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.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; +import java.util.function.Consumer; + +/** + * A instance of this interface represents information about an specific asset in a repository. + * The asset may be an real artifact, a directory, or a virtual asset. + * + * Each asset has a unique path relative to the repository. + * + * The implementation may read the data directly from the filesystem or underlying storage implementation. + * + * @author Martin Stockhammer <martin_s@apache.org> + */ +public interface StorageAsset +{ + /** + * Returns the complete path relative to the repository to the given asset. + * + * @return A path starting with '/' that uniquely identifies the asset in the repository. + */ + String getPath(); + + /** + * Returns the name of the asset. It may be just the filename. + * @return + */ + String getName(); + + /** + * Returns the time of the last modification. + * + * @return + */ + Instant getModificationTime(); + + /** + * Returns true, if this asset is a container type and contains further child assets. + * @return + */ + boolean isContainer(); + + /** + * List the child assets. + * + * @return The list of children. If there are no children, a empty list will be returned. + */ + List<StorageAsset> list(); + + /** + * The size in bytes of the asset. If the asset does not have a size, -1 should be returned. + * + * @return The size if the asset has a size, otherwise -1 + */ + long getSize(); + + /** + * Returns the input stream of the artifact content. + * It will throw a IOException, if the stream could not be created. + * Implementations should create a new stream instance for each invocation. + * + * @return The InputStream representing the content of the artifact. + * @throws IOException + */ + InputStream getData() throws IOException; + + /** + * + * Returns an output stream where you can write data to the asset. + * + * @param replace If true, the original data will be replaced, otherwise the data will be appended. + * @return The OutputStream where the data can be written. + * @throws IOException + */ + OutputStream writeData( boolean replace) throws IOException; + + /** + * Replaces the content. The implementation may do an atomic move operation, or keep a backup. If + * the operation fails, the implementation should try to restore the old data, if possible. + * + * The original file may be deleted, if the storage was successful. + * + * @param newData Replaces the data by the content of the given file. + */ + boolean storeDataFile( Path newData) throws IOException; + + /** + * Returns true, if the asset exists. + * + * @return True, if the asset exists, otherwise false. + */ + boolean exists(); + + /** + * Creates the asset in the underlying storage, if it does not exist. + */ + void create() throws IOException; + + /** + * Returns the real path to the asset, if it exist. Not all implementations may implement this method. + * + * @return The filesystem path to the asset. + * @throws UnsupportedOperationException + */ + Path getFilePath() throws UnsupportedOperationException; +} diff --git a/archiva-modules/archiva-base/archiva-repository-layer/src/main/java/org/apache/archiva/repository/content/FilesystemAsset.java b/archiva-modules/archiva-base/archiva-repository-layer/src/main/java/org/apache/archiva/repository/content/FilesystemAsset.java new file mode 100644 index 000000000..ef3aad306 --- /dev/null +++ b/archiva-modules/archiva-base/archiva-repository-layer/src/main/java/org/apache/archiva/repository/content/FilesystemAsset.java @@ -0,0 +1,357 @@ +package org.apache.archiva.repository.content; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.AclEntry; +import java.nio.file.attribute.AclEntryPermission; +import java.nio.file.attribute.AclEntryType; +import java.nio.file.attribute.AclFileAttributeView; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Martin Stockhammer <martin_s@apache.org> + */ +public class FilesystemAsset implements StorageAsset +{ + + private final static Logger log = LoggerFactory.getLogger( FilesystemAsset.class ); + + private final Path basePath; + private final Path assetPath; + private final Path completeAssetPath; + + public String DEFAULT_POSIX_FILE_PERMS = "rw-rw----"; + public String DEFAULT_POSIX_DIR_PERMS = "rwxrwx---"; + + List<AclEntry> defaultFileAcls; + Set<PosixFilePermission> defaultPosixFilePermissions; + List<AclEntry> defaultDirectoryAcls; + Set<PosixFilePermission> defaultPosixDirectoryPermissions; + + boolean supportsAcl = false; + boolean supportsPosix = false; + + boolean directory = false; + + public FilesystemAsset( Path basePath, String assetPath ) + { + this.basePath = basePath; + this.assetPath = Paths.get( assetPath ); + this.completeAssetPath = basePath.resolve( assetPath ).toAbsolutePath( ); + init( ); + } + + public FilesystemAsset( Path basePath, String assetPath, boolean directory ) + { + this.basePath = basePath; + this.assetPath = Paths.get( assetPath ); + this.completeAssetPath = basePath.resolve( assetPath ).toAbsolutePath( ); + this.directory = directory; + init( ); + } + + private void init( ) + { + defaultFileAcls = new ArrayList<>( ); + AclEntry.Builder aclBuilder = AclEntry.newBuilder( ); + aclBuilder.setPermissions( AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_ACL, + AclEntryPermission.WRITE_ATTRIBUTES, AclEntryPermission.WRITE_DATA, AclEntryPermission.APPEND_DATA ); + aclBuilder.setType( AclEntryType.ALLOW ); + defaultFileAcls.add( aclBuilder.build( ) ); + AclEntry.Builder aclDirBuilder = AclEntry.newBuilder( ); + aclDirBuilder.setPermissions( AclEntryPermission.ADD_FILE, AclEntryPermission.ADD_SUBDIRECTORY, AclEntryPermission.DELETE_CHILD, + AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_ACL, + AclEntryPermission.WRITE_ATTRIBUTES, AclEntryPermission.WRITE_DATA, AclEntryPermission.APPEND_DATA ); + aclDirBuilder.setType( AclEntryType.ALLOW ); + defaultDirectoryAcls.add( aclDirBuilder.build( ) ); + + defaultPosixFilePermissions = PosixFilePermissions.fromString( DEFAULT_POSIX_FILE_PERMS ); + defaultPosixDirectoryPermissions = PosixFilePermissions.fromString( DEFAULT_POSIX_DIR_PERMS ); + + try + { + supportsAcl = Files.getFileStore( completeAssetPath ).supportsFileAttributeView( AclFileAttributeView.class ); + } + catch ( IOException e ) + { + log.error( "Could not check filesystem capabilities {}", e.getMessage( ) ); + } + try + { + supportsPosix = Files.getFileStore( completeAssetPath ).supportsFileAttributeView( PosixFileAttributeView.class ); + } + catch ( IOException e ) + { + log.error( "Could not check filesystem capabilities {}", e.getMessage( ) ); + } + + } + + + @Override + public String getPath( ) + { + return assetPath.toString( ); + } + + @Override + public String getName( ) + { + return assetPath.getFileName( ).toString( ); + } + + @Override + public Instant getModificationTime( ) + { + try + { + return Files.getLastModifiedTime( completeAssetPath ).toInstant( ); + } + catch ( IOException e ) + { + log.error( "Could not read modification time of {}", completeAssetPath ); + return Instant.now( ); + } + } + + @Override + public boolean isContainer( ) + { + return Files.isDirectory( completeAssetPath ); + } + + @Override + public List<StorageAsset> list( ) + { + try + { + return Files.list( completeAssetPath ).map( p -> new FilesystemAsset( basePath, basePath.relativize( p ).toString( ) ) ) + .collect( Collectors.toList( ) ); + } + catch ( IOException e ) + { + return Collections.EMPTY_LIST; + } + } + + @Override + public long getSize( ) + { + try + { + return Files.size( completeAssetPath ); + } + catch ( IOException e ) + { + return -1; + } + } + + @Override + public InputStream getData( ) throws IOException + { + return Files.newInputStream( completeAssetPath ); + } + + @Override + public OutputStream writeData( boolean replace ) throws IOException + { + OpenOption[] options; + if ( replace ) + { + options = new OpenOption[]{StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE}; + } + else + { + options = new OpenOption[]{StandardOpenOption.APPEND}; + } + return Files.newOutputStream( completeAssetPath, options ); + } + + @Override + public boolean storeDataFile( Path newData ) throws IOException + { + final boolean createNew = !Files.exists( completeAssetPath ); + Path backup = null; + if ( !createNew ) + { + backup = findBackupFile( completeAssetPath ); + } + try + { + if ( !createNew ) + { + Files.move( completeAssetPath, backup ); + } + Files.move( newData, completeAssetPath, StandardCopyOption.REPLACE_EXISTING ); + setDefaultPermissions( completeAssetPath ); + return true; + } + catch ( IOException e ) + { + log.error( "Could not overwrite file {}", completeAssetPath ); + // Revert if possible + if ( backup != null && Files.exists( backup ) ) + { + Files.move( backup, completeAssetPath, StandardCopyOption.REPLACE_EXISTING ); + } + throw e; + } + finally + { + if ( backup != null ) + { + try + { + Files.deleteIfExists( backup ); + } + catch ( IOException e ) + { + log.error( "Could not delete backup file {}", backup ); + } + } + } + + } + + private void setDefaultPermissions(Path filePath) { + try + { + if ( supportsPosix ) + { + Set<PosixFilePermission> perms; + if ( Files.isDirectory( filePath ) ) + { + perms = defaultPosixFilePermissions; + } + else + { + perms = defaultPosixDirectoryPermissions; + } + Files.setPosixFilePermissions( filePath, perms ); + } + else if ( supportsAcl ) + { + List<AclEntry> perms; + if ( Files.isDirectory( filePath ) ) + { + perms = defaultDirectoryAcls; + } + else + { + perms = defaultFileAcls; + } + AclFileAttributeView aclAttr = Files.getFileAttributeView( filePath, AclFileAttributeView.class ); + aclAttr.setAcl( perms ); + } + } catch (IOException e) { + log.error("Could not set permissions for {}: {}", filePath, e.getMessage()); + } + } + + private Path findBackupFile( Path file ) + { + String ext = ".bak"; + Path backupPath = file.getParent( ).resolve( file.getFileName( ).toString( ) + ext ); + int idx = 0; + while ( Files.exists( backupPath ) ) + { + backupPath = file.getParent( ).resolve( file.getFileName( ).toString( ) + ext + idx++ ); + } + return backupPath; + } + + @Override + public boolean exists( ) + { + return Files.exists( completeAssetPath ); + } + + @Override + public Path getFilePath( ) throws UnsupportedOperationException + { + return completeAssetPath; + } + + + public void setDefaultFileAcls( List<AclEntry> acl ) + { + defaultFileAcls = acl; + } + + public List<AclEntry> getDefaultFileAcls( ) + { + return defaultFileAcls; + } + + public void setDefaultPosixFilePermissions( Set<PosixFilePermission> perms ) + { + defaultPosixFilePermissions = perms; + } + + public Set<PosixFilePermission> getDefaultPosixFilePermissions( ) + { + return defaultPosixFilePermissions; + } + + public void setDefaultDirectoryAcls( List<AclEntry> acl ) + { + defaultDirectoryAcls = acl; + } + + public List<AclEntry> getDefaultDirectoryAcls( ) + { + return defaultDirectoryAcls; + } + + public void setDefaultPosixDirectoryPermissions( Set<PosixFilePermission> perms ) + { + defaultPosixDirectoryPermissions = perms; + } + + public Set<PosixFilePermission> getDefaultPosixDirectoryPermissions( ) + { + return defaultPosixDirectoryPermissions; + } + + @Override + public void create( ) throws IOException + { + if ( !Files.exists( completeAssetPath ) ) + { + if ( directory ) + { + Files.createDirectories( completeAssetPath ); + } else { + Files.createFile( completeAssetPath ); + } + setDefaultPermissions( completeAssetPath ); + } + } + + @Override + public String toString( ) + { + return assetPath.toString(); + } + + +} |