Android 11 + Kotlin:读取 .zip 文件
Android 11 + Kotlin: Reading a .zip File
我有一个用 Kotlin 目标框架 30+ 编写的 Android 应用程序,所以我在新的 Android 11 file access restrictions 中工作。该应用程序需要能够在共享存储(由用户交互选择)中打开任意 .zip 文件,然后使用该 .zip 文件的内容进行操作。
我正在获取 .zip 文件的 URI,我被引导理解的是规范方式:
val activity = this
val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) {
CoroutineScope(Dispatchers.Main).launch {
if(it != null) doStuffWithZip(activity, it)
...
}
}
getContent.launch("application/zip")
我的问题是Java.util.zip.ZipFile class I'm using only knows how to open a .zip file specified by a String or a File, and I don't have any easy way to get to either of those from a Uri。 (我猜 ZipFile 对象需要实际文件而不是某种流,因为它需要能够查找...)
我目前使用的解决方法是将 Uri 转换为 InputStream,将内容复制到私有存储中的临时文件,并从中创建一个 ZipFile 实例:
private suspend fun <T> withZipFromUri(
context: Context,
uri: Uri, block: suspend (ZipFile) -> T
) : T {
val file = File(context.filesDir, "tempzip.zip")
try {
return withContext(Dispatchers.IO) {
kotlin.runCatching {
context.contentResolver.openInputStream(uri).use { input ->
if (input == null) throw FileNotFoundException("openInputStream failed")
file.outputStream().use { input.copyTo(it) }
}
ZipFile(file, ZipFile.OPEN_READ).use { block.invoke(it) }
}.getOrThrow()
}
} finally {
file.delete()
}
}
那么,我可以这样使用它:
suspend fun doStuffWithZip(context: Context, uri: Uri) {
withZipFromUri(context, uri) { // it: ZipFile
for (entry in it.entries()) {
dbg("entry: ${entry.name}") // or whatever
}
}
}
这有效,并且(在我的特定情况下,有问题的 .zip 文件永远不会超过几 MB)性能合理。
但是,我倾向于把临时文件编程当成绝地无能者最后的避难所,因此我总有一种错失良机的感觉。 (诚然,我 是 在 Android + Kotlin 的背景下完全无能,但我想学会不...)
有什么更好的主意吗?有没有更简洁的方法来实现这个不涉及制作文件的额外副本?
从外部来源复制(并冒着投票被遗忘的风险),这不是一个完整的答案,但评论时间太长
public class ZipFileUnZipExample {
public static void main(String[] args) {
Path source = Paths.get("/home/mkyong/zip/test.zip");
Path target = Paths.get("/home/mkyong/zip/");
try {
unzipFolder(source, target);
System.out.println("Done");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void unzipFolder(Path source, Path target) throws IOException {
// Put the InputStream obtained from Uri here instead of the FileInputStream perhaps?
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(source.toFile()))) {
// list files in zip
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
boolean isDirectory = false;
// example 1.1
// some zip stored files and folders separately
// e.g data/
// data/folder/
// data/folder/file.txt
if (zipEntry.getName().endsWith(File.separator)) {
isDirectory = true;
}
Path newPath = zipSlipProtect(zipEntry, target);
if (isDirectory) {
Files.createDirectories(newPath);
} else {
// example 1.2
// some zip stored file path only, need create parent directories
// e.g data/folder/file.txt
if (newPath.getParent() != null) {
if (Files.notExists(newPath.getParent())) {
Files.createDirectories(newPath.getParent());
}
}
// copy files, nio
Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING);
// copy files, classic
/*try (FileOutputStream fos = new FileOutputStream(newPath.toFile())) {
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}*/
}
zipEntry = zis.getNextEntry();
}
zis.closeEntry();
}
}
// protect zip slip attack
public static Path zipSlipProtect(ZipEntry zipEntry, Path targetDir)
throws IOException {
// test zip slip vulnerability
// Path targetDirResolved = targetDir.resolve("../../" + zipEntry.getName());
Path targetDirResolved = targetDir.resolve(zipEntry.getName());
// make sure normalized file still has targetDir as its prefix
// else throws exception
Path normalizePath = targetDirResolved.normalize();
if (!normalizePath.startsWith(targetDir)) {
throw new IOException("Bad zip entry: " + zipEntry.getName());
}
return normalizePath;
}
}
这显然适用于预先存在的文件;但是,由于您已经从 Uri
读取了一个 InputStream - 您可以调整它并试一试。
编辑:
它似乎也在提取到 File
s - 您 可以 将各个 ByteArrays 存储在某个地方,然后决定稍后如何处理它们。但我希望您能了解总体思路 - 您可以在内存中完成所有这些操作,而不必在两者之间使用磁盘(临时文件或文件)。
不过你的要求有点含糊不清,所以我不知道你想做什么,只是建议 venue/approach 尝试一下
What about a simple ZipInputStream ? –
Shark
好主意@Shark。
InputSteam is = getContentResolver().openInputStream(uri);
ZipInputStream zis = new ZipInputStream(is);
@Shark 有 ZipInputStream。我不确定我是怎么错过的,但我确实错过了。
我的 withZipFromUri()
方法现在更简单更好了:
suspend fun <T> withZipFromUri(
context: Context,
uri: Uri, block: suspend (ZipInputStream) -> T
) : T =
withContext(Dispatchers.IO) {
kotlin.runCatching {
context.contentResolver.openInputStream(uri).use { input ->
if (input == null) throw FileNotFoundException("openInputStream failed")
ZipInputStream(input).use {
block.invoke(it)
}
}
}.getOrThrow()
}
这与旧函数调用不兼容(因为块函数现在将 ZipInputStream
作为参数而不是 ZipFile
)。在我的特殊情况下——实际上,在任何情况下,消费者不介意按条目出现的顺序处理条目——没关系。
Okio (3-Alpha) 有一个 ZipFileSystem https://github.com/square/okio/blob/master/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt
您可以将它与读取该文件内容的自定义文件系统结合起来。它需要相当多的代码,但效率很高。
但我怀疑将 URI 转换为文件并避免任何复制或附加代码更简单。
我有一个用 Kotlin 目标框架 30+ 编写的 Android 应用程序,所以我在新的 Android 11 file access restrictions 中工作。该应用程序需要能够在共享存储(由用户交互选择)中打开任意 .zip 文件,然后使用该 .zip 文件的内容进行操作。
我正在获取 .zip 文件的 URI,我被引导理解的是规范方式:
val activity = this
val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) {
CoroutineScope(Dispatchers.Main).launch {
if(it != null) doStuffWithZip(activity, it)
...
}
}
getContent.launch("application/zip")
我的问题是Java.util.zip.ZipFile class I'm using only knows how to open a .zip file specified by a String or a File, and I don't have any easy way to get to either of those from a Uri。 (我猜 ZipFile 对象需要实际文件而不是某种流,因为它需要能够查找...)
我目前使用的解决方法是将 Uri 转换为 InputStream,将内容复制到私有存储中的临时文件,并从中创建一个 ZipFile 实例:
private suspend fun <T> withZipFromUri(
context: Context,
uri: Uri, block: suspend (ZipFile) -> T
) : T {
val file = File(context.filesDir, "tempzip.zip")
try {
return withContext(Dispatchers.IO) {
kotlin.runCatching {
context.contentResolver.openInputStream(uri).use { input ->
if (input == null) throw FileNotFoundException("openInputStream failed")
file.outputStream().use { input.copyTo(it) }
}
ZipFile(file, ZipFile.OPEN_READ).use { block.invoke(it) }
}.getOrThrow()
}
} finally {
file.delete()
}
}
那么,我可以这样使用它:
suspend fun doStuffWithZip(context: Context, uri: Uri) {
withZipFromUri(context, uri) { // it: ZipFile
for (entry in it.entries()) {
dbg("entry: ${entry.name}") // or whatever
}
}
}
这有效,并且(在我的特定情况下,有问题的 .zip 文件永远不会超过几 MB)性能合理。
但是,我倾向于把临时文件编程当成绝地无能者最后的避难所,因此我总有一种错失良机的感觉。 (诚然,我 是 在 Android + Kotlin 的背景下完全无能,但我想学会不...)
有什么更好的主意吗?有没有更简洁的方法来实现这个不涉及制作文件的额外副本?
从外部来源复制(并冒着投票被遗忘的风险),这不是一个完整的答案,但评论时间太长
public class ZipFileUnZipExample {
public static void main(String[] args) {
Path source = Paths.get("/home/mkyong/zip/test.zip");
Path target = Paths.get("/home/mkyong/zip/");
try {
unzipFolder(source, target);
System.out.println("Done");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void unzipFolder(Path source, Path target) throws IOException {
// Put the InputStream obtained from Uri here instead of the FileInputStream perhaps?
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(source.toFile()))) {
// list files in zip
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
boolean isDirectory = false;
// example 1.1
// some zip stored files and folders separately
// e.g data/
// data/folder/
// data/folder/file.txt
if (zipEntry.getName().endsWith(File.separator)) {
isDirectory = true;
}
Path newPath = zipSlipProtect(zipEntry, target);
if (isDirectory) {
Files.createDirectories(newPath);
} else {
// example 1.2
// some zip stored file path only, need create parent directories
// e.g data/folder/file.txt
if (newPath.getParent() != null) {
if (Files.notExists(newPath.getParent())) {
Files.createDirectories(newPath.getParent());
}
}
// copy files, nio
Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING);
// copy files, classic
/*try (FileOutputStream fos = new FileOutputStream(newPath.toFile())) {
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}*/
}
zipEntry = zis.getNextEntry();
}
zis.closeEntry();
}
}
// protect zip slip attack
public static Path zipSlipProtect(ZipEntry zipEntry, Path targetDir)
throws IOException {
// test zip slip vulnerability
// Path targetDirResolved = targetDir.resolve("../../" + zipEntry.getName());
Path targetDirResolved = targetDir.resolve(zipEntry.getName());
// make sure normalized file still has targetDir as its prefix
// else throws exception
Path normalizePath = targetDirResolved.normalize();
if (!normalizePath.startsWith(targetDir)) {
throw new IOException("Bad zip entry: " + zipEntry.getName());
}
return normalizePath;
}
}
这显然适用于预先存在的文件;但是,由于您已经从 Uri
读取了一个 InputStream - 您可以调整它并试一试。
编辑:
它似乎也在提取到 File
s - 您 可以 将各个 ByteArrays 存储在某个地方,然后决定稍后如何处理它们。但我希望您能了解总体思路 - 您可以在内存中完成所有这些操作,而不必在两者之间使用磁盘(临时文件或文件)。
不过你的要求有点含糊不清,所以我不知道你想做什么,只是建议 venue/approach 尝试一下
What about a simple ZipInputStream ? – Shark
好主意@Shark。
InputSteam is = getContentResolver().openInputStream(uri);
ZipInputStream zis = new ZipInputStream(is);
@Shark 有 ZipInputStream。我不确定我是怎么错过的,但我确实错过了。
我的 withZipFromUri()
方法现在更简单更好了:
suspend fun <T> withZipFromUri(
context: Context,
uri: Uri, block: suspend (ZipInputStream) -> T
) : T =
withContext(Dispatchers.IO) {
kotlin.runCatching {
context.contentResolver.openInputStream(uri).use { input ->
if (input == null) throw FileNotFoundException("openInputStream failed")
ZipInputStream(input).use {
block.invoke(it)
}
}
}.getOrThrow()
}
这与旧函数调用不兼容(因为块函数现在将 ZipInputStream
作为参数而不是 ZipFile
)。在我的特殊情况下——实际上,在任何情况下,消费者不介意按条目出现的顺序处理条目——没关系。
Okio (3-Alpha) 有一个 ZipFileSystem https://github.com/square/okio/blob/master/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt
您可以将它与读取该文件内容的自定义文件系统结合起来。它需要相当多的代码,但效率很高。
但我怀疑将 URI 转换为文件并避免任何复制或附加代码更简单。