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 path = path.toAbsolutePath(); 068 069 Path writePath = getTemporaryPath(path.getParent(), path.getFileName().toString()); 070 if (Files.exists(path)) { 071 Files.copy(path, writePath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); 072 } 073 074 BufferedWriter output = Files.newBufferedWriter(writePath, charset); 075 return new BufferedWriter(new AtomicFileWriter(writePath, path, output)); 076 } 077 078 @NonNull 079 private static Path getTemporaryPath(@NonNull Path parent, @NonNull String key) { 080 String fileName = System.nanoTime() + ThreadLocalRandom.current().nextInt() + requireNonNull(key, "key").replaceAll("\\\\|/|:", 081 "-") + ".tmp"; 082 return parent.resolve(fileName); 083 } 084 085 private static class AtomicFileWriter extends FilterWriter { 086 private final Path targetPath, writePath; 087 088 protected AtomicFileWriter(Path writePath, Path targetPath, Writer wrapping) { 089 super(wrapping); 090 this.writePath = writePath; 091 this.targetPath = targetPath; 092 } 093 094 @Override 095 public void close() throws IOException { 096 super.close(); 097 Files.createDirectories(targetPath.getParent()); 098 Files.move(writePath, targetPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); 099 } 100 } 101}