Last active
February 28, 2022 13:56
-
-
Save shiguruikai/76b4c65cfab5be6408703c1de07d8023 to your computer and use it in GitHub Desktop.
JavaでZIPを圧縮・展開するユーティリティクラス。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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