1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.eclipse.aether.util;
20
21 import java.io.Closeable;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.nio.ByteBuffer;
26 import java.nio.file.Files;
27 import java.nio.file.Path;
28 import java.nio.file.StandardCopyOption;
29 import java.util.concurrent.ThreadLocalRandom;
30 import java.util.concurrent.atomic.AtomicBoolean;
31
32 import static java.util.Objects.requireNonNull;
33
34 /**
35 * A utility class to write files.
36 *
37 * @since 1.9.0
38 */
39 public final class FileUtils {
40 // Logic borrowed from Commons-Lang3: we really need only this, to decide do we "atomic move" or not
41 private static final boolean IS_WINDOWS =
42 System.getProperty("os.name", "unknown").startsWith("Windows");
43
44 private FileUtils() {
45 // hide constructor
46 }
47
48 /**
49 * A temporary file, that is removed when closed.
50 */
51 public interface TempFile extends Closeable {
52 /**
53 * Returns the path of the created temp file.
54 */
55 Path getPath();
56 }
57
58 /**
59 * A collocated temporary file, that resides next to a "target" file, and is removed when closed.
60 */
61 public interface CollocatedTempFile extends TempFile {
62 /**
63 * Upon close, atomically moves temp file to target file it is collocated with overwriting target (if exists).
64 * Invocation of this method merely signals that caller ultimately wants temp file to replace the target
65 * file, but when this method returns, the move operation did not yet happen, it will happen when this
66 * instance is closed.
67 * <p>
68 * Invoking this method <em>without writing to temp file</em> {@link #getPath()} (thus, not creating a temp
69 * file to be moved) is considered a bug, a mistake of the caller. Caller of this method should ensure
70 * that this method is invoked ONLY when the temp file is created and moving it to its final place is
71 * required.
72 */
73 void move() throws IOException;
74 }
75
76 /**
77 * Creates a {@link TempFile} instance and backing temporary file on file system. It will be located in the default
78 * temporary-file directory. Returned instance should be handled in try-with-resource construct and created
79 * temp file is removed (if exists) when returned instance is closed.
80 * <p>
81 * This method uses {@link Files#createTempFile(String, String, java.nio.file.attribute.FileAttribute[])} to create
82 * the temporary file on file system.
83 */
84 public static TempFile newTempFile() throws IOException {
85 Path tempFile = Files.createTempFile("resolver", "tmp");
86 return new TempFile() {
87 @Override
88 public Path getPath() {
89 return tempFile;
90 }
91
92 @Override
93 public void close() throws IOException {
94 Files.deleteIfExists(tempFile);
95 }
96 };
97 }
98
99 /**
100 * Creates a {@link CollocatedTempFile} instance for given file without backing file. The path will be located in
101 * same directory where given file is, and will reuse its name for generated (randomized) name. Returned instance
102 * should be handled in try-with-resource and created temp path is removed (if exists) when returned instance is
103 * closed. The {@link CollocatedTempFile#move()} makes possible to atomically replace passed in file with the
104 * processed content written into a file backing the {@link CollocatedTempFile} instance.
105 * <p>
106 * The {@code file} nor it's parent directories have to exist. The parent directories are created if needed.
107 * <p>
108 * This method uses {@link Path#resolve(String)} to create the temporary file path in passed in file parent
109 * directory, but it does NOT create backing file on file system.
110 */
111 public static CollocatedTempFile newTempFile(Path file) throws IOException {
112 Path parent = requireNonNull(file.getParent(), "file must have parent");
113 Files.createDirectories(parent);
114 Path tempFile = parent.resolve(file.getFileName() + "."
115 + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
116 return new CollocatedTempFile() {
117 private final AtomicBoolean wantsMove = new AtomicBoolean(false);
118
119 @Override
120 public Path getPath() {
121 return tempFile;
122 }
123
124 @Override
125 public void move() {
126 wantsMove.set(true);
127 }
128
129 @Override
130 public void close() throws IOException {
131 if (wantsMove.get()) {
132 if (IS_WINDOWS) {
133 copy(tempFile, file);
134 } else {
135 Files.move(tempFile, file, StandardCopyOption.REPLACE_EXISTING);
136 }
137 }
138 Files.deleteIfExists(tempFile);
139 }
140 };
141 }
142
143 /**
144 * On Windows we use pre-NIO2 way to copy files, as for some reason it works. Beat me why.
145 */
146 private static void copy(Path source, Path target) throws IOException {
147 ByteBuffer buffer = ByteBuffer.allocate(1024 * 32);
148 byte[] array = buffer.array();
149 try (InputStream is = Files.newInputStream(source);
150 OutputStream os = Files.newOutputStream(target)) {
151 while (true) {
152 int bytes = is.read(array);
153 if (bytes < 0) {
154 break;
155 }
156 os.write(array, 0, bytes);
157 }
158 }
159 }
160
161 /**
162 * A file writer, that accepts a {@link Path} to write some content to. Note: the file denoted by path may exist,
163 * hence implementation have to ensure it is able to achieve its goal ("replace existing" option or equivalent
164 * should be used).
165 */
166 @FunctionalInterface
167 public interface FileWriter {
168 void write(Path path) throws IOException;
169 }
170
171 /**
172 * Writes file without backup.
173 *
174 * @param target that is the target file (must be file, the path must have parent).
175 * @param writer the writer that will accept a {@link Path} to write content to.
176 * @throws IOException if at any step IO problem occurs.
177 */
178 public static void writeFile(Path target, FileWriter writer) throws IOException {
179 writeFile(target, writer, false);
180 }
181
182 /**
183 * Writes file with backup copy (appends ".bak" extension).
184 *
185 * @param target that is the target file (must be file, the path must have parent).
186 * @param writer the writer that will accept a {@link Path} to write content to.
187 * @throws IOException if at any step IO problem occurs.
188 */
189 public static void writeFileWithBackup(Path target, FileWriter writer) throws IOException {
190 writeFile(target, writer, true);
191 }
192
193 /**
194 * Utility method to write out file to disk in "atomic" manner, with optional backups (".bak") if needed. This
195 * ensures that no other thread or process will be able to read not fully written files. Finally, this methos
196 * may create the needed parent directories, if the passed in target parents does not exist.
197 *
198 * @param target that is the target file (must be an existing or non-existing file, the path must have parent).
199 * @param writer the writer that will accept a {@link Path} to write content to.
200 * @param doBackup if {@code true}, and target file is about to be overwritten, a ".bak" file with old contents will
201 * be created/overwritten.
202 * @throws IOException if at any step IO problem occurs.
203 */
204 private static void writeFile(Path target, FileWriter writer, boolean doBackup) throws IOException {
205 requireNonNull(target, "target is null");
206 requireNonNull(writer, "writer is null");
207 Path parent = requireNonNull(target.getParent(), "target must have parent");
208
209 try (CollocatedTempFile tempFile = newTempFile(target)) {
210 writer.write(tempFile.getPath());
211 if (doBackup && Files.isRegularFile(target)) {
212 Files.copy(target, parent.resolve(target.getFileName() + ".bak"), StandardCopyOption.REPLACE_EXISTING);
213 }
214 tempFile.move();
215 }
216 }
217 }