package com.couchbase.libcouch; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.zip.GZIPInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.utils.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.util.Log; public class CouchInstaller { public static String appNamespace; final static String TAG = "CouchDB"; public static String dataPath() { return "/data/data/" + appNamespace; } public static String externalPath() { return Environment.getExternalStorageDirectory() + "/Android/data/" + appNamespace; } public static String indexFile() { return dataPath() + "/installedfiles.index"; } public static void doInstall(String url, String pkg, Handler handler, CouchService service) throws IOException { if(!checkInstalled(pkg)) { /* * WARNING: the following two stanzas delete any previously installed * CouchDB and Erlang binaries stored in the app data space. It isn't * usually possible (in a non-rooted or emulated environment) to hurt * other data directories but one must be especially careful when carrying * out this sort of operation on external storage where there are no ways * of protecting ourselves from wiping the entire SD card with a typo. */ File couchDir = new File(dataPath() + "/couchdb"); if (couchDir.exists()) { deleteDirectory(couchDir); } File erlangDir = new File(dataPath() + "/erlang"); if (erlangDir.exists()) { deleteDirectory(erlangDir); } installPackage(url, pkg, handler, service); } Message done = Message.obtain(); done.what = CouchService.COMPLETE; handler.sendMessage(done); } /* * This fetches a given package from amazon and tarbombs it to the filsystem */ private static void installPackage(String baseUrl, String pkg, Handler handler, CouchService service) throws IOException { Log.v(TAG, "Installing " + pkg); // Later used initialization of /data/data/... ArrayList<String> installedfiles = new ArrayList<String>(); ArrayList<String> allInstalledFiles = new ArrayList<String>(); Map<String, Integer> allInstalledFileModes = new HashMap<String, Integer>(); Map<String, String> allInstalledFileTypes = new HashMap<String, String>(); Map<String, String> allInstalledLinks = new HashMap<String, String>(); InputStream instream = null; // If no URL is provided, load tarball from assets. if (baseUrl == null) { // XXX Stupid android 2.1 bug // XXX Cannot load compressed assets >1M and // XXX most files are automatically compressed, // XXX Certain files are NOT auto compressed (eg. jpg). instream = service.getAssets().open(pkg + ".tgz" + ".jpg"); } else { HttpClient pkgHttpClient = new DefaultHttpClient(); HttpGet tgzrequest = new HttpGet(baseUrl + pkg + ".tgz"); HttpResponse response = pkgHttpClient.execute(tgzrequest); StatusLine status = response.getStatusLine(); Log.d(TAG, "Request returned status " + status); if (status.getStatusCode() == 200) { HttpEntity entity = response.getEntity(); instream = entity.getContent(); } else { throw new IOException(); } } // Ensure /sdcard/Android/data/com.my.app/db exists File externalPath = new File(externalPath() + "/db/"); if (!externalPath.exists()) { externalPath.mkdirs(); } TarArchiveInputStream tarstream = new TarArchiveInputStream( new GZIPInputStream(instream)); TarArchiveEntry e = null; int files = 0; float filesInArchive = 0; float filesUnpacked = 0; while ((e = tarstream.getNextTarEntry()) != null) { String fullName = dataPath() + "/" + e.getName(); // Obtain count of files in this archive so that we can indicate install progress if (filesInArchive == 0 && e.getName().startsWith("filecount")) { String[] count = e.getName().split("\\."); filesInArchive = Integer.valueOf(count[1]); continue; } if (e.isDirectory()) { File f = new File(fullName); if (!f.exists() && !new File(fullName).mkdirs()) { throw new IOException("Unable to create directory: " + fullName); } Log.v(TAG, "MKDIR: " + fullName); allInstalledFiles.add(fullName); allInstalledFileModes.put(fullName, e.getMode()); allInstalledFileTypes.put(fullName, "d"); } else if (!"".equals(e.getLinkName())) { Log.v(TAG, "LINK: " + fullName + " -> " + e.getLinkName()); Runtime.getRuntime().exec(new String[] { "ln", "-s", fullName, e.getLinkName() }); installedfiles.add(fullName); allInstalledFiles.add(fullName); allInstalledLinks.put(fullName, e.getLinkName()); allInstalledFileModes.put(fullName, e.getMode()); allInstalledFileTypes.put(fullName, "l"); } else { File target = new File(fullName); if(target.getParent() != null) { new File(target.getParent()).mkdirs(); } Log.v(TAG, "Extracting " + fullName); IOUtils.copy(tarstream, new FileOutputStream(target)); installedfiles.add(fullName); allInstalledFiles.add(fullName); allInstalledFileModes.put(fullName, e.getMode()); allInstalledFileTypes.put(fullName, "f"); } // getMode: 420 (644), 493 (755), 509 (775), 511 (link 775) //Log.v(TAG, "File mode is " + e.getMode()); //TODO: Set to actual tar perms. Runtime.getRuntime().exec("chmod 755 " + fullName); // This tells the ui how much progress has been made files++; Message progress = new Message(); progress.arg1 = (int) ++filesUnpacked; progress.arg2 = (int) filesInArchive; progress.what = CouchService.PROGRESS; handler.sendMessage(progress); } tarstream.close(); instream.close(); FileWriter iLOWriter = new FileWriter(dataPath() + "/" + pkg + ".installedfiles"); for (String file : installedfiles) { iLOWriter.write(file+"\n"); } iLOWriter.close(); /* * Write out full list of all installed files + file modes (the data in this file * only represents the most recently installed release) */ iLOWriter = new FileWriter(indexFile()); for (String file : allInstalledFiles) { iLOWriter.write( allInstalledFileTypes.get(file).toString() + " " + allInstalledFileModes.get(file).toString() + " " + file + " " + allInstalledLinks.get(file) + "\n"); } iLOWriter.close(); String[][] replacements = new String[][]{ {"%app_name%", CouchInstaller.appNamespace}, {"%sdk_int%", Integer.toString(android.os.Build.VERSION.SDK_INT)} }; replace(CouchInstaller.dataPath() + "/erlang/erts-5.7.5/bin/start", replacements); replace(CouchInstaller.dataPath() + "/erlang/erts-5.7.5/bin/erl", replacements); replace(CouchInstaller.dataPath() + "/erlang/bin/start", replacements); replace(CouchInstaller.dataPath() + "/erlang/bin/erl", replacements); replace(CouchInstaller.dataPath() + "/couchdb/lib/couchdb/erlang/lib/couch-1.0.2/ebin/couch.app", replacements); replace(CouchInstaller.dataPath() + "/couchdb/lib/couchdb/erlang/lib/couch-1.0.2/priv/lib/couch_icu_driver.la", replacements); replace(CouchInstaller.dataPath() + "/couchdb/bin/couchdb", replacements); replace(CouchInstaller.dataPath() + "/couchdb/bin/couchjs", replacements); replace(CouchInstaller.dataPath() + "/couchdb/bin/couchjs_wrapper", replacements); replace(CouchInstaller.dataPath() + "/couchdb/etc/couchdb/local.ini", replacements); } /* * Verifies that requested version of CouchDB is installed by checking for the presence of * the package files we write upon installation in the data directory of the app. */ public static boolean checkInstalled(String pkg) { File file = new File(dataPath() + "/" + pkg + ".installedfiles"); if (!file.exists()) { return false; } return true; } /* * Recursively delete directory */ public static Boolean deleteDirectory(File dir) { if (dir.isDirectory()) { String[] children = dir.list(); for (int i = 0; i < children.length; i++) { boolean success = deleteDirectory(new File(dir, children[i])); if (!success) { return false; } } } return dir.delete(); } static void replace(String fileName, String[][] replacements) { try { File file = new File(fileName); BufferedReader reader = new BufferedReader(new FileReader(file)); String line = "", content = ""; while((line = reader.readLine()) != null) { content += line + "\n"; } for (int i = 0; i < replacements.length; i++) { content = content.replaceAll(replacements[i][0], replacements[i][1]); } reader.close(); FileWriter writer = new FileWriter(fileName); writer.write(content); writer.close(); Runtime.getRuntime().exec("/system/bin/chmod 755 " + fileName); } catch (IOException e) { e.printStackTrace(); } } }