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.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/android/CMakeLists.txt +16 -0
- package/android/build.gradle +70 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/unzip/HybridUnzip.kt +29 -0
- package/android/src/main/java/com/margelo/nitro/unzip/HybridUnzipTask.kt +256 -0
- package/android/src/main/java/com/margelo/nitro/unzip/HybridZipTask.kt +157 -0
- package/ios/HybridUnzip.swift +33 -0
- package/ios/HybridUnzipTask.swift +238 -0
- package/ios/HybridZipTask.swift +266 -0
- package/lib/commonjs/index.js +31 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/specs/Unzip.nitro.js +6 -0
- package/lib/commonjs/specs/Unzip.nitro.js.map +1 -0
- package/lib/module/index.js +27 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/Unzip.nitro.js +4 -0
- package/lib/module/specs/Unzip.nitro.js.map +1 -0
- package/lib/typescript/index.d.ts +24 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/specs/Unzip.nitro.d.ts +170 -0
- package/lib/typescript/specs/Unzip.nitro.d.ts.map +1 -0
- package/nitro.json +26 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroUnzip+autolinking.cmake +85 -0
- package/nitrogen/generated/android/NitroUnzip+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroUnzipOnLoad.cpp +71 -0
- package/nitrogen/generated/android/NitroUnzipOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JFunc_void_UnzipProgress.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_ZipProgress.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridUnzipSpec.cpp +82 -0
- package/nitrogen/generated/android/c++/JHybridUnzipSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridUnzipTaskSpec.cpp +94 -0
- package/nitrogen/generated/android/c++/JHybridUnzipTaskSpec.hpp +68 -0
- package/nitrogen/generated/android/c++/JHybridZipTaskSpec.cpp +94 -0
- package/nitrogen/generated/android/c++/JHybridZipTaskSpec.hpp +68 -0
- package/nitrogen/generated/android/c++/JUnzipProgress.hpp +73 -0
- package/nitrogen/generated/android/c++/JUnzipResult.hpp +73 -0
- package/nitrogen/generated/android/c++/JZipProgress.hpp +69 -0
- package/nitrogen/generated/android/c++/JZipResult.hpp +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/Func_void_UnzipProgress.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/Func_void_ZipProgress.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/HybridUnzipSpec.kt +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/HybridUnzipTaskSpec.kt +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/HybridZipTaskSpec.kt +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/NitroUnzipOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/UnzipProgress.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/UnzipResult.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/ZipProgress.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/unzip/ZipResult.kt +50 -0
- package/nitrogen/generated/ios/NitroUnzip+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroUnzip-Swift-Cxx-Bridge.cpp +107 -0
- package/nitrogen/generated/ios/NitroUnzip-Swift-Cxx-Bridge.hpp +270 -0
- package/nitrogen/generated/ios/NitroUnzip-Swift-Cxx-Umbrella.hpp +68 -0
- package/nitrogen/generated/ios/NitroUnzipAutolinking.mm +49 -0
- package/nitrogen/generated/ios/NitroUnzipAutolinking.swift +50 -0
- package/nitrogen/generated/ios/c++/HybridUnzipSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridUnzipSpecSwift.hpp +112 -0
- package/nitrogen/generated/ios/c++/HybridUnzipTaskSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridUnzipTaskSpecSwift.hpp +104 -0
- package/nitrogen/generated/ios/c++/HybridZipTaskSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridZipTaskSpecSwift.hpp +104 -0
- package/nitrogen/generated/ios/swift/Func_void_UnzipProgress.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_UnzipResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ZipProgress.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ZipResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridUnzipSpec.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridUnzipSpec_cxx.swift +186 -0
- package/nitrogen/generated/ios/swift/HybridUnzipTaskSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridUnzipTaskSpec_cxx.swift +177 -0
- package/nitrogen/generated/ios/swift/HybridZipTaskSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridZipTaskSpec_cxx.swift +177 -0
- package/nitrogen/generated/ios/swift/UnzipProgress.swift +49 -0
- package/nitrogen/generated/ios/swift/UnzipResult.swift +49 -0
- package/nitrogen/generated/ios/swift/ZipProgress.swift +44 -0
- package/nitrogen/generated/ios/swift/ZipResult.swift +49 -0
- package/nitrogen/generated/shared/c++/HybridUnzipSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridUnzipSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/HybridUnzipTaskSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridUnzipTaskSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/HybridZipTaskSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridZipTaskSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/UnzipProgress.hpp +99 -0
- package/nitrogen/generated/shared/c++/UnzipResult.hpp +99 -0
- package/nitrogen/generated/shared/c++/ZipProgress.hpp +95 -0
- package/nitrogen/generated/shared/c++/ZipResult.hpp +99 -0
- package/package.json +165 -0
- package/react-native-nitro-unzip.podspec +24 -0
- package/src/index.ts +36 -0
- 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,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
|
+
}
|