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.AccessDeniedException; 027import java.nio.file.Files; 028import java.nio.file.Path; 029import java.nio.file.StandardCopyOption; 030import java.util.concurrent.Callable; 031import java.util.concurrent.ThreadLocalRandom; 032 033/** 034 * A utility for creating "atomic" file writers. 035 * 036 * <p>An atomic writer copies any existing file at the given path to a temporary 037 * location, then writes to the same temporary location, before moving the file 038 * back to the desired output path once the write is fully complete.</p> 039 * 040 * @since 4.0.0 041 */ 042public final class AtomicFiles { 043 044 private static final int MAX_TRIES = 2; 045 046 private AtomicFiles() {} 047 048 /** 049 * Creates and returns an "atomic" writer factory for the given path. 050 * 051 * @param path path the complete file should be written to 052 * @param charset the charset to be used by the writer 053 * @return a new writer factory 054 * @since 4.0.0 055 */ 056 public static Callable<BufferedWriter> atomicWriterFactory(final Path path, final Charset charset) { 057 requireNonNull(path, "path"); 058 return () -> atomicBufferedWriter(path, charset); 059 } 060 061 /** 062 * Creates and returns an "atomic" writer for the given path. 063 * 064 * @param path the path 065 * @param charset the charset to be used by the writer 066 * @return a new writer factory 067 * @throws IOException for any underlying filesystem errors 068 * @since 4.0.0 069 */ 070 public static BufferedWriter atomicBufferedWriter(Path path, final Charset charset) throws IOException { 071 // absolute 072 path = path.toAbsolutePath(); 073 074 // unwrap any symbolic links 075 try { 076 while (Files.isSymbolicLink(path)) { 077 path = Files.readSymbolicLink(path); 078 } 079 } catch (final UnsupportedOperationException | IOException ex) { 080 // ignore 081 } 082 083 final Path writePath = temporaryPath(path.getParent(), path.getFileName().toString()); 084 if (Files.exists(path)) { 085 Files.copy(path, writePath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); 086 } 087 088 createDirectoriesIfNecessary(writePath.getParent()); 089 final BufferedWriter output = Files.newBufferedWriter(writePath, charset); 090 return new BufferedWriter(new AtomicFileWriter(writePath, path, output)); 091 } 092 093 // symlink-aware directory creation 094 private static void createDirectoriesIfNecessary(final Path directory) throws IOException { 095 if (!Files.isDirectory(directory)) { 096 Files.createDirectories(directory); 097 } 098 } 099 100 private static Path temporaryPath(final Path parent, final String key) { 101 final String fileName = "." + System.nanoTime() + ThreadLocalRandom.current().nextInt() 102 + requireNonNull(key, "key").replaceAll("[\\\\/:]", "-") + ".tmp"; 103 return parent.resolve(fileName); 104 } 105 106 private static class AtomicFileWriter extends FilterWriter { 107 108 private final Path targetPath; 109 private final Path writePath; 110 111 protected AtomicFileWriter(final Path writePath, final Path targetPath, final Writer wrapping) { 112 super(wrapping); 113 this.writePath = writePath; 114 this.targetPath = targetPath; 115 } 116 117 @Override 118 public void close() throws IOException { 119 super.close(); 120 try { 121 Files.move(this.writePath, this.targetPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); 122 } catch (final AccessDeniedException ex) { 123 // Sometimes because of file locking this will fail... Let's just try again and hope for the best 124 // Thanks Windows! 125 for (int tries = 0; tries < MAX_TRIES; ++tries) { 126 // Pause for a bit 127 try { 128 Thread.sleep(5L * (tries + 1)); 129 Files.move(this.writePath, this.targetPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); 130 return; 131 } catch (final AccessDeniedException ex2) { 132 if (tries == MAX_TRIES - 1) { 133 throw ex; 134 } 135 } catch (final InterruptedException exInterrupt) { 136 Thread.currentThread().interrupt(); 137 throw ex; 138 } 139 } 140 } 141 } 142 143 } 144 145}