react-native-nitro-unzip 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/android/CMakeLists.txt +16 -0
  4. package/android/build.gradle +70 -0
  5. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  6. package/android/src/main/java/com/margelo/nitro/unzip/HybridUnzip.kt +29 -0
  7. package/android/src/main/java/com/margelo/nitro/unzip/HybridUnzipTask.kt +256 -0
  8. package/android/src/main/java/com/margelo/nitro/unzip/HybridZipTask.kt +157 -0
  9. package/ios/HybridUnzip.swift +33 -0
  10. package/ios/HybridUnzipTask.swift +238 -0
  11. package/ios/HybridZipTask.swift +266 -0
  12. package/lib/commonjs/index.js +31 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/specs/Unzip.nitro.js +6 -0
  15. package/lib/commonjs/specs/Unzip.nitro.js.map +1 -0
  16. package/lib/module/index.js +27 -0
  17. package/lib/module/index.js.map +1 -0
  18. package/lib/module/package.json +1 -0
  19. package/lib/module/specs/Unzip.nitro.js +4 -0
  20. package/lib/module/specs/Unzip.nitro.js.map +1 -0
  21. package/lib/typescript/index.d.ts +24 -0
  22. package/lib/typescript/index.d.ts.map +1 -0
  23. package/lib/typescript/specs/Unzip.nitro.d.ts +170 -0
  24. package/lib/typescript/specs/Unzip.nitro.d.ts.map +1 -0
  25. package/nitro.json +26 -0
  26. package/nitrogen/generated/.gitattributes +1 -0
  27. package/nitrogen/generated/android/NitroUnzip+autolinking.cmake +85 -0
  28. package/nitrogen/generated/android/NitroUnzip+autolinking.gradle +27 -0
  29. package/nitrogen/generated/android/NitroUnzipOnLoad.cpp +71 -0
  30. package/nitrogen/generated/android/NitroUnzipOnLoad.hpp +34 -0
  31. package/nitrogen/generated/android/c++/JFunc_void_UnzipProgress.hpp +77 -0
  32. package/nitrogen/generated/android/c++/JFunc_void_ZipProgress.hpp +77 -0
  33. package/nitrogen/generated/android/c++/JHybridUnzipSpec.cpp +82 -0
  34. package/nitrogen/generated/android/c++/JHybridUnzipSpec.hpp +69 -0
  35. package/nitrogen/generated/android/c++/JHybridUnzipTaskSpec.cpp +94 -0
  36. package/nitrogen/generated/android/c++/JHybridUnzipTaskSpec.hpp +68 -0
  37. package/nitrogen/generated/android/c++/JHybridZipTaskSpec.cpp +94 -0
  38. package/nitrogen/generated/android/c++/JHybridZipTaskSpec.hpp +68 -0
  39. package/nitrogen/generated/android/c++/JUnzipProgress.hpp +73 -0
  40. package/nitrogen/generated/android/c++/JUnzipResult.hpp +73 -0
  41. package/nitrogen/generated/android/c++/JZipProgress.hpp +69 -0
  42. package/nitrogen/generated/android/c++/JZipResult.hpp +73 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/Func_void_UnzipProgress.kt +80 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/Func_void_ZipProgress.kt +80 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/HybridUnzipSpec.kt +69 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/HybridUnzipTaskSpec.kt +73 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/HybridZipTaskSpec.kt +73 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/NitroUnzipOnLoad.kt +35 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/UnzipProgress.kt +50 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/UnzipResult.kt +50 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/ZipProgress.kt +47 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/ZipResult.kt +50 -0
  53. package/nitrogen/generated/ios/NitroUnzip+autolinking.rb +60 -0
  54. package/nitrogen/generated/ios/NitroUnzip-Swift-Cxx-Bridge.cpp +107 -0
  55. package/nitrogen/generated/ios/NitroUnzip-Swift-Cxx-Bridge.hpp +270 -0
  56. package/nitrogen/generated/ios/NitroUnzip-Swift-Cxx-Umbrella.hpp +68 -0
  57. package/nitrogen/generated/ios/NitroUnzipAutolinking.mm +49 -0
  58. package/nitrogen/generated/ios/NitroUnzipAutolinking.swift +50 -0
  59. package/nitrogen/generated/ios/c++/HybridUnzipSpecSwift.cpp +11 -0
  60. package/nitrogen/generated/ios/c++/HybridUnzipSpecSwift.hpp +112 -0
  61. package/nitrogen/generated/ios/c++/HybridUnzipTaskSpecSwift.cpp +11 -0
  62. package/nitrogen/generated/ios/c++/HybridUnzipTaskSpecSwift.hpp +104 -0
  63. package/nitrogen/generated/ios/c++/HybridZipTaskSpecSwift.cpp +11 -0
  64. package/nitrogen/generated/ios/c++/HybridZipTaskSpecSwift.hpp +104 -0
  65. package/nitrogen/generated/ios/swift/Func_void_UnzipProgress.swift +46 -0
  66. package/nitrogen/generated/ios/swift/Func_void_UnzipResult.swift +46 -0
  67. package/nitrogen/generated/ios/swift/Func_void_ZipProgress.swift +46 -0
  68. package/nitrogen/generated/ios/swift/Func_void_ZipResult.swift +46 -0
  69. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  70. package/nitrogen/generated/ios/swift/HybridUnzipSpec.swift +58 -0
  71. package/nitrogen/generated/ios/swift/HybridUnzipSpec_cxx.swift +186 -0
  72. package/nitrogen/generated/ios/swift/HybridUnzipTaskSpec.swift +57 -0
  73. package/nitrogen/generated/ios/swift/HybridUnzipTaskSpec_cxx.swift +177 -0
  74. package/nitrogen/generated/ios/swift/HybridZipTaskSpec.swift +57 -0
  75. package/nitrogen/generated/ios/swift/HybridZipTaskSpec_cxx.swift +177 -0
  76. package/nitrogen/generated/ios/swift/UnzipProgress.swift +49 -0
  77. package/nitrogen/generated/ios/swift/UnzipResult.swift +49 -0
  78. package/nitrogen/generated/ios/swift/ZipProgress.swift +44 -0
  79. package/nitrogen/generated/ios/swift/ZipResult.swift +49 -0
  80. package/nitrogen/generated/shared/c++/HybridUnzipSpec.cpp +24 -0
  81. package/nitrogen/generated/shared/c++/HybridUnzipSpec.hpp +71 -0
  82. package/nitrogen/generated/shared/c++/HybridUnzipTaskSpec.cpp +24 -0
  83. package/nitrogen/generated/shared/c++/HybridUnzipTaskSpec.hpp +71 -0
  84. package/nitrogen/generated/shared/c++/HybridZipTaskSpec.cpp +24 -0
  85. package/nitrogen/generated/shared/c++/HybridZipTaskSpec.hpp +71 -0
  86. package/nitrogen/generated/shared/c++/UnzipProgress.hpp +99 -0
  87. package/nitrogen/generated/shared/c++/UnzipResult.hpp +99 -0
  88. package/nitrogen/generated/shared/c++/ZipProgress.hpp +95 -0
  89. package/nitrogen/generated/shared/c++/ZipResult.hpp +99 -0
  90. package/package.json +165 -0
  91. package/react-native-nitro-unzip.podspec +24 -0
  92. package/src/index.ts +36 -0
  93. package/src/specs/Unzip.nitro.ts +193 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Isaac Rowntree
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # react-native-nitro-unzip
2
+
3
+ High-performance ZIP extraction for React Native, powered by [Nitro Modules](https://nitro.margelo.com/).
4
+
5
+ - **iOS**: SSZipArchive (C-based libz) — ~500 files/sec
6
+ - **Android**: Optimized ZipInputStream with 64KB buffers — ~474 files/sec
7
+ - **Zero bridge overhead** for progress callbacks (JSI-based)
8
+ - **Proper object instances** — each extraction is an `UnzipTask` you can observe and cancel
9
+ - **Concurrent extractions** supported out of the box
10
+ - **iOS background task** management for continued extraction when app is backgrounded
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install react-native-nitro-unzip react-native-nitro-modules
16
+ cd ios && pod install
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```typescript
22
+ import { getUnzip } from 'react-native-nitro-unzip'
23
+
24
+ const unzip = getUnzip()
25
+ const task = unzip.extract('/path/to/archive.zip', '/path/to/output')
26
+
27
+ // Track progress
28
+ task.onProgress((p) => {
29
+ console.log(`${(p.progress * 100).toFixed(0)}% — ${p.extractedFiles}/${p.totalFiles} files`)
30
+ console.log(`Speed: ${p.speed.toFixed(0)} files/sec`)
31
+ })
32
+
33
+ // Await result
34
+ const result = await task.await()
35
+ console.log(`Extracted ${result.extractedFiles} files in ${result.duration}ms`)
36
+
37
+ // Or cancel
38
+ task.cancel()
39
+ ```
40
+
41
+ ## API
42
+
43
+ ### `getUnzip(): Unzip`
44
+
45
+ Creates an `Unzip` factory instance.
46
+
47
+ ### `Unzip.extract(zipPath, destinationPath): UnzipTask`
48
+
49
+ Starts extracting a ZIP archive. Returns an `UnzipTask` instance immediately.
50
+
51
+ - `zipPath` — absolute path to the ZIP file (`file://` URIs accepted)
52
+ - `destinationPath` — absolute path to extract into (created if missing)
53
+
54
+ ### `UnzipTask`
55
+
56
+ | Property/Method | Type | Description |
57
+ |---|---|---|
58
+ | `taskId` | `string` | Unique identifier for this extraction |
59
+ | `onProgress(callback)` | `(progress: UnzipProgress) => void` | Register a progress callback (throttled to ~1/sec) |
60
+ | `cancel()` | `void` | Cancel this extraction |
61
+ | `await()` | `Promise<UnzipResult>` | Await the extraction result |
62
+
63
+ ### `UnzipProgress`
64
+
65
+ | Field | Type | Description |
66
+ |---|---|---|
67
+ | `extractedFiles` | `number` | Files extracted so far |
68
+ | `totalFiles` | `number` | Total files in archive |
69
+ | `progress` | `number` | 0.0 to 1.0 |
70
+ | `speed` | `number` | Files per second |
71
+ | `processedBytes` | `number` | Bytes processed |
72
+
73
+ ### `UnzipResult`
74
+
75
+ | Field | Type | Description |
76
+ |---|---|---|
77
+ | `success` | `boolean` | Whether extraction completed |
78
+ | `extractedFiles` | `number` | Total files extracted |
79
+ | `duration` | `number` | Duration in milliseconds |
80
+ | `averageSpeed` | `number` | Average files per second |
81
+ | `totalBytes` | `number` | Total bytes extracted |
82
+
83
+ ## Performance
84
+
85
+ Benchmarked on a 350MB archive with 10,432 small files (map tiles):
86
+
87
+ | Platform | Speed | Time |
88
+ |---|---|---|
89
+ | iOS (iPhone) | ~500 files/sec | ~20s |
90
+ | Android | ~474 files/sec | ~22s |
91
+
92
+ ### Why it's fast
93
+
94
+ - **iOS**: SSZipArchive uses C-based libz decompression with streaming extraction
95
+ - **Android**: 64KB I/O buffers (8x default), batch directory creation, buffered streams
96
+ - **Both**: Progress callbacks go through JSI (no bridge serialization), throttled to 1/sec
97
+
98
+ ## Requirements
99
+
100
+ - React Native 0.75+
101
+ - Nitro Modules 0.34+
102
+ - iOS 13+
103
+ - Android SDK 21+
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,16 @@
1
+ cmake_minimum_required(VERSION 3.9.0)
2
+ project(NitroUnzip)
3
+
4
+ # C++ 20 required by Nitro
5
+ set(CMAKE_CXX_STANDARD 20)
6
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
7
+
8
+ # Define the C++ library
9
+ add_library(
10
+ NitroUnzip
11
+ SHARED
12
+ src/main/cpp/cpp-adapter.cpp
13
+ )
14
+
15
+ # Include Nitrogen generated sources + link NitroModules
16
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroUnzip+autolinking.cmake)
@@ -0,0 +1,70 @@
1
+ buildscript {
2
+ ext.safeExtGet = {prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+
6
+ repositories {
7
+ mavenCentral()
8
+ google()
9
+ }
10
+ dependencies {
11
+ classpath("com.android.tools.build:gradle:8.2.1")
12
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
13
+ }
14
+ }
15
+
16
+ apply plugin: 'com.android.library'
17
+ apply plugin: 'kotlin-android'
18
+
19
+ // Nitrogen autolinking — adds generated Kotlin sources
20
+ apply from: '../nitrogen/generated/android/NitroUnzip+autolinking.gradle'
21
+
22
+ android {
23
+ namespace "com.margelo.nitro.unzip"
24
+ compileSdkVersion safeExtGet("compileSdkVersion", 35)
25
+
26
+ defaultConfig {
27
+ minSdkVersion safeExtGet("minSdkVersion", 21)
28
+ targetSdkVersion safeExtGet("targetSdkVersion", 35)
29
+
30
+ externalNativeBuild {
31
+ cmake {
32
+ cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all"
33
+ abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
34
+ }
35
+ }
36
+ }
37
+
38
+ compileOptions {
39
+ sourceCompatibility JavaVersion.VERSION_17
40
+ targetCompatibility JavaVersion.VERSION_17
41
+ }
42
+ kotlinOptions {
43
+ jvmTarget = '17'
44
+ }
45
+
46
+ sourceSets {
47
+ main {
48
+ java.srcDirs += ['src/main/java']
49
+ }
50
+ }
51
+
52
+ externalNativeBuild {
53
+ cmake {
54
+ path "CMakeLists.txt"
55
+ }
56
+ }
57
+ }
58
+
59
+ repositories {
60
+ mavenCentral()
61
+ google()
62
+ }
63
+
64
+ dependencies {
65
+ implementation "com.facebook.react:react-android:+"
66
+ implementation "com.facebook.react:react-native:+"
67
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
68
+ // zip4j — needed for password-protected zip/unzip (java.util.zip has no password API)
69
+ implementation "net.lingala.zip4j:zip4j:2.11.5"
70
+ }
@@ -0,0 +1,6 @@
1
+ #include <jni.h>
2
+ #include "NitroUnzipOnLoad.hpp"
3
+
4
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
5
+ return margelo::nitro::unzip::initialize(vm);
6
+ }
@@ -0,0 +1,29 @@
1
+ package com.margelo.nitro.unzip
2
+
3
+ import androidx.annotation.Keep
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+
6
+ /**
7
+ * Factory HybridObject that creates extraction and compression tasks.
8
+ */
9
+ @DoNotStrip
10
+ @Keep
11
+ class HybridUnzip : HybridUnzipSpec() {
12
+ override val memorySize: Long = 0L
13
+
14
+ override fun extract(zipPath: String, destinationPath: String): HybridUnzipTaskSpec {
15
+ return HybridUnzipTask(zipPath, destinationPath)
16
+ }
17
+
18
+ override fun extractWithPassword(zipPath: String, destinationPath: String, password: String): HybridUnzipTaskSpec {
19
+ return HybridUnzipTask(zipPath, destinationPath, password)
20
+ }
21
+
22
+ override fun zip(sourcePath: String, destinationZipPath: String): HybridZipTaskSpec {
23
+ return HybridZipTask(sourcePath, destinationZipPath)
24
+ }
25
+
26
+ override fun zipWithPassword(sourcePath: String, destinationZipPath: String, password: String): HybridZipTaskSpec {
27
+ return HybridZipTask(sourcePath, destinationZipPath, password)
28
+ }
29
+ }
@@ -0,0 +1,256 @@
1
+ package com.margelo.nitro.unzip
2
+
3
+ import androidx.annotation.Keep
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+ import com.margelo.nitro.core.Promise
6
+ import kotlinx.coroutines.*
7
+ import java.io.BufferedInputStream
8
+ import java.io.BufferedOutputStream
9
+ import java.io.File
10
+ import java.io.FileOutputStream
11
+ import java.util.zip.ZipInputStream
12
+
13
+ /**
14
+ * A single extraction operation as a proper HybridObject instance.
15
+ *
16
+ * Performance (350MB archive, 10k+ files):
17
+ * - Speed: ~474 files/second
18
+ * - Optimizations: 64KB buffers, batch directory creation, buffered streams
19
+ * - Coroutine-based with cooperative cancellation
20
+ *
21
+ * When a password is provided, uses zip4j for decryption. Otherwise uses
22
+ * the fast built-in ZipInputStream path (no extra dependency overhead).
23
+ */
24
+ @DoNotStrip
25
+ @Keep
26
+ class HybridUnzipTask(
27
+ private val zipPath: String,
28
+ private val destinationPath: String,
29
+ private val password: String? = null
30
+ ) : HybridUnzipTaskSpec() {
31
+
32
+ override val memorySize: Long = 0L
33
+
34
+ override val taskId: String = "unzip_${System.nanoTime()}_${(Math.random() * 1e9).toLong()}"
35
+
36
+ private var progressCallback: ((UnzipProgress) -> Unit)? = null
37
+ private var extractionJob: Job? = null
38
+
39
+ @Volatile
40
+ private var shouldCancel = false
41
+
42
+ override fun onProgress(callback: (progress: UnzipProgress) -> Unit) {
43
+ progressCallback = callback
44
+ }
45
+
46
+ override fun cancel() {
47
+ shouldCancel = true
48
+ extractionJob?.cancel()
49
+ }
50
+
51
+ override fun await(): Promise<UnzipResult> {
52
+ return Promise.async { resolve, reject ->
53
+ try {
54
+ val result = if (password != null) extractWithPassword() else extract()
55
+ resolve(result)
56
+ } catch (e: CancellationException) {
57
+ reject(Exception("Extraction cancelled"))
58
+ } catch (e: Exception) {
59
+ reject(e)
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Fast path — built-in ZipInputStream, no password support.
66
+ */
67
+ private suspend fun extract(): UnzipResult = withContext(Dispatchers.IO) {
68
+ extractionJob = coroutineContext[Job]
69
+
70
+ val startTime = System.currentTimeMillis()
71
+
72
+ val cleanZip = zipPath.replace("file://", "")
73
+ val cleanDest = destinationPath.replace("file://", "")
74
+
75
+ val destDir = File(cleanDest)
76
+ if (!destDir.exists()) {
77
+ destDir.mkdirs()
78
+ }
79
+
80
+ val sourceFile = File(cleanZip)
81
+ if (!sourceFile.exists()) {
82
+ throw Exception("Source ZIP file not found: $cleanZip")
83
+ }
84
+
85
+ // --- Pass 1: collect directories ---
86
+ val directoriesToCreate = hashSetOf<String>()
87
+ val fileEntries = mutableListOf<String>()
88
+
89
+ ZipInputStream(BufferedInputStream(sourceFile.inputStream(), BUFFER_SIZE)).use { zis ->
90
+ var entry = zis.nextEntry
91
+ while (entry != null) {
92
+ if (entry.isDirectory) {
93
+ directoriesToCreate.add(entry.name)
94
+ } else {
95
+ fileEntries.add(entry.name)
96
+ val parent = File(entry.name).parent
97
+ if (parent != null) {
98
+ directoriesToCreate.add(parent)
99
+ }
100
+ }
101
+ entry = zis.nextEntry
102
+ }
103
+ }
104
+
105
+ // Batch create all directories
106
+ directoriesToCreate.sorted().forEach { dirPath ->
107
+ File(destDir, dirPath).mkdirs()
108
+ }
109
+
110
+ // --- Pass 2: extract files ---
111
+ var extractedCount = 0
112
+ var lastProgressUpdate = System.currentTimeMillis()
113
+ val totalEntries = fileEntries.size
114
+
115
+ ZipInputStream(BufferedInputStream(sourceFile.inputStream(), BUFFER_SIZE)).use { zis ->
116
+ var entry = zis.nextEntry
117
+
118
+ while (entry != null && isActive && !shouldCancel) {
119
+ if (!entry.isDirectory) {
120
+ val entryFile = File(destDir, entry.name)
121
+
122
+ BufferedOutputStream(FileOutputStream(entryFile), BUFFER_SIZE).use { output ->
123
+ val buffer = ByteArray(BUFFER_SIZE)
124
+ var bytesRead: Int
125
+ while (zis.read(buffer).also { bytesRead = it } != -1) {
126
+ output.write(buffer, 0, bytesRead)
127
+ }
128
+ }
129
+
130
+ extractedCount++
131
+
132
+ // Throttle progress updates
133
+ val now = System.currentTimeMillis()
134
+ val shouldUpdate = (now - lastProgressUpdate >= PROGRESS_THROTTLE_MS)
135
+ || (extractedCount == totalEntries)
136
+ || (extractedCount == 1)
137
+
138
+ if (shouldUpdate) {
139
+ val progress = if (totalEntries > 0) extractedCount.toDouble() / totalEntries else 0.0
140
+ val elapsed = (now - startTime) / 1000.0
141
+ val speed = if (elapsed > 0) extractedCount / elapsed else 0.0
142
+
143
+ progressCallback?.invoke(
144
+ UnzipProgress(
145
+ extractedFiles = extractedCount.toDouble(),
146
+ totalFiles = totalEntries.toDouble(),
147
+ progress = progress,
148
+ speed = speed,
149
+ processedBytes = 0.0
150
+ )
151
+ )
152
+ lastProgressUpdate = now
153
+ }
154
+ }
155
+
156
+ entry = zis.nextEntry
157
+ }
158
+ }
159
+
160
+ if (shouldCancel) {
161
+ throw CancellationException("Extraction cancelled")
162
+ }
163
+
164
+ val durationMs = (System.currentTimeMillis() - startTime).toDouble()
165
+ val avgSpeed = if (durationMs > 0) extractedCount / (durationMs / 1000.0) else 0.0
166
+
167
+ UnzipResult(
168
+ success = true,
169
+ extractedFiles = extractedCount.toDouble(),
170
+ duration = durationMs,
171
+ averageSpeed = avgSpeed,
172
+ totalBytes = 0.0
173
+ )
174
+ }
175
+
176
+ /**
177
+ * Password-protected extraction using zip4j.
178
+ */
179
+ private suspend fun extractWithPassword(): UnzipResult = withContext(Dispatchers.IO) {
180
+ extractionJob = coroutineContext[Job]
181
+
182
+ val startTime = System.currentTimeMillis()
183
+
184
+ val cleanZip = zipPath.replace("file://", "")
185
+ val cleanDest = destinationPath.replace("file://", "")
186
+
187
+ val destDir = File(cleanDest)
188
+ if (!destDir.exists()) {
189
+ destDir.mkdirs()
190
+ }
191
+
192
+ val sourceFile = File(cleanZip)
193
+ if (!sourceFile.exists()) {
194
+ throw Exception("Source ZIP file not found: $cleanZip")
195
+ }
196
+
197
+ val zipFile = net.lingala.zip4j.ZipFile(sourceFile)
198
+ zipFile.setPassword(password!!.toCharArray())
199
+
200
+ val fileHeaders = zipFile.fileHeaders
201
+ val totalEntries = fileHeaders.size
202
+ var extractedCount = 0
203
+ var lastProgressUpdate = System.currentTimeMillis()
204
+
205
+ for (header in fileHeaders) {
206
+ if (!isActive || shouldCancel) break
207
+
208
+ if (!header.isDirectory) {
209
+ zipFile.extractFile(header, cleanDest)
210
+ extractedCount++
211
+
212
+ val now = System.currentTimeMillis()
213
+ val shouldUpdate = (now - lastProgressUpdate >= PROGRESS_THROTTLE_MS)
214
+ || (extractedCount == totalEntries)
215
+ || (extractedCount == 1)
216
+
217
+ if (shouldUpdate) {
218
+ val progress = if (totalEntries > 0) extractedCount.toDouble() / totalEntries else 0.0
219
+ val elapsed = (now - startTime) / 1000.0
220
+ val speed = if (elapsed > 0) extractedCount / elapsed else 0.0
221
+
222
+ progressCallback?.invoke(
223
+ UnzipProgress(
224
+ extractedFiles = extractedCount.toDouble(),
225
+ totalFiles = totalEntries.toDouble(),
226
+ progress = progress,
227
+ speed = speed,
228
+ processedBytes = 0.0
229
+ )
230
+ )
231
+ lastProgressUpdate = now
232
+ }
233
+ }
234
+ }
235
+
236
+ if (shouldCancel) {
237
+ throw CancellationException("Extraction cancelled")
238
+ }
239
+
240
+ val durationMs = (System.currentTimeMillis() - startTime).toDouble()
241
+ val avgSpeed = if (durationMs > 0) extractedCount / (durationMs / 1000.0) else 0.0
242
+
243
+ UnzipResult(
244
+ success = true,
245
+ extractedFiles = extractedCount.toDouble(),
246
+ duration = durationMs,
247
+ averageSpeed = avgSpeed,
248
+ totalBytes = 0.0
249
+ )
250
+ }
251
+
252
+ companion object {
253
+ private const val BUFFER_SIZE = 65536
254
+ private const val PROGRESS_THROTTLE_MS = 1000L
255
+ }
256
+ }
@@ -0,0 +1,157 @@
1
+ package com.margelo.nitro.unzip
2
+
3
+ import androidx.annotation.Keep
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+ import com.margelo.nitro.core.Promise
6
+ import kotlinx.coroutines.*
7
+ import net.lingala.zip4j.ZipFile
8
+ import net.lingala.zip4j.model.ZipParameters
9
+ import net.lingala.zip4j.model.enums.AesKeyStrength
10
+ import net.lingala.zip4j.model.enums.CompressionLevel
11
+ import net.lingala.zip4j.model.enums.CompressionMethod
12
+ import net.lingala.zip4j.model.enums.EncryptionMethod
13
+ import java.io.File
14
+
15
+ /**
16
+ * A single zip creation operation as a proper HybridObject instance.
17
+ *
18
+ * Uses zip4j for both standard and password-protected zip creation.
19
+ * AES-256 encryption when password is provided.
20
+ */
21
+ @DoNotStrip
22
+ @Keep
23
+ class HybridZipTask(
24
+ private val sourcePath: String,
25
+ private val destinationZipPath: String,
26
+ private val password: String? = null
27
+ ) : HybridZipTaskSpec() {
28
+
29
+ override val memorySize: Long = 0L
30
+
31
+ override val taskId: String = "zip_${System.nanoTime()}_${(Math.random() * 1e9).toLong()}"
32
+
33
+ private var progressCallback: ((ZipProgress) -> Unit)? = null
34
+ private var compressionJob: Job? = null
35
+
36
+ @Volatile
37
+ private var shouldCancel = false
38
+
39
+ override fun onProgress(callback: (progress: ZipProgress) -> Unit) {
40
+ progressCallback = callback
41
+ }
42
+
43
+ override fun cancel() {
44
+ shouldCancel = true
45
+ compressionJob?.cancel()
46
+ }
47
+
48
+ override fun await(): Promise<ZipResult> {
49
+ return Promise.async { resolve, reject ->
50
+ try {
51
+ val result = compress()
52
+ resolve(result)
53
+ } catch (e: CancellationException) {
54
+ reject(Exception("Zip creation cancelled"))
55
+ } catch (e: Exception) {
56
+ reject(e)
57
+ }
58
+ }
59
+ }
60
+
61
+ private suspend fun compress(): ZipResult = withContext(Dispatchers.IO) {
62
+ compressionJob = coroutineContext[Job]
63
+
64
+ val startTime = System.currentTimeMillis()
65
+
66
+ val cleanSource = sourcePath.replace("file://", "")
67
+ val cleanDest = destinationZipPath.replace("file://", "")
68
+
69
+ val sourceDir = File(cleanSource)
70
+ if (!sourceDir.exists()) {
71
+ throw Exception("Source path not found: $cleanSource")
72
+ }
73
+
74
+ // Collect all files for progress tracking
75
+ val allFiles = mutableListOf<File>()
76
+ sourceDir.walkTopDown().forEach { file ->
77
+ if (file.isFile) {
78
+ allFiles.add(file)
79
+ }
80
+ }
81
+
82
+ val totalFiles = allFiles.size
83
+ var compressedCount = 0
84
+ var lastProgressUpdate = System.currentTimeMillis()
85
+
86
+ val zipFile = ZipFile(cleanDest)
87
+
88
+ val zipParams = ZipParameters().apply {
89
+ compressionMethod = CompressionMethod.DEFLATE
90
+ compressionLevel = CompressionLevel.NORMAL
91
+ }
92
+
93
+ if (password != null) {
94
+ zipFile.setPassword(password.toCharArray())
95
+ zipParams.isEncryptFiles = true
96
+ zipParams.encryptionMethod = EncryptionMethod.AES
97
+ zipParams.aesKeyStrength = AesKeyStrength.KEY_STRENGTH_256
98
+ }
99
+
100
+ // Add files one by one for progress tracking
101
+ for (file in allFiles) {
102
+ if (!isActive || shouldCancel) break
103
+
104
+ val relativePath = file.relativeTo(sourceDir).parent
105
+ val params = ZipParameters(zipParams)
106
+ if (relativePath != null) {
107
+ params.rootFolderNameInZip = relativePath
108
+ }
109
+
110
+ zipFile.addFile(file, params)
111
+ compressedCount++
112
+
113
+ val now = System.currentTimeMillis()
114
+ val shouldUpdate = (now - lastProgressUpdate >= PROGRESS_THROTTLE_MS)
115
+ || (compressedCount == totalFiles)
116
+ || (compressedCount == 1)
117
+
118
+ if (shouldUpdate) {
119
+ val progress = if (totalFiles > 0) compressedCount.toDouble() / totalFiles else 0.0
120
+ val elapsed = (now - startTime) / 1000.0
121
+ val speed = if (elapsed > 0) compressedCount / elapsed else 0.0
122
+
123
+ progressCallback?.invoke(
124
+ ZipProgress(
125
+ compressedFiles = compressedCount.toDouble(),
126
+ totalFiles = totalFiles.toDouble(),
127
+ progress = progress,
128
+ speed = speed
129
+ )
130
+ )
131
+ lastProgressUpdate = now
132
+ }
133
+ }
134
+
135
+ if (shouldCancel) {
136
+ // Clean up partial zip file
137
+ File(cleanDest).delete()
138
+ throw CancellationException("Zip creation cancelled")
139
+ }
140
+
141
+ val durationMs = (System.currentTimeMillis() - startTime).toDouble()
142
+ val avgSpeed = if (durationMs > 0) compressedCount / (durationMs / 1000.0) else 0.0
143
+ val totalBytes = File(cleanDest).length().toDouble()
144
+
145
+ ZipResult(
146
+ success = true,
147
+ compressedFiles = compressedCount.toDouble(),
148
+ duration = durationMs,
149
+ averageSpeed = avgSpeed,
150
+ totalBytes = totalBytes
151
+ )
152
+ }
153
+
154
+ companion object {
155
+ private const val PROGRESS_THROTTLE_MS = 1000L
156
+ }
157
+ }
@@ -0,0 +1,33 @@
1
+ import Foundation
2
+ import NitroModules
3
+
4
+ /**
5
+ * Factory HybridObject that creates extraction and compression tasks.
6
+ *
7
+ * Usage from JS:
8
+ * ```js
9
+ * const unzip = NitroModules.createHybridObject('Unzip')
10
+ * const task = unzip.extract(zipPath, destPath)
11
+ * const zipTask = unzip.zip(sourcePath, destZipPath)
12
+ * ```
13
+ */
14
+ class HybridUnzip: HybridUnzipSpec {
15
+ var hybridContext = margelo.nitro.HybridContext()
16
+ var memorySize: Int { return getSizeOf(self) }
17
+
18
+ func extract(zipPath: String, destinationPath: String) throws -> any HybridUnzipTaskSpec {
19
+ return HybridUnzipTask(zipPath: zipPath, destinationPath: destinationPath)
20
+ }
21
+
22
+ func extractWithPassword(zipPath: String, destinationPath: String, password: String) throws -> any HybridUnzipTaskSpec {
23
+ return HybridUnzipTask(zipPath: zipPath, destinationPath: destinationPath, password: password)
24
+ }
25
+
26
+ func zip(sourcePath: String, destinationZipPath: String) throws -> any HybridZipTaskSpec {
27
+ return HybridZipTask(sourcePath: sourcePath, destinationZipPath: destinationZipPath)
28
+ }
29
+
30
+ func zipWithPassword(sourcePath: String, destinationZipPath: String, password: String) throws -> any HybridZipTaskSpec {
31
+ return HybridZipTask(sourcePath: sourcePath, destinationZipPath: destinationZipPath, password: password)
32
+ }
33
+ }