diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushInvalidPackageException.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushInvalidPackageException.java new file mode 100644 index 0000000..72f124e --- /dev/null +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushInvalidPackageException.java @@ -0,0 +1,7 @@ +package com.microsoft.codepush.react; + +public class CodePushInvalidPackageException extends RuntimeException { + public CodePushInvalidPackageException() { + super("Update is invalid - no files with extension .jsbundle or .bundle were found in the update package."); + } +} diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushPackage.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushPackage.java index d91364a..3c9ebd7 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushPackage.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushPackage.java @@ -2,10 +2,14 @@ package com.microsoft.codepush.react; import android.content.Context; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; +import org.json.JSONException; +import org.json.JSONObject; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; @@ -14,18 +18,23 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; +import java.nio.ByteBuffer; public class CodePushPackage { public final String CODE_PUSH_FOLDER_PREFIX = "CodePush"; - public final String STATUS_FILE = "codepush.json"; - public final String UPDATE_BUNDLE_FILE_NAME = "app.jsbundle"; public final String CURRENT_PACKAGE_KEY = "currentPackage"; - public final String PREVIOUS_PACKAGE_KEY = "previousPackage"; + public final String DIFF_MANIFEST_FILE_NAME = "hotcodepush.json"; + public final int DOWNLOAD_BUFFER_SIZE = 1024 * 256; + public final String DOWNLOAD_FILE_NAME = "download.zip"; + public final String DOWNLOAD_URL_KEY = "downloadUrl"; public final String PACKAGE_FILE_NAME = "app.json"; public final String PACKAGE_HASH_KEY = "packageHash"; - public final String DOWNLOAD_URL_KEY = "downloadUrl"; - public final int DOWNLOAD_BUFFER_SIZE = 1024 * 256; + public final String PREVIOUS_PACKAGE_KEY = "previousPackage"; + public final String RELATIVE_BUNDLE_PATH_KEY = "bundlePath"; + public final String STATUS_FILE = "codepush.json"; + public final String UNZIPPED_FOLDER_NAME = "unzipped"; + public final String UPDATE_BUNDLE_FILE_NAME = "app.jsbundle"; private String documentsDirectory; @@ -33,6 +42,14 @@ public class CodePushPackage { this.documentsDirectory = documentsDirectory; } + public String getDownloadFilePath() { + return CodePushUtils.appendPathComponent(getCodePushPath(), DOWNLOAD_FILE_NAME); + } + + public String getUnzippedFolderPath() { + return CodePushUtils.appendPathComponent(getCodePushPath(), UNZIPPED_FOLDER_NAME); + } + public String getDocumentsDirectory() { return documentsDirectory; } @@ -87,7 +104,13 @@ public class CodePushPackage { return null; } - return CodePushUtils.appendPathComponent(packageFolder, UPDATE_BUNDLE_FILE_NAME); + WritableMap currentPackage = getCurrentPackage(); + String relativeBundlePath = CodePushUtils.tryGetString(currentPackage, RELATIVE_BUNDLE_PATH_KEY); + if (relativeBundlePath == null) { + return CodePushUtils.appendPathComponent(packageFolder, UPDATE_BUNDLE_FILE_NAME); + } else { + return CodePushUtils.appendPathComponent(packageFolder, relativeBundlePath); + } } public String getPackageFolderPath(String packageHash) { @@ -132,7 +155,7 @@ public class CodePushPackage { public void downloadPackage(Context applicationContext, ReadableMap updatePackage, DownloadProgressCallback progressCallback) throws IOException { - String packageFolderPath = getPackageFolderPath(CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY)); + String newPackageFolderPath = getPackageFolderPath(CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY)); String downloadUrlString = CodePushUtils.tryGetString(updatePackage, DOWNLOAD_URL_KEY); URL downloadUrl = null; @@ -140,6 +163,8 @@ public class CodePushPackage { BufferedInputStream bin = null; FileOutputStream fos = null; BufferedOutputStream bout = null; + File downloadFile = null; + boolean isZip = false; try { downloadUrl = new URL(downloadUrlString); @@ -149,23 +174,34 @@ public class CodePushPackage { long receivedBytes = 0; bin = new BufferedInputStream(connection.getInputStream()); - File downloadFolder = new File(packageFolderPath); + File downloadFolder = new File(getCodePushPath()); downloadFolder.mkdirs(); - File downloadFile = new File(downloadFolder, UPDATE_BUNDLE_FILE_NAME); + downloadFile = new File(downloadFolder, DOWNLOAD_FILE_NAME); fos = new FileOutputStream(downloadFile); bout = new BufferedOutputStream(fos, DOWNLOAD_BUFFER_SIZE); byte[] data = new byte[DOWNLOAD_BUFFER_SIZE]; + byte[] header = new byte[4]; + int numBytesRead = 0; while ((numBytesRead = bin.read(data, 0, DOWNLOAD_BUFFER_SIZE)) >= 0) { + if (receivedBytes < 4) { + for (int i = 0; i < numBytesRead; i++) { + int headerOffset = (int)(receivedBytes) + i; + if (headerOffset >= 4) { + break; + } + + header[headerOffset] = data[i]; + } + } + receivedBytes += numBytesRead; bout.write(data, 0, numBytesRead); progressCallback.call(new DownloadProgress(totalBytes, receivedBytes)); } assert totalBytes == receivedBytes; - - String bundlePath = CodePushUtils.appendPathComponent(packageFolderPath, PACKAGE_FILE_NAME); - CodePushUtils.writeReadableMapToFile(updatePackage, bundlePath); + isZip = ByteBuffer.wrap(header).getInt() == 0x504b0304; } catch (MalformedURLException e) { throw new CodePushMalformedDataException(downloadUrlString, e); } finally { @@ -178,6 +214,78 @@ public class CodePushPackage { throw new CodePushUnknownException("Error closing IO resources.", e); } } + + if (isZip) { + System.err.println("THIS IS A ZIP!"); + String unzippedFolderPath = getUnzippedFolderPath(); + CodePushUtils.unzipFile(downloadFile, unzippedFolderPath); + CodePushUtils.deleteFileSilently(downloadFile); + String diffManifestFilePath = CodePushUtils.appendPathComponent(unzippedFolderPath, + DIFF_MANIFEST_FILE_NAME); + File diffManifestFile = new File(unzippedFolderPath, DIFF_MANIFEST_FILE_NAME); + if (diffManifestFile.exists()) { + String currentPackageFolderPath = getCurrentPackageFolderPath(); + CodePushUtils.mergeEntriesInFolder(currentPackageFolderPath, newPackageFolderPath); + WritableMap diffManifest = CodePushUtils.getWritableMapFromFile(diffManifestFilePath); + ReadableArray deletedFiles = diffManifest.getArray("deletedFiles"); + for (int i = 0; i < deletedFiles.size(); i++) { + String fileNameToDelete = deletedFiles.getString(i); + File fileToDelete = new File(newPackageFolderPath, fileNameToDelete); + CodePushUtils.deleteFileSilently(fileToDelete); + } + } + + CodePushUtils.mergeEntriesInFolder(unzippedFolderPath, newPackageFolderPath); + CodePushUtils.deleteFileAtPathSilently(unzippedFolderPath); + String relativeBundlePath = findMainBundleInFolder(newPackageFolderPath); + + if (relativeBundlePath == null) { + throw new CodePushInvalidPackageException(); + } else { + JSONObject updatePackageJSON = CodePushUtils.convertReadableToJsonObject(updatePackage); + try { + updatePackageJSON.put(RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath); + } catch (JSONException e) { + throw new CodePushUnknownException("Unable to set key " + + RELATIVE_BUNDLE_PATH_KEY + " to value " + relativeBundlePath + + " in update package.", e); + } + updatePackage = CodePushUtils.convertJsonObjectToWriteable(updatePackageJSON); + } + } else { + System.err.println("THIS IS NOT A ZIP!"); + // File is not a zip. + File updateBundleFile = new File(newPackageFolderPath, UPDATE_BUNDLE_FILE_NAME); + downloadFile.renameTo(updateBundleFile); + } + + String bundlePath = CodePushUtils.appendPathComponent(newPackageFolderPath, PACKAGE_FILE_NAME); + CodePushUtils.writeReadableMapToFile(updatePackage, bundlePath); + } + + public String findMainBundleInFolder(String folderPath) { + File folder = new File(folderPath); + File[] folderFiles = folder.listFiles(); + for (File file : folderFiles) { + String fullFilePath = CodePushUtils.appendPathComponent(folderPath, file.getName()); + if (file.isDirectory()) { + String mainBundlePathInSubFolder = findMainBundleInFolder(fullFilePath); + if (mainBundlePathInSubFolder != null) { + return CodePushUtils.appendPathComponent(file.getName(), mainBundlePathInSubFolder); + } + } else { + String fileName = file.getName(); + int dotIndex = fileName.lastIndexOf("."); + if (dotIndex >= 0) { + String fileExtension = fileName.substring(dotIndex + 1); + if (fileExtension.equals("bundle") || fileExtension.equals("js") || fileExtension.equals("jsbundle")) { + return fileName; + } + } + } + } + + return null; } public void installPackage(ReadableMap updatePackage) throws IOException { diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java index 295fc37..5aefef4 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java @@ -15,17 +15,23 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.Iterator; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class CodePushUtils { + public static final String CODE_PUSH_TAG = "CodePush"; public static final String REACT_NATIVE_LOG_TAG = "ReactNative"; + public static final int WRITE_BUFFER_SIZE = 1024 * 8; public static String appendPathComponent(String basePath, String appendPathComponent) { return new File(basePath, appendPathComponent).getAbsolutePath(); @@ -265,6 +271,108 @@ public class CodePushUtils { } } + public static void unzipFile(File zipFile, String destination) throws IOException { + FileInputStream fis = null; + BufferedInputStream bis = null; + ZipInputStream zis = null; + try { + fis = new FileInputStream(zipFile); + bis = new BufferedInputStream(fis); + zis = new ZipInputStream(bis); + ZipEntry entry; + + File destinationFolder = new File(destination); + if (!destinationFolder.exists()) { + destinationFolder.mkdirs(); + } + + byte[] buffer = new byte[WRITE_BUFFER_SIZE]; + while ((entry = zis.getNextEntry()) != null) { + String fileName = entry.getName(); + File file = new File(destinationFolder, fileName); + if (entry.isDirectory()) { + file.mkdirs(); + } else { + File parent = file.getParentFile(); + if (!parent.exists()) { + parent.mkdirs(); + } + + FileOutputStream fout = new FileOutputStream(file); + try { + int numBytesRead; + while ((numBytesRead = zis.read(buffer)) != -1) { + fout.write(buffer, 0, numBytesRead); + } + } finally { + fout.close(); + } + } + long time = entry.getTime(); + if (time > 0) { + file.setLastModified(time); + } + } + } finally { + try { + if (zis != null) zis.close(); + if (bis != null) bis.close(); + if (fis != null) fis.close(); + } catch (IOException e) { + throw new CodePushUnknownException("Error closing IO resources.", e); + } + } + } + + public static void mergeEntriesInFolder(String fromPath, String destinationPath) throws IOException { + File fromDir = new File(fromPath); + File destDir = new File(destinationPath); + if (!destDir.exists()) { + destDir.mkdir(); + } + + for (File fromFile : fromDir.listFiles()) { + if (fromFile.isDirectory()) { + mergeEntriesInFolder( + CodePushUtils.appendPathComponent(fromPath, fromFile.getName()), + CodePushUtils.appendPathComponent(destinationPath, fromFile.getName())); + } else { + File destFile = new File(destDir, fromFile.getName()); + FileInputStream fromFileStream = null; + BufferedInputStream fromBufferedStream = null; + FileOutputStream destStream = null; + byte[] buffer = new byte[WRITE_BUFFER_SIZE]; + try { + fromFileStream = new FileInputStream(fromFile); + fromBufferedStream = new BufferedInputStream(fromFileStream); + destStream = new FileOutputStream(destFile); + int bytesRead; + while ((bytesRead = fromBufferedStream.read(buffer)) > 0) { + destStream.write(buffer, 0, bytesRead); + } + } finally { + try { + if (fromFileStream != null) fromFileStream.close(); + if (fromBufferedStream != null) fromBufferedStream.close(); + if (destStream != null) destStream.close(); + } catch (IOException e) { + throw new CodePushUnknownException("Error closing IO resources.", e); + } + } + } + } + } + + public static void deleteFileAtPathSilently(String path) { + deleteFileSilently(new File(path)); + } + + public static void deleteFileSilently(File file) { + if (!file.delete()) { + Log.e(CODE_PUSH_TAG, "Error deleting file " + file.getName()); + } + } + public static void log(String message) { Log.d(REACT_NATIVE_LOG_TAG, "[CodePush] " + message); }