TemporaryAssetFolder.java
/*
* Copyright (c) 2017, Stein Eldar Johnsen
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.morimekta.file;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.newOutputStream;
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
/**
* A scoped temporary folder that guarantees complete cleanup on close. Any
* number of files and sub-directories can be created inside it, and all
* contents are deleted recursively when the folder is closed. Example usage:
*
* <pre>{@code
* try (TemporaryAssetFolder tmp = new TemporaryAssetFolder(base)) {
* // create files, run operations, etc.
* }
* // all contents are deleted.
* }</pre>
*
* Each instance creates a new temporary directory under the given base
* directory. This is preferable to standard temporary files in environments
* where temp storage is backed by memory (e.g. Kubernetes {@code emptyDir}
* with {@code medium: Memory}), since accumulated temp files would otherwise
* consume memory until JVM exit.
*/
public class TemporaryAssetFolder implements Closeable {
private static final String PARENT_DIR = "..";
private final Path path;
/**
* Create a temporary asset folder in {@code /tmp/}.
*
* @throws IOException If unable to create the temporary directory.
*/
public TemporaryAssetFolder() throws IOException {
this(Paths.get("/tmp/"));
}
/**
* Create a temporary asset folder in the given base directory with
* the default prefix {@code "tmp"}.
*
* @param baseTempDirectory The parent directory to create the temp folder in.
* @throws IOException If unable to create the temporary directory.
*/
public TemporaryAssetFolder(Path baseTempDirectory) throws IOException {
this(baseTempDirectory, "tmp");
}
/**
* Create a temporary asset folder in the given base directory.
*
* @param baseTempDirectory The parent directory to create the temp folder in.
* @param prefix The prefix for the temporary directory name.
* @throws IOException If unable to create the temporary directory.
*/
public TemporaryAssetFolder(Path baseTempDirectory, String prefix) throws IOException {
path = Files.createTempDirectory(baseTempDirectory, prefix);
}
/**
* List all files in the temporary folder.
*
* @return Collection of files in the temporary folder.
* @throws IOException When unable to list files.
*/
public Collection<Path> list() throws IOException {
return FileUtil.list(path);
}
/**
* List all files in the temporary folder.
*
* @param recursive If true, recursively list files in sub-directories.
* @return Collection of files in the temporary folder.
* @throws IOException When unable to list files.
*/
public Collection<Path> list(boolean recursive) throws IOException {
return FileUtil.list(path, recursive);
}
/**
* @return The path to the temporary folder.
*/
public Path getPath() {
return path;
}
/**
* @return The temporary folder as a {@link File}.
*/
public File getFile() {
return getPath().toFile();
}
/**
* Resolve a relative name to a path within the temporary folder. The name
* must not be absolute or contain parent directory references.
*
* @param name The relative file name to resolve.
* @return The resolved path.
* @throws IllegalArgumentException If the name is absolute or references a parent directory.
*/
public Path resolvePath(String name) {
if (name.startsWith(File.separator)) {
throw new IllegalArgumentException("Absolute path not allowed in file name");
} else if (name.contains(File.separator + PARENT_DIR + File.separator) ||
name.startsWith(PARENT_DIR + File.separator)) {
throw new IllegalArgumentException("Up path not allowed in file name");
}
return getPath().resolve(name);
}
/**
* Resolve a relative name to a {@link File} within the temporary folder.
*
* @param name The relative file name to resolve.
* @return The resolved file.
* @throws IllegalArgumentException If the name is absolute or references a parent directory.
*/
public File resolveFile(String name) {
return resolvePath(name).toFile();
}
/**
* Write byte content to a file in the temporary folder. Creates parent
* directories as needed. Replaces existing content.
*
* @param name The relative file name.
* @param content The byte content to write.
* @return The path to the written file.
* @throws IOException If unable to write the file.
*/
public Path put(String name, byte[] content) throws IOException {
Path file = resolvePath(name);
Path parent = file.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
return Files.write(file, content, TRUNCATE_EXISTING, CREATE);
}
/**
* Write character content to a file in the temporary folder. Creates parent
* directories as needed. Replaces existing content.
*
* @param name The relative file name.
* @param content The character content to write.
* @return The path to the written file.
* @throws IOException If unable to write the file.
*/
public Path put(String name, CharSequence content) throws IOException {
Path file = resolvePath(name);
Path parent = file.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
return Files.writeString(file, content, TRUNCATE_EXISTING, CREATE);
}
/**
* Get an output stream for writing to a file in the temporary folder.
*
* @param name The relative file name.
* @return An output stream to the file.
* @throws IOException If unable to open the stream.
*/
public OutputStream getOutputStream(String name) throws IOException {
return getOutputStream(name, false);
}
/**
* Get an output stream for writing to a file in the temporary folder.
* Creates parent directories as needed.
*
* @param name The relative file name.
* @param append If true, append to existing content instead of replacing.
* @return An output stream to the file.
* @throws IOException If unable to open the stream.
*/
public OutputStream getOutputStream(String name, boolean append) throws IOException {
Path file = resolvePath(name);
Path parent = file.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
return newOutputStream(file, CREATE, append ? APPEND : TRUNCATE_EXISTING);
}
/**
* Get a UTF-8 writer for writing to a file in the temporary folder.
*
* @param name The relative file name.
* @return A writer to the file.
* @throws IOException If unable to open the writer.
*/
public Writer getWriter(String name) throws IOException {
return getWriter(name, false);
}
/**
* Get a UTF-8 writer for writing to a file in the temporary folder.
*
* @param name The relative file name.
* @param append If true, append to existing content instead of replacing.
* @return A writer to the file.
* @throws IOException If unable to open the writer.
*/
public Writer getWriter(String name, boolean append) throws IOException {
return new OutputStreamWriter(getOutputStream(name, append), UTF_8);
}
/**
* Read the content of a file as a UTF-8 string.
*
* @param name The relative file name.
* @return The file content as a string.
* @throws IOException If unable to read the file.
*/
public String getString(String name) throws IOException {
return new String(getBytes(name), UTF_8);
}
/**
* Read the content of a file as a byte array.
*
* @param name The relative file name.
* @return The file content as bytes.
* @throws IOException If unable to read the file.
*/
public byte[] getBytes(String name) throws IOException {
try (InputStream in = getInputStream(name);
BufferedInputStream bis = new BufferedInputStream(in)) {
return bis.readAllBytes();
}
}
/**
* Get a UTF-8 reader for reading from a file in the temporary folder.
*
* @param name The relative file name.
* @return A reader for the file.
* @throws IOException If unable to open the reader.
*/
public Reader getReader(String name) throws IOException {
return new InputStreamReader(getInputStream(name), UTF_8);
}
/**
* Get an input stream for reading from a file in the temporary folder.
*
* @param name The relative file name.
* @return An input stream for the file.
* @throws IOException If unable to open the stream.
*/
public InputStream getInputStream(String name) throws IOException {
return Files.newInputStream(resolvePath(name));
}
@Override
public void close() throws IOException {
FileUtil.deleteRecursively(path);
}
}