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.OutputStream; 026import java.nio.ByteBuffer; 027import java.nio.charset.StandardCharsets; 028import java.nio.file.AccessDeniedException; 029import java.nio.file.FileSystemException; 030import java.nio.file.Files; 031import java.nio.file.Path; 032import java.nio.file.StandardCopyOption; 033import java.nio.file.attribute.FileTime; 034import java.util.concurrent.ThreadLocalRandom; 035import java.util.concurrent.atomic.AtomicBoolean; 036 037import static java.util.Objects.requireNonNull; 038 039/** 040 * Utility class serving as base of {@link PathProcessor} implementations. This class can be extended or replaced 041 * (as component) when needed. Also, this class is published in Resolver implementation for path processor interface. 042 * 043 * @since 2.0.13 044 */ 045public class PathProcessorSupport implements PathProcessor { 046 /** 047 * Logic borrowed from Commons-Lang3: we really need only this, to decide do we NIO2 file ops or not. 048 * For some reason non-NIO2 works better on Windows. 049 */ 050 protected static final boolean IS_WINDOWS = 051 System.getProperty("os.name", "unknown").startsWith("Windows"); 052 053 /** 054 * Escape hatch if atomic move is not desired on system we run on. 055 */ 056 protected static final boolean ATOMIC_MOVE = 057 Boolean.parseBoolean(System.getProperty(PathProcessor.class.getName() + "ATOMIC_MOVE", "true")); 058 059 @Override 060 public boolean setLastModified(Path path, long value) throws IOException { 061 try { 062 Files.setLastModifiedTime(path, FileTime.fromMillis(value)); 063 return true; 064 } catch (FileSystemException e) { 065 // MRESOLVER-536: Java uses generic FileSystemException for some weird cases, 066 // but some subclasses like AccessDeniedEx should be re-thrown 067 if (e instanceof AccessDeniedException) { 068 throw e; 069 } 070 return false; 071 } 072 } 073 074 @Override 075 public void write(Path target, String data) throws IOException { 076 writeFile(target, p -> Files.write(p, data.getBytes(StandardCharsets.UTF_8)), false); 077 } 078 079 @Override 080 public void write(Path target, InputStream source) throws IOException { 081 writeFile(target, p -> Files.copy(source, p, StandardCopyOption.REPLACE_EXISTING), false); 082 } 083 084 @Override 085 public void writeWithBackup(Path target, String data) throws IOException { 086 writeFile(target, p -> Files.write(p, data.getBytes(StandardCharsets.UTF_8)), true); 087 } 088 089 @Override 090 public void writeWithBackup(Path target, InputStream source) throws IOException { 091 writeFile(target, p -> Files.copy(source, p, StandardCopyOption.REPLACE_EXISTING), true); 092 } 093 094 /** 095 * A file writer, that accepts a {@link Path} to write some content to. Note: the file denoted by path may exist, 096 * hence implementation have to ensure it is able to achieve its goal ("replace existing" option or equivalent 097 * should be used). 098 */ 099 @FunctionalInterface 100 public interface FileWriter { 101 void write(Path path) throws IOException; 102 } 103 104 /** 105 * Utility method to write out file to disk in "atomic" manner, with optional backups (".bak") if needed. This 106 * ensures that no other thread or process will be able to read not fully written files. Finally, this method 107 * may create the needed parent directories, if the passed in target parents does not exist. 108 * 109 * @param target that is the target file (must be an existing or non-existing file, the path must have parent) 110 * @param writer the writer that will accept a {@link Path} to write content to 111 * @param doBackup if {@code true}, and target file is about to be overwritten, a ".bak" file with old contents will 112 * be created/overwritten 113 * @throws IOException if at any step IO problem occurs 114 */ 115 public void writeFile(Path target, FileWriter writer, boolean doBackup) throws IOException { 116 requireNonNull(target, "target is null"); 117 requireNonNull(writer, "writer is null"); 118 Path parent = requireNonNull(target.getParent(), "target must have parent"); 119 120 try (CollocatedTempFile tempFile = newTempFile(target)) { 121 writer.write(tempFile.getPath()); 122 if (doBackup && Files.isRegularFile(target)) { 123 Files.copy(target, parent.resolve(target.getFileName() + ".bak"), StandardCopyOption.REPLACE_EXISTING); 124 } 125 tempFile.move(); 126 } 127 } 128 129 @Override 130 public long copy(Path source, Path target, ProgressListener listener) throws IOException { 131 try (InputStream in = new BufferedInputStream(Files.newInputStream(source)); 132 CollocatedTempFile tempTarget = newTempFile(target); 133 OutputStream out = new BufferedOutputStream(Files.newOutputStream(tempTarget.getPath()))) { 134 long result = copy(out, in, listener); 135 tempTarget.move(); 136 return result; 137 } 138 } 139 140 private long copy(OutputStream os, InputStream is, ProgressListener listener) throws IOException { 141 long total = 0L; 142 byte[] buffer = new byte[1024 * 32]; 143 while (true) { 144 int bytes = is.read(buffer); 145 if (bytes < 0) { 146 break; 147 } 148 149 os.write(buffer, 0, bytes); 150 151 total += bytes; 152 153 if (listener != null && bytes > 0) { 154 try { 155 listener.progressed(ByteBuffer.wrap(buffer, 0, bytes)); 156 } catch (Exception e) { 157 // too bad 158 } 159 } 160 } 161 162 return total; 163 } 164 165 @Override 166 public void move(Path source, Path target) throws IOException { 167 final StandardCopyOption[] copyOption = ATOMIC_MOVE 168 ? new StandardCopyOption[] { 169 StandardCopyOption.ATOMIC_MOVE, 170 StandardCopyOption.REPLACE_EXISTING, 171 StandardCopyOption.COPY_ATTRIBUTES 172 } 173 : new StandardCopyOption[] {StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES}; 174 if (IS_WINDOWS) { 175 classicCopy(source, target); 176 } else { 177 Files.move(source, target, copyOption); 178 } 179 Files.deleteIfExists(source); 180 } 181 182 // Temp files 183 184 @Override 185 public TempFile newTempFile() throws IOException { 186 Path tempFile = Files.createTempFile("resolver", "tmp"); 187 return new TempFile() { 188 @Override 189 public Path getPath() { 190 return tempFile; 191 } 192 193 @Override 194 public void close() throws IOException { 195 Files.deleteIfExists(tempFile); 196 } 197 }; 198 } 199 200 @Override 201 public CollocatedTempFile newTempFile(Path file) throws IOException { 202 Path parent = requireNonNull(file.getParent(), "file must have parent"); 203 Files.createDirectories(parent); 204 Path tempFile = parent.resolve(file.getFileName() + "." 205 + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp"); 206 return new CollocatedTempFile() { 207 private final AtomicBoolean wantsMove = new AtomicBoolean(false); 208 private final StandardCopyOption[] copyOption = ATOMIC_MOVE 209 ? new StandardCopyOption[] {StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING} 210 : new StandardCopyOption[] {StandardCopyOption.REPLACE_EXISTING}; 211 212 @Override 213 public Path getPath() { 214 return tempFile; 215 } 216 217 @Override 218 public void move() { 219 wantsMove.set(true); 220 } 221 222 @Override 223 public void close() throws IOException { 224 if (wantsMove.get()) { 225 if (IS_WINDOWS) { 226 classicCopy(tempFile, file); 227 } else { 228 Files.move(tempFile, file, copyOption); 229 } 230 } 231 Files.deleteIfExists(tempFile); 232 } 233 }; 234 } 235 236 /** 237 * On Windows we use pre-NIO2 way to copy files, as for some reason it works. Beat me why. 238 */ 239 protected void classicCopy(Path source, Path target) throws IOException { 240 ByteBuffer buffer = ByteBuffer.allocate(1024 * 32); 241 byte[] array = buffer.array(); 242 try (InputStream is = Files.newInputStream(source); 243 OutputStream os = Files.newOutputStream(target)) { 244 while (true) { 245 int bytes = is.read(array); 246 if (bytes < 0) { 247 break; 248 } 249 os.write(array, 0, bytes); 250 } 251 } 252 } 253}