001package org.apache.archiva.repository.storage; 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.commons.lang3.StringUtils; 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.OutputStream; 029import java.nio.channels.FileChannel; 030import java.nio.channels.ReadableByteChannel; 031import java.nio.channels.WritableByteChannel; 032import java.nio.file.*; 033import java.nio.file.attribute.*; 034import java.time.Instant; 035import java.util.ArrayList; 036import java.util.Collections; 037import java.util.List; 038import java.util.Set; 039import java.util.stream.Collectors; 040 041/** 042 * Implementation of an asset that is stored on the filesystem. 043 * <p> 044 * The implementation does not check the given paths. Caller should normalize the asset path 045 * and check, if the base path is a parent of the resulting path. 046 * <p> 047 * The file must not exist for all operations. 048 * 049 * @author Martin Stockhammer <martin_s@apache.org> 050 */ 051public class FilesystemAsset implements StorageAsset, Comparable { 052 053 private final static Logger log = LoggerFactory.getLogger(FilesystemAsset.class); 054 055 private final Path basePath; 056 private final Path assetPath; 057 private final String relativePath; 058 059 public static final String DEFAULT_POSIX_FILE_PERMS = "rw-rw----"; 060 public static final String DEFAULT_POSIX_DIR_PERMS = "rwxrwx---"; 061 062 public static final Set<PosixFilePermission> DEFAULT_POSIX_FILE_PERMISSIONS; 063 public static final Set<PosixFilePermission> DEFAULT_POSIX_DIR_PERMISSIONS; 064 065 public static final AclEntryPermission[] DEFAULT_ACL_FILE_PERMISSIONS = new AclEntryPermission[]{ 066 AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_ACL, 067 AclEntryPermission.WRITE_ATTRIBUTES, AclEntryPermission.WRITE_DATA, AclEntryPermission.APPEND_DATA 068 }; 069 070 public static final AclEntryPermission[] DEFAULT_ACL_DIR_PERMISSIONS = new AclEntryPermission[]{ 071 AclEntryPermission.ADD_FILE, AclEntryPermission.ADD_SUBDIRECTORY, AclEntryPermission.DELETE_CHILD, 072 AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_ACL, 073 AclEntryPermission.WRITE_ATTRIBUTES, AclEntryPermission.WRITE_DATA, AclEntryPermission.APPEND_DATA 074 }; 075 076 static { 077 078 DEFAULT_POSIX_FILE_PERMISSIONS = PosixFilePermissions.fromString(DEFAULT_POSIX_FILE_PERMS); 079 DEFAULT_POSIX_DIR_PERMISSIONS = PosixFilePermissions.fromString(DEFAULT_POSIX_DIR_PERMS); 080 } 081 082 Set<PosixFilePermission> defaultPosixFilePermissions = DEFAULT_POSIX_FILE_PERMISSIONS; 083 Set<PosixFilePermission> defaultPosixDirectoryPermissions = DEFAULT_POSIX_DIR_PERMISSIONS; 084 085 List<AclEntry> defaultFileAcls; 086 List<AclEntry> defaultDirectoryAcls; 087 088 boolean supportsAcl = false; 089 boolean supportsPosix = false; 090 final boolean setPermissionsForNew; 091 final RepositoryStorage storage; 092 093 boolean directoryHint = false; 094 095 private static final OpenOption[] REPLACE_OPTIONS = new OpenOption[]{StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE}; 096 private static final OpenOption[] APPEND_OPTIONS = new OpenOption[]{StandardOpenOption.APPEND}; 097 098 099 FilesystemAsset(RepositoryStorage storage, String path, Path assetPath, Path basePath) { 100 this.assetPath = assetPath; 101 this.relativePath = normalizePath(path); 102 this.setPermissionsForNew=false; 103 this.basePath = basePath; 104 this.storage = storage; 105 init(); 106 } 107 108 /** 109 * Creates an asset for the given path. The given paths are not checked. 110 * The base path should be an absolute path. 111 * 112 * @param path The logical path for the asset relative to the repository. 113 * @param assetPath The asset path. 114 */ 115 public FilesystemAsset(RepositoryStorage storage, String path, Path assetPath) { 116 this.assetPath = assetPath; 117 this.relativePath = normalizePath(path); 118 this.setPermissionsForNew = false; 119 this.basePath = null; 120 this.storage = storage; 121 // The base directory is always a directory 122 if ("".equals(path) || "/".equals(path)) { 123 this.directoryHint = true; 124 } 125 init(); 126 } 127 128 /** 129 * Creates an asset for the given path. The given paths are not checked. 130 * The base path should be an absolute path. 131 * 132 * @param path The logical path for the asset relative to the repository 133 * @param assetPath The asset path. 134 * @param directory This is only relevant, if the represented file or directory does not exist yet and 135 * is a hint. 136 */ 137 public FilesystemAsset(RepositoryStorage storage, String path, Path assetPath, Path basePath, boolean directory) { 138 this.assetPath = assetPath; 139 this.relativePath = normalizePath(path); 140 this.directoryHint = directory; 141 this.setPermissionsForNew = false; 142 this.basePath = basePath; 143 this.storage = storage; 144 init(); 145 } 146 147 /** 148 * Creates an asset for the given path. The given paths are not checked. 149 * The base path should be an absolute path. 150 * 151 * @param path The logical path for the asset relative to the repository 152 * @param assetPath The asset path. 153 * @param directory This is only relevant, if the represented file or directory does not exist yet and 154 * is a hint. 155 */ 156 public FilesystemAsset(RepositoryStorage storage, String path, Path assetPath, Path basePath, boolean directory, boolean setPermissionsForNew) { 157 this.assetPath = assetPath; 158 this.relativePath = normalizePath(path); 159 this.directoryHint = directory; 160 this.setPermissionsForNew = setPermissionsForNew; 161 this.basePath = basePath; 162 this.storage = storage; 163 init(); 164 } 165 166 private String normalizePath(final String path) { 167 if (!path.startsWith("/")) { 168 return "/"+path; 169 } else { 170 String tmpPath = path; 171 while (tmpPath.startsWith("//")) { 172 tmpPath = tmpPath.substring(1); 173 } 174 return tmpPath; 175 } 176 } 177 178 private void init() { 179 180 if (setPermissionsForNew) { 181 try { 182 supportsAcl = Files.getFileStore(assetPath.getRoot()).supportsFileAttributeView(AclFileAttributeView.class); 183 } catch (IOException e) { 184 log.error("Could not check filesystem capabilities {}", e.getMessage()); 185 } 186 try { 187 supportsPosix = Files.getFileStore(assetPath.getRoot()).supportsFileAttributeView(PosixFileAttributeView.class); 188 } catch (IOException e) { 189 log.error("Could not check filesystem capabilities {}", e.getMessage()); 190 } 191 192 if (supportsAcl) { 193 AclFileAttributeView aclView = Files.getFileAttributeView(assetPath.getParent(), AclFileAttributeView.class); 194 UserPrincipal owner = null; 195 try { 196 owner = aclView.getOwner(); 197 setDefaultFileAcls(processPermissions(owner, DEFAULT_ACL_FILE_PERMISSIONS)); 198 setDefaultDirectoryAcls(processPermissions(owner, DEFAULT_ACL_DIR_PERMISSIONS)); 199 200 } catch (IOException e) { 201 supportsAcl = false; 202 } 203 204 205 } 206 } 207 } 208 209 private List<AclEntry> processPermissions(UserPrincipal owner, AclEntryPermission[] defaultAclFilePermissions) { 210 AclEntry.Builder aclBuilder = AclEntry.newBuilder(); 211 aclBuilder.setPermissions(defaultAclFilePermissions); 212 aclBuilder.setType(AclEntryType.ALLOW); 213 aclBuilder.setPrincipal(owner); 214 ArrayList<AclEntry> aclList = new ArrayList<>(); 215 aclList.add(aclBuilder.build()); 216 return aclList; 217 } 218 219 220 @Override 221 public RepositoryStorage getStorage( ) 222 { 223 return storage; 224 } 225 226 @Override 227 public String getPath() { 228 return relativePath; 229 } 230 231 @Override 232 public String getName() { 233 return assetPath.getFileName().toString(); 234 } 235 236 @Override 237 public Instant getModificationTime() { 238 try { 239 return Files.getLastModifiedTime(assetPath).toInstant(); 240 } catch (IOException e) { 241 log.error("Could not read modification time of {}", assetPath); 242 return Instant.now(); 243 } 244 } 245 246 /** 247 * Returns true, if the path of this asset points to a directory 248 * 249 * @return 250 */ 251 @Override 252 public boolean isContainer() { 253 if (Files.exists(assetPath)) { 254 return Files.isDirectory(assetPath); 255 } else { 256 return directoryHint; 257 } 258 } 259 260 /** 261 * Returns the list of directory entries, if this asset represents a directory. 262 * Otherwise a empty list will be returned. 263 * 264 * @return The list of entries in the directory, if it exists. 265 */ 266 @Override 267 public List<StorageAsset> list() { 268 try { 269 return Files.list(assetPath).map(p -> new FilesystemAsset(storage, relativePath + "/" + p.getFileName().toString(), assetPath.resolve(p))) 270 .collect(Collectors.toList()); 271 } catch (IOException e) { 272 return Collections.EMPTY_LIST; 273 } 274 } 275 276 /** 277 * Returns the size of the represented file. If it cannot be determined, -1 is returned. 278 * 279 * @return 280 */ 281 @Override 282 public long getSize() { 283 try { 284 return Files.size(assetPath); 285 } catch (IOException e) { 286 return -1; 287 } 288 } 289 290 /** 291 * Returns a input stream to the underlying file, if it exists. The caller has to make sure, that 292 * the stream is closed after it was used. 293 * 294 * @return 295 * @throws IOException 296 */ 297 @Override 298 public InputStream getReadStream() throws IOException { 299 if (isContainer()) { 300 throw new IOException("Can not create input stream for container"); 301 } 302 return Files.newInputStream(assetPath); 303 } 304 305 @Override 306 public ReadableByteChannel getReadChannel( ) throws IOException 307 { 308 return FileChannel.open( assetPath, StandardOpenOption.READ ); 309 } 310 311 private OpenOption[] getOpenOptions(boolean replace) { 312 return replace ? REPLACE_OPTIONS : APPEND_OPTIONS; 313 } 314 315 @Override 316 public OutputStream getWriteStream( boolean replace) throws IOException { 317 OpenOption[] options = getOpenOptions( replace ); 318 if (!Files.exists( assetPath )) { 319 create(); 320 } 321 return Files.newOutputStream(assetPath, options); 322 } 323 324 @Override 325 public WritableByteChannel getWriteChannel( boolean replace ) throws IOException 326 { 327 OpenOption[] options = getOpenOptions( replace ); 328 return FileChannel.open( assetPath, options ); 329 } 330 331 @Override 332 public boolean replaceDataFromFile( Path newData) throws IOException { 333 final boolean createNew = !Files.exists(assetPath); 334 Path backup = null; 335 if (!createNew) { 336 backup = findBackupFile(assetPath); 337 } 338 try { 339 if (!createNew) { 340 Files.move(assetPath, backup); 341 } 342 Files.move(newData, assetPath, StandardCopyOption.REPLACE_EXISTING); 343 applyDefaultPermissions(assetPath); 344 return true; 345 } catch (IOException e) { 346 log.error("Could not overwrite file {}", assetPath); 347 // Revert if possible 348 if (backup != null && Files.exists(backup)) { 349 Files.move(backup, assetPath, StandardCopyOption.REPLACE_EXISTING); 350 } 351 throw e; 352 } finally { 353 if (backup != null) { 354 try { 355 Files.deleteIfExists(backup); 356 } catch (IOException e) { 357 log.error("Could not delete backup file {}", backup); 358 } 359 } 360 } 361 362 } 363 364 private void applyDefaultPermissions(Path filePath) { 365 try { 366 if (supportsPosix) { 367 Set<PosixFilePermission> perms; 368 if (Files.isDirectory(filePath)) { 369 perms = defaultPosixFilePermissions; 370 } else { 371 perms = defaultPosixDirectoryPermissions; 372 } 373 Files.setPosixFilePermissions(filePath, perms); 374 } else if (supportsAcl) { 375 List<AclEntry> perms; 376 if (Files.isDirectory(filePath)) { 377 perms = getDefaultDirectoryAcls(); 378 } else { 379 perms = getDefaultFileAcls(); 380 } 381 AclFileAttributeView aclAttr = Files.getFileAttributeView(filePath, AclFileAttributeView.class); 382 aclAttr.setAcl(perms); 383 } 384 } catch (IOException e) { 385 log.error("Could not set permissions for {}: {}", filePath, e.getMessage()); 386 } 387 } 388 389 private Path findBackupFile(Path file) { 390 String ext = ".bak"; 391 Path backupPath = file.getParent().resolve(file.getFileName().toString() + ext); 392 int idx = 0; 393 while (Files.exists(backupPath)) { 394 backupPath = file.getParent().resolve(file.getFileName().toString() + ext + idx++); 395 } 396 return backupPath; 397 } 398 399 @Override 400 public boolean exists() { 401 return Files.exists(assetPath); 402 } 403 404 @Override 405 public Path getFilePath() throws UnsupportedOperationException { 406 return assetPath; 407 } 408 409 @Override 410 public boolean isFileBased( ) 411 { 412 return true; 413 } 414 415 @Override 416 public boolean hasParent( ) 417 { 418 if (basePath!=null && assetPath.equals(basePath)) { 419 return false; 420 } 421 return assetPath.getParent()!=null; 422 } 423 424 @Override 425 public StorageAsset getParent( ) 426 { 427 Path parentPath; 428 if (basePath!=null && assetPath.equals( basePath )) { 429 parentPath=null; 430 } else 431 { 432 parentPath = assetPath.getParent( ); 433 } 434 String relativeParent = StringUtils.substringBeforeLast( relativePath,"/"); 435 if (parentPath!=null) { 436 return new FilesystemAsset(storage, relativeParent, parentPath, basePath, true, setPermissionsForNew ); 437 } else { 438 return null; 439 } 440 } 441 442 @Override 443 public StorageAsset resolve(String toPath) { 444 return storage.getAsset(this.getPath()+"/"+toPath); 445 } 446 447 448 public void setDefaultFileAcls(List<AclEntry> acl) { 449 defaultFileAcls = acl; 450 } 451 452 public List<AclEntry> getDefaultFileAcls() { 453 return defaultFileAcls; 454 } 455 456 public void setDefaultPosixFilePermissions(Set<PosixFilePermission> perms) { 457 defaultPosixFilePermissions = perms; 458 } 459 460 public Set<PosixFilePermission> getDefaultPosixFilePermissions() { 461 return defaultPosixFilePermissions; 462 } 463 464 public void setDefaultDirectoryAcls(List<AclEntry> acl) { 465 defaultDirectoryAcls = acl; 466 } 467 468 public List<AclEntry> getDefaultDirectoryAcls() { 469 return defaultDirectoryAcls; 470 } 471 472 public void setDefaultPosixDirectoryPermissions(Set<PosixFilePermission> perms) { 473 defaultPosixDirectoryPermissions = perms; 474 } 475 476 public Set<PosixFilePermission> getDefaultPosixDirectoryPermissions() { 477 return defaultPosixDirectoryPermissions; 478 } 479 480 @Override 481 public void create() throws IOException { 482 if (!Files.exists(assetPath)) { 483 if (directoryHint) { 484 Files.createDirectories(assetPath); 485 } else { 486 if (!Files.exists( assetPath.getParent() )) { 487 Files.createDirectories( assetPath.getParent( ) ); 488 } 489 Files.createFile(assetPath); 490 } 491 if (setPermissionsForNew) { 492 applyDefaultPermissions(assetPath); 493 } 494 } 495 } 496 497 @Override 498 public String toString() { 499 return relativePath+":"+assetPath; 500 } 501 502 @Override 503 public int compareTo(Object o) { 504 if (o instanceof FilesystemAsset) { 505 if (this.getPath()!=null) { 506 return this.getPath().compareTo(((FilesystemAsset) o).getPath()); 507 } else { 508 return 0; 509 } 510 } 511 return 0; 512 } 513}