Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(native): manage up to two bundles for immediate rollback #130

Merged
merged 4 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,34 @@ class HotUpdater : ReactPackage {

val baseDir = context.getExternalFilesDir(null)
val bundleStoreDir = File(baseDir, "bundle-store")
val zipFilePath = File(bundleStoreDir, "build.zip")
if (!bundleStoreDir.exists()) {
bundleStoreDir.mkdirs()
}

val finalBundleDir = File(bundleStoreDir, bundleId)
if (finalBundleDir.exists()) {
Log.d("HotUpdater", "Bundle for bundleId $bundleId already exists. Using cached bundle.")
val existingIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
if (existingIndexFile != null) {
// Update directory modification time to current time after update
finalBundleDir.setLastModified(System.currentTimeMillis())
setBundleURL(context, existingIndexFile.absolutePath)
cleanupOldBundles(bundleStoreDir)
return true
} else {
finalBundleDir.deleteRecursively()
}
}

// Delete existing folder logic
if (bundleStoreDir.exists()) {
bundleStoreDir.deleteRecursively()
val tempDir = File(baseDir, "bundle-temp")
if (tempDir.exists()) {
tempDir.deleteRecursively()
}
bundleStoreDir.mkdirs()
tempDir.mkdirs()

val tempZipFile = File(tempDir, "build.zip")
val extractedDir = File(tempDir, "extracted")
extractedDir.mkdirs()

val isSuccess =
withContext(Dispatchers.IO) {
Expand All @@ -144,6 +165,7 @@ class HotUpdater : ReactPackage {
downloadUrl.openConnection() as HttpURLConnection
} catch (e: Exception) {
Log.d("HotUpdater", "Failed to open connection: ${e.message}")
tempDir.deleteRecursively()
return@withContext false
}

Expand All @@ -152,12 +174,11 @@ class HotUpdater : ReactPackage {
val totalSize = conn.contentLength
if (totalSize <= 0) {
Log.d("HotUpdater", "Invalid content length: $totalSize")
tempDir.deleteRecursively()
return@withContext false
}

// File download
conn.inputStream.use { input ->
zipFilePath.outputStream().use { output ->
tempZipFile.outputStream().use { output ->
val buffer = ByteArray(8 * 1024)
var bytesRead: Int
var totalRead = 0L
Expand All @@ -178,34 +199,80 @@ class HotUpdater : ReactPackage {
}
} catch (e: Exception) {
Log.d("HotUpdater", "Failed to download data from URL: $zipUrl, Error: ${e.message}")
tempDir.deleteRecursively()
return@withContext false
} finally {
conn.disconnect()
}

// Extract zip
if (!extractZipFileAtPath(zipFilePath.absolutePath, bundleStoreDir.absolutePath)) {
if (!extractZipFileAtPath(tempZipFile.absolutePath, extractedDir.absolutePath)) {
Log.d("HotUpdater", "Failed to extract zip file.")
tempDir.deleteRecursively()
return@withContext false
}
true
}

// Find index.android.bundle
val indexFile = bundleStoreDir.walk().find { it.name == "index.android.bundle" }
if (!isSuccess) {
tempDir.deleteRecursively()
return false
}

if (indexFile != null) {
val bundlePath = indexFile.absolutePath
Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
setBundleURL(context, bundlePath)
} else {
Log.d("HotUpdater", "index.android.bundle not found.")
return@withContext false
}
val indexFileExtracted = extractedDir.walk().find { it.name == "index.android.bundle" }
if (indexFileExtracted == null) {
Log.d("HotUpdater", "index.android.bundle not found in extracted files.")
tempDir.deleteRecursively()
return false
}

Log.d("HotUpdater", "Downloaded and extracted file successfully.")
true
}
// Move (or copy) contents from temp folder to finalBundleDir
if (finalBundleDir.exists()) {
finalBundleDir.deleteRecursively()
}
if (!extractedDir.renameTo(finalBundleDir)) {
extractedDir.copyRecursively(finalBundleDir, overwrite = true)
extractedDir.deleteRecursively()
}

val finalIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
if (finalIndexFile == null) {
Log.d("HotUpdater", "index.android.bundle not found in final directory.")
tempDir.deleteRecursively()
return false
}

// Update final bundle directory modification time to current time after bundle update
finalBundleDir.setLastModified(System.currentTimeMillis())

val bundlePath = finalIndexFile.absolutePath
Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
setBundleURL(context, bundlePath)

// Clean up old bundles in the bundle store to keep only up to 2 bundles
cleanupOldBundles(bundleStoreDir)

return isSuccess
// Clean up temp directory
tempDir.deleteRecursively()

Log.d("HotUpdater", "Downloaded and extracted file successfully.")
return true
}

// Helper function to delete old bundles, keeping only up to 2 bundles in the bundle-store folder
private fun cleanupOldBundles(bundleStoreDir: File) {
// Get list of all directories in bundle-store folder
val bundles = bundleStoreDir.listFiles { file -> file.isDirectory }?.toList() ?: return

// Sort by last modified time in descending order to keep most recently updated bundles at the top
val sortedBundles = bundles.sortedByDescending { it.lastModified() }

// Delete all bundles except the top 2
if (sortedBundles.size > 2) {
sortedBundles.drop(2).forEach { oldBundle ->
Log.d("HotUpdater", "Removing old bundle: ${oldBundle.name}")
oldBundle.deleteRecursively()
}
}
}
}
}
Loading