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}