001/* 002 * Configurate 003 * Copyright (C) zml and Configurate contributors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package ninja.leaping.configurate.loader; 018 019import org.checkerframework.checker.nullness.qual.NonNull; 020 021import java.io.BufferedWriter; 022import java.io.FilterWriter; 023import java.io.IOException; 024import java.io.Writer; 025import java.nio.charset.Charset; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.nio.file.StandardCopyOption; 029import java.util.concurrent.Callable; 030import java.util.concurrent.ThreadLocalRandom; 031 032import static java.util.Objects.requireNonNull; 033 034/** 035 * A utility for creating "atomic" file writers. 036 * 037 * <p>An atomic writer copies any existing file at the given path to a temporary location, then 038 * writes to the same temporary location, before moving the file back to the desired output path 039 * once the write is fully complete.</p> 040 */ 041public final class AtomicFiles { 042 private AtomicFiles() {} 043 044 /** 045 * Creates and returns an "atomic" writer factory for the given path. 046 * 047 * @param path The path 048 * @param charset The charset to be used by the writer 049 * @return The writer factory 050 */ 051 @NonNull 052 public static Callable<BufferedWriter> createAtomicWriterFactory(@NonNull Path path, @NonNull Charset charset) { 053 requireNonNull(path, "path"); 054 return () -> createAtomicBufferedWriter(path, charset); 055 } 056 057 /** 058 * Creates and returns an "atomic" writer for the given path. 059 * 060 * @param path The path 061 * @param charset The charset to be used by the writer 062 * @return The writer factory 063 * @throws IOException For any underlying filesystem errors 064 */ 065 @NonNull 066 public static BufferedWriter createAtomicBufferedWriter(@NonNull Path path, @NonNull Charset charset) throws IOException { 067 // absolute 068 path = path.toAbsolutePath(); 069 070 // unwrap any symbolic links 071 try { 072 while (Files.isSymbolicLink(path)) { 073 path = Files.readSymbolicLink(path); 074 } 075 } catch (final UnsupportedOperationException | IOException ex) { 076 // ignore, FS probably doesn't support symlinks 077 } 078 079 final Path writePath = getTemporaryPath(path.getParent(), path.getFileName().toString()); 080 if (Files.exists(path)) { 081 Files.copy(path, writePath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); 082 } 083 084 BufferedWriter output = Files.newBufferedWriter(writePath, charset); 085 return new BufferedWriter(new AtomicFileWriter(writePath, path, output)); 086 } 087 088 @NonNull 089 private static Path getTemporaryPath(@NonNull Path parent, @NonNull String key) { 090 String fileName = System.nanoTime() + ThreadLocalRandom.current().nextInt() + requireNonNull(key, "key").replaceAll("\\\\|/|:", 091 "-") + ".tmp"; 092 return parent.resolve(fileName); 093 } 094 095 private static class AtomicFileWriter extends FilterWriter { 096 private final Path targetPath, writePath; 097 098 protected AtomicFileWriter(Path writePath, Path targetPath, Writer wrapping) { 099 super(wrapping); 100 this.writePath = writePath; 101 this.targetPath = targetPath; 102 } 103 104 @Override 105 public void close() throws IOException { 106 super.close(); 107 Files.createDirectories(targetPath.getParent()); 108 Files.move(writePath, targetPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); 109 } 110 } 111}