package org.commcare.models.database; import android.content.ContentValues; import android.util.Pair; import net.sqlcipher.Cursor; import net.sqlcipher.database.SQLiteDatabase; import org.commcare.logging.AndroidLogger; import org.commcare.modern.database.DatabaseHelper; import org.javarosa.core.services.Logger; import org.javarosa.core.util.PropertyUtils; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Methods for performing column queries on database table that uses files to * store serialized object payloads. * * @author Phillip Mates (pmates@dimagi.com). */ public class HybridFileBackedSqlHelpers { protected static Pair<String, byte[]> getEntryFilenameAndKey(AndroidDbHelper helper, String table, int id) { Cursor c; c = helper.getHandle().query(table, HybridFileBackedSqlStorage.dataColumns, DatabaseHelper.ID_COL + "=?", new String[]{String.valueOf(id)}, null, null, null); try { c.moveToFirst(); return new Pair<>(c.getString(c.getColumnIndexOrThrow(DatabaseHelper.FILE_COL)), c.getBlob(c.getColumnIndexOrThrow(DatabaseHelper.AES_COL))); } finally { if (c != null) { c.close(); } } } protected static String getEntryFilename(AndroidDbHelper helper, String table, int id) { Cursor c; SQLiteDatabase db = helper.getHandle(); String[] columns = new String[]{DatabaseHelper.FILE_COL}; c = db.query(table, columns, DatabaseHelper.ID_COL + "=?", new String[]{String.valueOf(id)}, null, null, null); try { c.moveToFirst(); return c.getString(c.getColumnIndexOrThrow(DatabaseHelper.FILE_COL)); } finally { if (c != null) { c.close(); } } } protected static List<String> getFilesToRemove(List<Integer> idsBeingRemoved, AndroidDbHelper helper, String table) { ArrayList<String> files = new ArrayList<>(); // delete files storing data for entries being removed for (Integer id : idsBeingRemoved) { String filename = HybridFileBackedSqlHelpers.getEntryFilename(helper, table, id); if (filename != null && new File(filename).exists()) { files.add(filename); } } return files; } protected static void removeFiles(List<String> filesToRemove) { for (String filename : filesToRemove) { File datafile = new File(filename); datafile.delete(); } } protected static void setFileAsOrphan(SQLiteDatabase db, String filename) { db.beginTransaction(); try { ContentValues cv = new ContentValues(); cv.put(DatabaseHelper.FILE_COL, filename); db.insertOrThrow(DbUtil.orphanFileTableName, DatabaseHelper.FILE_COL, cv); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } protected static void unsetFileAsOrphan(SQLiteDatabase db, String filename) { int deleteCount = db.delete(DbUtil.orphanFileTableName, DatabaseHelper.FILE_COL + "=?", new String[]{filename}); if (deleteCount != 1) { Logger.log(AndroidLogger.SOFT_ASSERT, "Unable to unset orphaned file: " + deleteCount + " entries effected.b"); } } /** * Remove files in the orphaned file table. Files are added to this table * when file-backed db transactions fail, leaving the file on the * filesystem. * * Order of operations expects filenames to be globally unique. */ public static void removeOrphanedFiles(SQLiteDatabase db) { Cursor cur = db.query(DbUtil.orphanFileTableName, new String[]{DatabaseHelper.FILE_COL}, null, null, null, null, null); ArrayList<String> files = new ArrayList<>(); try { if (cur.getCount() > 0) { cur.moveToFirst(); int fileColIndex = cur.getColumnIndexOrThrow(DatabaseHelper.FILE_COL); while (!cur.isAfterLast()) { files.add(cur.getString(fileColIndex)); cur.moveToNext(); } } } finally { if (cur != null) { cur.close(); } } removeFiles(files); db.beginTransaction(); try { db.delete(DbUtil.orphanFileTableName, null, null); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } protected static File newFileForEntry(File dbDir) throws IOException { File newFile = getUniqueFilename(dbDir); if (!newFile.createNewFile()) { throw new RuntimeException("Trying to create a new file using existing filename; " + "Shouldn't be possible since we already checked for uniqueness"); } return newFile; } private static File getUniqueFilename(File dbDir) { String filename = PropertyUtils.genUUID(); File newFile = new File(dbDir, filename); // keep trying until we find a filename that doesn't already exist while (newFile.exists()) { newFile = getUniqueFilename(dbDir); } return newFile; } }