001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.spi.io; 020 021import java.io.BufferedInputStream; 022import java.io.BufferedOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.InterruptedIOException; 026import java.io.OutputStream; 027import java.nio.ByteBuffer; 028import java.nio.charset.StandardCharsets; 029import java.nio.file.AccessDeniedException; 030import java.nio.file.AtomicMoveNotSupportedException; 031import java.nio.file.FileSystemException; 032import java.nio.file.Files; 033import java.nio.file.Path; 034import java.nio.file.StandardCopyOption; 035import java.nio.file.attribute.FileTime; 036import java.util.concurrent.ThreadLocalRandom; 037import java.util.concurrent.atomic.AtomicBoolean; 038 039import static java.util.Objects.requireNonNull; 040 041/** 042 * Utility class serving as base of {@link PathProcessor} implementations. This class can be extended or replaced 043 * (as component) when needed. Also, this class is published in Resolver implementation for path processor interface. 044 * 045 * @since 2.0.13 046 */ 047public class PathProcessorSupport implements PathProcessor { 048 /** 049 * Logic borrowed from Commons-Lang3: we really need only this, to decide do we NIO2 file ops or not. 050 * On Windows the final move needs retry/staging logic, see {@link #retryingMove(Path, Path, StandardCopyOption[])}. 051 */ 052 protected static final boolean IS_WINDOWS = 053 System.getProperty("os.name", "unknown").startsWith("Windows"); 054 055 /** 056 * Escape hatch if atomic move is not desired on system we run on. 057 */ 058 protected static final boolean ATOMIC_MOVE = 059 Boolean.parseBoolean(System.getProperty(PathProcessor.class.getName() + "ATOMIC_MOVE", "true")); 060 061 /** 062 * The number of attempts for the final move on Windows, where the move target may be transiently locked by a 063 * virus scanner, an indexer or a concurrent reader. 064 */ 065 protected static final int WINDOWS_MOVE_ATTEMPTS = 066 Math.max(1, Integer.getInteger(PathProcessor.class.getName() + "WINDOWS_MOVE_ATTEMPTS", 5)); 067 068 /** 069 * The delay in milliseconds applied between the move attempts on Windows. 070 */ 071 protected static final long WINDOWS_MOVE_RETRY_DELAY = 072 Long.getLong(PathProcessor.class.getName() + "WINDOWS_MOVE_RETRY_DELAY", 50L); 073 074 @Override 075 public boolean setLastModified(Path path, long value) throws IOException { 076 try { 077 Files.setLastModifiedTime(path, FileTime.fromMillis(value)); 078 return true; 079 } catch (FileSystemException e) { 080 // MRESOLVER-536: Java uses generic FileSystemException for some weird cases, 081 // but some subclasses like AccessDeniedEx should be re-thrown 082 if (e instanceof AccessDeniedException) { 083 throw e; 084 } 085 return false; 086 } 087 } 088 089 @Override 090 public void write(Path target, String data) throws IOException { 091 writeFile(target, p -> Files.write(p, data.getBytes(StandardCharsets.UTF_8)), false); 092 } 093 094 @Override 095 public void write(Path target, InputStream source) throws IOException { 096 writeFile(target, p -> Files.copy(source, p, StandardCopyOption.REPLACE_EXISTING), false); 097 } 098 099 @Override 100 public void writeWithBackup(Path target, String data) throws IOException { 101 writeFile(target, p -> Files.write(p, data.getBytes(StandardCharsets.UTF_8)), true); 102 } 103 104 @Override 105 public void writeWithBackup(Path target, InputStream source) throws IOException { 106 writeFile(target, p -> Files.copy(source, p, StandardCopyOption.REPLACE_EXISTING), true); 107 } 108 109 /** 110 * A file writer, that accepts a {@link Path} to write some content to. Note: the file denoted by path may exist, 111 * hence implementation have to ensure it is able to achieve its goal ("replace existing" option or equivalent 112 * should be used). 113 */ 114 @FunctionalInterface 115 public interface FileWriter { 116 void write(Path path) throws IOException; 117 } 118 119 /** 120 * Utility method to write out file to disk in "atomic" manner, with optional backups (".bak") if needed. This 121 * ensures that no other thread or process will be able to read not fully written files. Finally, this method 122 * may create the needed parent directories, if the passed in target parents does not exist. 123 * 124 * @param target that is the target file (must be an existing or non-existing file, the path must have parent) 125 * @param writer the writer that will accept a {@link Path} to write content to 126 * @param doBackup if {@code true}, and target file is about to be overwritten, a ".bak" file with old contents will 127 * be created/overwritten 128 * @throws IOException if at any step IO problem occurs 129 */ 130 public void writeFile(Path target, FileWriter writer, boolean doBackup) throws IOException { 131 requireNonNull(target, "target is null"); 132 requireNonNull(writer, "writer is null"); 133 Path parent = requireNonNull(target.getParent(), "target must have parent"); 134 135 try (CollocatedTempFile tempFile = newTempFile(target)) { 136 writer.write(tempFile.getPath()); 137 if (doBackup && Files.isRegularFile(target)) { 138 Files.copy(target, parent.resolve(target.getFileName() + ".bak"), StandardCopyOption.REPLACE_EXISTING); 139 } 140 tempFile.move(); 141 } 142 } 143 144 @Override 145 public long copy(Path source, Path target, ProgressListener listener) throws IOException { 146 try (InputStream in = new BufferedInputStream(Files.newInputStream(source)); 147 CollocatedTempFile tempTarget = newTempFile(target); 148 OutputStream out = new BufferedOutputStream(Files.newOutputStream(tempTarget.getPath()))) { 149 long result = copy(out, in, listener); 150 tempTarget.move(); 151 return result; 152 } 153 } 154 155 private long copy(OutputStream os, InputStream is, ProgressListener listener) throws IOException { 156 long total = 0L; 157 byte[] buffer = new byte[1024 * 32]; 158 while (true) { 159 int bytes = is.read(buffer); 160 if (bytes < 0) { 161 break; 162 } 163 164 os.write(buffer, 0, bytes); 165 166 total += bytes; 167 168 if (listener != null && bytes > 0) { 169 try { 170 listener.progressed(ByteBuffer.wrap(buffer, 0, bytes)); 171 } catch (Exception e) { 172 // too bad 173 } 174 } 175 } 176 177 return total; 178 } 179 180 @Override 181 public void move(Path source, Path target) throws IOException { 182 final StandardCopyOption[] copyOption = ATOMIC_MOVE 183 ? new StandardCopyOption[] { 184 StandardCopyOption.ATOMIC_MOVE, 185 StandardCopyOption.REPLACE_EXISTING, 186 StandardCopyOption.COPY_ATTRIBUTES 187 } 188 : new StandardCopyOption[] {StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES}; 189 if (IS_WINDOWS) { 190 retryingMove(source, target, copyOption); 191 } else { 192 Files.move(source, target, copyOption); 193 } 194 Files.deleteIfExists(source); 195 } 196 197 // Temp files 198 199 @Override 200 public TempFile newTempFile() throws IOException { 201 Path tempFile = Files.createTempFile("resolver", "tmp"); 202 return new TempFile() { 203 @Override 204 public Path getPath() { 205 return tempFile; 206 } 207 208 @Override 209 public void close() throws IOException { 210 Files.deleteIfExists(tempFile); 211 } 212 }; 213 } 214 215 @Override 216 public CollocatedTempFile newTempFile(Path file) throws IOException { 217 Path parent = requireNonNull(file.getParent(), "file must have parent"); 218 Files.createDirectories(parent); 219 Path tempFile = parent.resolve(file.getFileName() + "." 220 + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp"); 221 return new CollocatedTempFile() { 222 private final AtomicBoolean wantsMove = new AtomicBoolean(false); 223 private final StandardCopyOption[] copyOption = ATOMIC_MOVE 224 ? new StandardCopyOption[] {StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING} 225 : new StandardCopyOption[] {StandardCopyOption.REPLACE_EXISTING}; 226 227 @Override 228 public Path getPath() { 229 return tempFile; 230 } 231 232 @Override 233 public void move() { 234 wantsMove.set(true); 235 } 236 237 @Override 238 public void close() throws IOException { 239 if (wantsMove.get()) { 240 if (IS_WINDOWS) { 241 retryingMove(tempFile, file, copyOption); 242 } else { 243 Files.move(tempFile, file, copyOption); 244 } 245 } 246 Files.deleteIfExists(tempFile); 247 } 248 }; 249 } 250 251 /** 252 * Moves the source file to the target path without ever truncating the target in place: the content visible at 253 * the target path is either the old file or the complete new file, never a partially written one. 254 * <p> 255 * Historically, on Windows the final move was implemented by opening the target path with a truncating stream 256 * and copying the source into it ({@link #classicCopy(Path, Path)}). That left a window during which a 257 * concurrent reader (for example a forked JVM resolving the same artifact) or an untimely process kill would 258 * observe, or durably leave behind, a truncated file at the final path - after checksum validation has already 259 * happened, so the torn content would from then on be trusted as verified. Instead, this method attempts the 260 * rename a bounded number of times ({@link #WINDOWS_MOVE_ATTEMPTS}, file locking by virus scanners, indexers or 261 * concurrent readers is transient on Windows), and if the file system refuses an atomic move (for example when 262 * source and target are on different stores), the source is first staged into a collocated temporary file next 263 * to the target and then renamed into place - the target path itself is never opened for writing. 264 */ 265 protected void retryingMove(Path source, Path target, StandardCopyOption[] copyOptions) throws IOException { 266 FileSystemException lastException = null; 267 for (int attempt = 0; attempt < WINDOWS_MOVE_ATTEMPTS; attempt++) { 268 try { 269 fileSystemMove(source, target, copyOptions); 270 return; 271 } catch (AtomicMoveNotSupportedException e) { 272 lastException = e; 273 break; // retrying will not help; stage a collocated copy instead, see below 274 } catch (FileSystemException e) { 275 // covers AccessDeniedException and sharing violations: transient on Windows 276 lastException = e; 277 try { 278 Thread.sleep(WINDOWS_MOVE_RETRY_DELAY); 279 } catch (InterruptedException ie) { 280 Thread.currentThread().interrupt(); 281 InterruptedIOException interrupted = 282 new InterruptedIOException("Interrupted while moving " + source + " to " + target); 283 interrupted.initCause(ie); 284 interrupted.addSuppressed(e); 285 throw interrupted; 286 } 287 } 288 } 289 if (lastException instanceof AtomicMoveNotSupportedException) { 290 Path staged = target.resolveSibling(target.getFileName() + "." 291 + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp"); 292 try { 293 classicCopy(source, staged); 294 fileSystemMove(staged, target, copyOptions); 295 return; 296 } finally { 297 Files.deleteIfExists(staged); 298 } 299 } 300 throw lastException; 301 } 302 303 /** 304 * Testable seam for {@link Files#move(Path, Path, java.nio.file.CopyOption...)}. 305 */ 306 protected void fileSystemMove(Path source, Path target, StandardCopyOption... copyOptions) throws IOException { 307 Files.move(source, target, copyOptions); 308 } 309 310 /** 311 * Pre-NIO2 way to copy files. Important: this method must never be pointed at a "final" (published) path, as it 312 * opens the target with truncation; callers stage into a temporary file and rename into place instead. 313 */ 314 protected void classicCopy(Path source, Path target) throws IOException { 315 ByteBuffer buffer = ByteBuffer.allocate(1024 * 32); 316 byte[] array = buffer.array(); 317 try (InputStream is = Files.newInputStream(source); 318 OutputStream os = Files.newOutputStream(target)) { 319 while (true) { 320 int bytes = is.read(array); 321 if (bytes < 0) { 322 break; 323 } 324 os.write(array, 0, bytes); 325 } 326 } 327 } 328}