Android 11 + Kotlin:读取 .zip 文件

Posted

技术标签:

【中文标题】Android 11 + Kotlin:读取 .zip 文件【英文标题】:Android 11 + Kotlin: Reading a .zip File 【发布时间】:2021-10-08 15:18:47 【问题描述】:

我有一个使用 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 只知道如何打开由String 或File 指定的.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 的环境中完全不称职,但我想学会不......)

有更好的想法吗?有没有更简洁的方法来实现这一点,而不涉及制作文件的额外副本?

【问题讨论】:

简单的ZipInputStream 怎么样? 基本上,将Uri 的内容读入InputStream,将其转换为ZipInputStream,提取(或其他)并避开临时文件。 谢谢,@Shark。试了一下,效果很好。完美! 如何将 Uri 转换为文件? ***.com/a/8370299/1542667 Yuri:在一般情况下,作为 GetContent() 合同的结果返回的 URI 可能有也可能没有“文件”方案。如果不是,则无法将其表示为文件。 (事实上​​,在快速测试中,在我的示例中,我得到了一个 content:// URI,并且 tofile() 抛出。)我担心任何规范的、面向未来的解决方案都需要与流一起使用。或者,我想,通过 DocumentProvider 的东西...... 【参考方案1】:

从外部来源复制(并冒着被否决的风险),这不是一个完整的答案,但对于评论来说太长了

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 - 您可以对其进行调整并试一试。

编辑: 似乎它也在提取到 Files - 您可以将各个 ByteArrays 存储在某个地方,然后决定以后如何处理它们。但我希望您能大致了解 - 您可以在内存中完成所有这些操作,而无需在两者之间使用磁盘(临时文件或文件)。

但是您的要求有点模糊和不清楚,所以我不知道您要做什么,只是建议尝试的场所/方法

【讨论】:

【参考方案2】:

简单的 ZipInputStream 怎么样? – 鲨鱼

好主意@Shark。

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

ZipInputStream zis = new ZipInputStream(is);

【讨论】:

【参考方案3】:

@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)。在我的特殊情况下——实际上,在消费者不介意按照条目出现的顺序处理条目的任何情况下——没关系。

【讨论】:

如果没有被最后期限所累,我会充满好主意:D【参考方案4】:

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 转换为文件并避免任何复制或附加代码更简单。

【讨论】:

每个文件都有一个 URI,但不是每个 URI 都指向一个文件。 URI 可以指向许多与文件系统中的文件不对应的资源。 Okio 文件系统可以正常工作。基于 Android Uri 的文件系统甚至可以为 okio 做出很好的贡献。这可能是这里的最佳解决方案,避免复制并允许您有效地浏览 zip 文件。

以上是关于Android 11 + Kotlin:读取 .zip 文件的主要内容,如果未能解决你的问题,请参考以下文章

Android kotlin:无法读取类文件

从Kotlin构造函数android中的Parcelable读取List列表

如何在 Kotlin Android 中读取 Apollo Client 响应/GraphQL 响应的响应数据

android 11 kotlin 中的包可见性

携手Flutter和Kotlin探索Android11

使用加速度计读取 android 手机的 x y z 坐标