Created
October 15, 2019 21:56
-
-
Save OJ7/b748328b7eb1be9e76de47cb88dc8e3d to your computer and use it in GitHub Desktop.
Migrate Android IndexedDB from Ionic 3 to Ionic 4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- 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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)))); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
will this logic also work for ios??