Skip to content

Instantly share code, notes, and snippets.

@shiguruikai
Last active February 28, 2022 13:56
Show Gist options
  • Save shiguruikai/76b4c65cfab5be6408703c1de07d8023 to your computer and use it in GitHub Desktop.
Save shiguruikai/76b4c65cfab5be6408703c1de07d8023 to your computer and use it in GitHub Desktop.
JavaでZIPを圧縮・展開するユーティリティクラス。
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
public final class ZipUtils {
private static final char ZIP_ENTRY_FILE_SEPARATOR_CHAR = '/';
private ZipUtils() {}
/** @see #zip(Iterable, Path, boolean, Charset) */
public static void zip(Path srcPath, Path dstPath) throws IOException {
zip(srcPath, dstPath, false);
}
/** @see #zip(Iterable, Path, boolean, Charset) */
public static void zip(Path srcPath, Path dstPath, boolean overwrite) throws IOException {
zip(srcPath, dstPath, overwrite, null);
}
/** @see #zip(Iterable, Path, boolean, Charset) */
public static void zip(Path srcPath, Path dstPath, boolean overwrite, Charset charset)
throws IOException {
zip(List.of(srcPath), dstPath, overwrite, charset);
}
/** @see #zip(Iterable, Path, boolean, Charset) */
public static void zip(Iterable<Path> srcPaths, Path dstPath) throws IOException {
zip(srcPaths, dstPath, false);
}
/** @see #zip(Iterable, Path, boolean, Charset) */
public static void zip(Iterable<Path> srcPaths, Path dstPath, boolean overwrite)
throws IOException {
zip(srcPaths, dstPath, overwrite, null);
}
/** @see #zip(Iterable, OutputStream, Charset) */
public static void zip(Path srcPath, OutputStream dstOut) throws IOException {
zip(srcPath, dstOut, null);
}
/** @see #zip(Iterable, OutputStream, Charset) */
public static void zip(Path srcPath, OutputStream dstOut, Charset charset) throws IOException {
zip(List.of(srcPath), dstOut, charset);
}
/** @see #zip(Iterable, OutputStream, Charset) */
public static void zip(Iterable<Path> srcPaths, OutputStream dstOut) throws IOException {
zip(srcPaths, dstOut, null);
}
/**
* ファイルをZIP形式で圧縮し、指定したパスにファイルを出力します。
*
* @param srcPaths 圧縮対象のパス。ディレクトリを指定した場合、そのサブディレクトリも対象になります。
* @param dstPath 出力するZIP形式のファイル
* @param overwrite trueの場合、既存のファイルを上書きします。falseの場合、既存のファイルがある場合は失敗します。
* @param charset ZIPエントリの名前のエンコードに使用する文字コード。nullの場合、UTF-8が使用されます。
* @throws IOException I/Oエラーが発生した場合
*/
public static void zip(Iterable<Path> srcPaths, Path dstPath, boolean overwrite, Charset charset)
throws IOException {
Objects.requireNonNull(srcPaths);
Objects.requireNonNull(dstPath);
final var dstParentPath = dstPath.toAbsolutePath().normalize().getParent();
if (dstParentPath == null) {
throw new IllegalArgumentException("dstPath must not be root.");
}
Files.createDirectories(dstParentPath);
try (final var dstOut = newOutputStream(dstPath, overwrite)) {
zip(srcPaths, dstOut, charset);
}
}
/**
* ファイルをZIP形式で圧縮し、出力ストリームに書き込みます。
*
* @param srcPaths 圧縮対象のパス。ディレクトリを指定した場合、そのサブディレクトリも対象になります。
* @param dstOut 出力ストリーム
* @param charset ZIPエントリの名前のエンコードに使用する文字コード。nullの場合、UTF-8が使用されます。
* @throws IOException I/Oエラーが発生した場合
*/
public static void zip(Iterable<Path> srcPaths, OutputStream dstOut, Charset charset)
throws IOException {
Objects.requireNonNull(srcPaths);
Objects.requireNonNull(dstOut);
try (final var zos = newZipOutputStream(dstOut, charset)) {
for (final var path : srcPaths) {
addZipEntry(zos, path);
}
}
}
/**
* 指定した{@code ZipOutputStream}に指定したファイルのZIPエントリを追加し、データを書き込みます。
*
* @param zos 書き込み先となるZIP出力ストリーム
* @param srcPath 追加対象のパス。ディレクトリを指定した場合、そのサブディレクトリも対象になります。
* @throws IOException I/Oエラーが発生した場合
*/
public static void addZipEntry(ZipOutputStream zos, Path srcPath) throws IOException {
Objects.requireNonNull(zos);
Objects.requireNonNull(srcPath);
final var srcRealPath = srcPath.toRealPath(LinkOption.NOFOLLOW_LINKS);
final var srcRealParentPath = Objects.requireNonNullElse(srcRealPath.getParent(), srcRealPath);
try (final var paths = Files.walk(srcRealPath)) {
for (final var entryPath : (Iterable<Path>) paths::iterator) {
var entryName = srcRealParentPath.relativize(entryPath).toString();
// if Windows, replace \ to /
if (File.separatorChar != ZIP_ENTRY_FILE_SEPARATOR_CHAR) {
entryName = entryName.replace(File.separatorChar, ZIP_ENTRY_FILE_SEPARATOR_CHAR);
}
final var attrs = Files.readAttributes(entryPath, BasicFileAttributes.class);
if (attrs.isDirectory()) {
entryName += ZIP_ENTRY_FILE_SEPARATOR_CHAR;
}
final var entry = new ZipEntry(entryName);
entry.setCreationTime(attrs.creationTime());
entry.setLastAccessTime(attrs.lastAccessTime());
entry.setLastModifiedTime(attrs.lastModifiedTime());
try {
zos.putNextEntry(entry);
} catch (Exception e) {
final var zipException = new ZipException("zip entry put error: " + entry.getName());
zipException.addSuppressed(e);
throw zipException;
}
if (!attrs.isDirectory()) {
Files.copy(entryPath, zos);
}
zos.closeEntry();
}
}
}
/** @see #unzip(Path, Path, boolean, Charset) */
public static void unzip(Path srcPath, Path dstPath) throws IOException {
unzip(srcPath, dstPath, false);
}
/** @see #unzip(Path, Path, boolean, Charset) */
public static void unzip(Path srcPath, Path dstPath, boolean overwrite) throws IOException {
unzip(srcPath, dstPath, overwrite, null);
}
/**
* ZIP形式のファイルを展開します。
*
* @param srcPath ZIP形式のファイル
* @param dstPath 展開先のディレクトリ
* @param overwrite trueの場合、既存のファイルを上書きします。falseの場合、既存のファイルがある場合は失敗します。
* @param charset ZIPエントリの名前のデコードに使用する文字コード。nullの場合、UTF-8が使用されます。
* @throws IOException I/Oエラーが発生した場合
*/
public static void unzip(Path srcPath, Path dstPath, boolean overwrite, Charset charset)
throws IOException {
Objects.requireNonNull(srcPath);
try (final var srcIn = Files.newInputStream(srcPath)) {
unzip(srcIn, dstPath, overwrite, charset);
}
}
/** @see #unzip(InputStream, Path, boolean, Charset) */
public static void unzip(InputStream srcIn, Path dstPath) throws IOException {
unzip(srcIn, dstPath, false);
}
/** @see #unzip(InputStream, Path, boolean, Charset) */
public static void unzip(InputStream srcIn, Path dstPath, boolean overwrite) throws IOException {
unzip(srcIn, dstPath, overwrite, null);
}
/**
* ZIP形式の入力ストリームを展開します。
*
* @param srcIn ZIP形式の入力ストリーム
* @param dstPath 展開先のディレクトリ
* @param overwrite trueの場合、既存のファイルを上書きします。falseの場合、既存のファイルがある場合は失敗します。
* @param charset ZIPエントリの名前のデコードに使用する文字コード。nullの場合、UTF-8が使用されます。
* @throws IOException I/Oエラーが発生した場合
*/
public static void unzip(InputStream srcIn, Path dstPath, boolean overwrite, Charset charset)
throws IOException {
Objects.requireNonNull(srcIn);
Objects.requireNonNull(dstPath);
final var dstAbsPath = dstPath.toAbsolutePath().normalize();
try (final var zis = newZipInputStream(srcIn, charset)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
final var entryPath = createSafeZipEntryDstPath(entry.getName(), dstAbsPath);
if (entry.isDirectory()) {
Files.createDirectories(entryPath);
} else {
Files.createDirectories(entryPath.getParent());
if (overwrite) {
Files.copy(zis, entryPath, REPLACE_EXISTING);
} else {
Files.copy(zis, entryPath);
}
}
final var attrView = Files.getFileAttributeView(entryPath, BasicFileAttributeView.class);
attrView.setTimes(
entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime());
zis.closeEntry();
}
}
}
/**
* Returns a safe destination path of the zip entry name protected from the "Zip Slip"
* vulnerability.
*
* @param entryName zip entry name
* @param dstParentPath destination directory
* @return safe destination path of the zip entry name
* @throws ZipException if bad zip entry name.
*/
private static Path createSafeZipEntryDstPath(String entryName, Path dstParentPath)
throws ZipException {
final var path = dstParentPath.resolve(entryName).normalize();
if (!path.startsWith(dstParentPath)) {
throw new ZipException("bad zip entry name: " + entryName);
}
return path;
}
private static ZipInputStream newZipInputStream(InputStream in, Charset charset) {
return charset != null ? new ZipInputStream(in, charset) : new ZipInputStream(in);
}
private static OutputStream newOutputStream(Path path, boolean overwrite) throws IOException {
return overwrite
? Files.newOutputStream(path, WRITE, CREATE, TRUNCATE_EXISTING)
: Files.newOutputStream(path, WRITE, CREATE_NEW);
}
private static ZipOutputStream newZipOutputStream(OutputStream out, Charset charset) {
return charset != null ? new ZipOutputStream(out, charset) : new ZipOutputStream(out);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment