Skip to content

Instantly share code, notes, and snippets.

@OJ7
Created October 15, 2019 21:56
Show Gist options
  • Save OJ7/b748328b7eb1be9e76de47cb88dc8e3d to your computer and use it in GitHub Desktop.
Save OJ7/b748328b7eb1be9e76de47cb88dc8e3d to your computer and use it in GitHub Desktop.
Migrate Android IndexedDB from Ionic 3 to Ionic 4
<!-- This file goes in 'src/assets/migration.html' -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Migration Example</title>
<base href="/" />
<meta name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<script type="text/javascript">
var dumpedIndexedDbData = [];
var chunkCount = -1;
function dumpIndexDbToIab() {
const openRequest = indexedDB.open('_ionicstorage');
openRequest.onerror = (err) => {
console.warn('raw IDB open err, ' + JSON.stringify(err));
signalDone();
};
openRequest.onsuccess = (even) => {
const db = (even.target).result;
const IONIC_3_OBJ_STORE_NAME = '_ionickv';
db.onerror = (event) => {
// Generic error handler for all errors targeted at this database's requests
console.warn('Database error: ' + event.target.errorCode);
signalDone();
};
if (db.objectStoreNames.contains(IONIC_3_OBJ_STORE_NAME)) {
const objectStore = db.transaction(IONIC_3_OBJ_STORE_NAME, 'readonly').objectStore(IONIC_3_OBJ_STORE_NAME);
objectStore.getAllKeys().onsuccess = (allKeysEvent) => {
const keys = allKeysEvent.target.result;
objectStore.getAll().onsuccess = (allValuesEvent) => {
const values = allValuesEvent.target.result;
// Reassemble everything that was in there.
const reassembled = {};
for (let i = 0; i < keys.length; ++i) {
reassembled[keys[i]] = values[i];
}
const asString = JSON.stringify(reassembled);
const MAX_IAB_BYTES = 8192;
// Figure out how many blocks we need.
const blockCount = Math.ceil(asString.length / MAX_IAB_BYTES);
for (let i = 0; i < blockCount; ++i) {
const block = asString.slice(i * MAX_IAB_BYTES, (i + 1) * MAX_IAB_BYTES);
dumpedIndexedDbData.push(block);
}
signalDone();
};
};
} else {
console.log('No Ionic object store to migrate, we\'re done.');
// No Ionic object store to migrate, we're done.
signalDone();
}
};
};
function signalDone() {
// set it to something to indicate it's done.
chunkCount = dumpedIndexedDbData.length;
// Seems to trigger another loadstop event, but url is empty? and executing javascript doesn't seem to return
location.hash = '#done';
}
function getChunkCount() {
return chunkCount;
}
function getChunk(index) {
return dumpedIndexedDbData[index];
}
dumpIndexDbToIab();
</script>
</head>
<body>Migration Example</body>
</html>
// Run `MigrationService.checkMigrationStatus()` before each and every time when trying to access Ionic Storage
// e.g. `migrationService.checkMigrationStatus().pipe(flatMap(() => from(storage.get(key))))`
import { Injectable, NgZone } from '@angular/core';
import { Platform } from '@ionic/angular';
import { File } from '@ionic-native/file/ngx';
import { InAppBrowser, InAppBrowserObject, InAppBrowserEvent } from '@ionic-native/in-app-browser/ngx';
import { Storage } from '@ionic/storage';
import { from, Observable, of, Subscriber } from 'rxjs';
import { flatMap, share } from 'rxjs/operators';
const migrationCompletedKey = 'migration-completed';
@Injectable({
providedIn: 'root'
})
export class MigrationService {
private dbMigration: Observable<void> = undefined;
constructor(
private storage: Storage,
private inAppBrowser: InAppBrowser,
private file: File,
private platform: Platform,
private zone: NgZone,
) { }
public checkMigrationStatus(): Observable<void> {
// Check if the database has been migrated, and start only one migration if there are multiple calls.
if (!this.dbMigration) {
const migration$ = Observable.create((sub: Subscriber<void>) => {
this.storage.ready()
.then(() => this.storage.get(migrationCompletedKey))
.then((migrationCompleted: boolean) => {
// Check if previously migrated.
if (migrationCompleted) {
this.completeMigration(sub, false);
} else {
if (this.platform.is('android')) {
this.migrateAndroidIndexedDb(sub);
}
}
});
});
this.dbMigration = migration$.pipe(share());
}
return this.dbMigration;
}
private migrateAndroidIndexedDb(sub: Subscriber<void>): void {
// Android db files that back IndexedDB apparently are tied to host and can't be read if just copied to the right spot.
// So this loads up a WebView with a 'file://' host in order to read from it, then extracts the data and readies it to be
// retrieved by a function call.
const target = '_blank';
const options = 'location=no,clearsessioncache=yes,clearcache=yes,enableViewportScale=yes,hidden=yes';
const migrationPage = this.file.applicationDirectory + 'www/assets/migration.html';
const inAppBrowserObject: InAppBrowserObject = this.inAppBrowser.create(migrationPage, target, options);
// Seems like migration can take 0-400ms. It'll signal back by setting a hash target, which apparently comes
// out to us as a loadstop event with the URL messed up.
const loadStopSub = inAppBrowserObject.on('loadstop').subscribe((event: InAppBrowserEvent) => {
// noticed event.url may be empty or may have the correct hook, checking if it does not equal original page
if (event.url !== migrationPage) {
loadStopSub.unsubscribe();
this.zone.run(() => {
this.checkForData(inAppBrowserObject, sub);
});
}
});
}
private checkForData(iab: InAppBrowserObject, sub: Subscriber<void>): void {
iab.executeScript({ code: 'getChunkCount();' }).then((chunkCountRet: any[]) => {
// count is the first item returned
const count: number = chunkCountRet[0];
if (count > 0) {
this.getData(iab, sub, count);
} else {
console.log('Nothing to migrate, all done');
this.completeMigration(sub, true, iab);
}
}).catch(() => {
console.error('Error in migration');
this.completeMigration(sub, true, iab);
});
}
private getData(iab: InAppBrowserObject, sub: Subscriber<void>, chunks: number): void {
// Reassemble everything that was in there by running a promise chain to get each block, then
// creating a string from the data, then parsing it as JSON, then running another promise chain to
// add all the data into app storage.
let reassembled = '';
let promise: Promise<void> = Promise.resolve(undefined);
for (let i = 0; i < chunks; ++i) {
promise = promise
.then(() => iab.executeScript({ code: `getChunk(${i});` }))
.then((chunkRet: Object[]) => {
reassembled = reassembled + chunkRet[0];
});
}
promise.then(() => {
const migratedData = JSON.parse(reassembled);
for (const key of Object.keys(migratedData)) {
promise = promise.then(() => this.storage.set(key, migratedData[key]));
}
});
promise.then(() => {
console.log('Finished migration, waiting 5 seconds');
// Somewhat of a hack, this 5s delay may or may not be required, depending on size of migrated data.
// This should only run the after the first install of the app when migrating from ionic 3 to 4.
setTimeout(() => {
this.completeMigration(sub, true, iab);
}, 5000);
}).catch((err) => {
console.error('Error in migration');
this.completeMigration(sub, true, iab);
});
}
private completeMigration(sub: Subscriber<void>, shouldSetDbVersion: boolean, iab?: InAppBrowserObject): void {
const completion = () => {
this.dbMigration = of(undefined);
sub.next();
sub.complete();
};
if (iab) {
iab.close();
}
if (shouldSetDbVersion) {
this.storage.set(migrationCompletedKey, true).then(() => completion());
} else {
completion();
}
}
private get(key: string): Observable<any> {
return this.checkMigrationStatus().pipe(flatMap(() => from(this.storage.get(key))));
}
private set(key: string, value: any): Observable<any> {
return this.checkMigrationStatus().pipe(flatMap(() => from(this.storage.set(key, value))));
}
}
@rajashekaranugu
Copy link

will this logic also work for ios??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment