安卓11 +Kotlin:阅读.zip文件

8ljdwjyq  于 2023-01-31  发布在  Kotlin
关注(0)|答案(5)|浏览(185)

我有一个用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只知道如何打开由StringFile指定的.zip文件,而我没有任何简单的方法从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的环境中,我 * 是 * 无能者,但我想学会不...)
有更好的主意吗?有没有一种更干净的方法来实现这个,而不涉及到额外的文件副本?

nbysray5

nbysray51#

从外部资源复制(并冒着被遗忘的风险),这不是一个很好的答案,但太长的评论

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读取的输入流-您可以调整这个并给予一下。
编辑:看起来它也在解压缩到File中--你可以将各个ByteArrays存储在某个地方,然后决定以后如何处理它们。但是我希望你能得到一个总体的想法--你可以在内存中完成所有这些工作,而不必在中间使用磁盘(临时文件或文件)。
但是,您的要求有点模糊不清,所以我不知道您想做什么,只是建议了一个尝试的地点/方法

6yoyoihd

6yoyoihd2#

简单的ZipInputStream怎么样?- Shark
好主意@鲨鱼。

InputSteam is = getContentResolver().openInputStream(uri);

ZipInputStream zis = new ZipInputStream(is);
ycggw6v2

ycggw6v23#

@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)。在我的特定情况下--实际上,在任何情况下,只要使用者不介意按照条目出现的顺序处理条目--这就可以了。

sbdsn5lh

sbdsn5lh4#

Okio(3-Alpha)具有一个ZipFileSystem https://github.com/square/okio/blob/master/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt
你可以把它和一个读取文件内容的自定义文件系统结合起来,这需要相当多的代码,但是效率很高。
这是自定义文件系统https://github.com/square/okio/blob/88fa50645946bc42725d2f33e143628e7892be1b/okio/src/jvmMain/kotlin/okio/internal/ResourceFileSystem.kt的示例
但我认为将URI转换为文件并避免任何复制或附加代码会更简单。

ux6nzvsh

ux6nzvsh5#

很容易在Android-Kotlin FileAdapter(使用文件管理器)中检查.zip.rar文件,将以下函数添加到您的代码中:

private fun isZip(name: String): Boolean {
        return name.contains(".zip") || name.contains(".rar")
    }

相关问题