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 org.spongepowered.configurate.loader; 018 019import static java.util.Objects.requireNonNull; 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 032/** 033 * A utility for creating "atomic" file writers. 034 * 035 * <p>An atomic writer copies any existing file at the given path to a temporary 036 * location, then writes to the same temporary location, before moving the file 037 * back to the desired output path once the write is fully complete.</p> 038 * 039 * @since 4.0.0 040 */ 041public final class AtomicFiles { 042 043 private AtomicFiles() {} 044 045 /** 046 * Creates and returns an "atomic" writer factory for the given path. 047 * 048 * @param path path the complete file should be written to 049 * @param charset the charset to be used by the writer 050 * @return a new writer factory 051 * @since 4.0.0 052 */ 053 public static Callable<BufferedWriter> atomicWriterFactory(final Path path, final Charset charset) { 054 requireNonNull(path, "path"); 055 return () -> atomicBufferedWriter(path, charset); 056 } 057 058 /** 059 * Creates and returns an "atomic" writer for the given path. 060 * 061 * @param path the path 062 * @param charset the charset to be used by the writer 063 * @return a new writer factory 064 * @throws IOException for any underlying filesystem errors 065 * @since 4.0.0 066 */ 067 public static BufferedWriter atomicBufferedWriter(Path path, final Charset charset) throws IOException { 068 // absolute 069 path = path.toAbsolutePath(); 070 071 // unwrap any symbolic links 072 try { 073 while (Files.isSymbolicLink(path)) { 074 path = Files.readSymbolicLink(path); 075 } 076 } catch (final UnsupportedOperationException | IOException ex) { 077 // ignore 078 } 079 080 final Path writePath = temporaryPath(path.getParent(), path.getFileName().toString()); 081 if (Files.exists(path)) { 082 Files.copy(path, writePath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); 083 } 084 085 Files.createDirectories(writePath.getParent()); 086 final BufferedWriter output = Files.newBufferedWriter(writePath, charset); 087 return new BufferedWriter(new AtomicFileWriter(writePath, path, output)); 088 } 089 090 private static Path temporaryPath(final Path parent, final String key) { 091 final String fileName = System.nanoTime() + ThreadLocalRandom.current().nextInt() 092 + requireNonNull(key, "key").replaceAll("[\\\\/:]", "-") + ".tmp"; 093 return parent.resolve(fileName); 094 } 095 096 private static class AtomicFileWriter extends FilterWriter { 097 098 private final Path targetPath; 099 private final Path writePath; 100 101 protected AtomicFileWriter(final Path writePath, final Path targetPath, final Writer wrapping) { 102 super(wrapping); 103 this.writePath = writePath; 104 this.targetPath = targetPath; 105 } 106 107 @Override 108 public void close() throws IOException { 109 super.close(); 110 Files.move(this.writePath, this.targetPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); 111 } 112 113 } 114 115}