diff --git a/package.json b/package.json index e35981946..4ffa0e0e0 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@ionic-native/clipboard": "4.1.0", "@ionic-native/core": "3.12.1", "@ionic-native/social-sharing": "4.1.0", + "@ionic-native/file": "^4.1.0", "@ionic-native/splash-screen": "3.12.1", "@ionic-native/status-bar": "3.12.1", "@ionic-native/toast": "4.1.0", diff --git a/src/providers/persistence/persistence.spec.ts b/src/providers/persistence/persistence.spec.ts index 7f0da9ac6..031671f5e 100644 --- a/src/providers/persistence/persistence.spec.ts +++ b/src/providers/persistence/persistence.spec.ts @@ -1,11 +1,13 @@ import { TestBed, inject } from '@angular/core/testing'; +import { Logger, Level as LoggerLevel } from '@nsalaun/ng-logger'; +import { Platform } from 'ionic-angular'; + import { PersistenceProvider } from './persistence'; import { IStorage, ISTORAGE } from './storage/istorage'; import { RamStorage } from './storage/ram-storage'; import { LocalStorage } from './storage/local-storage'; -import { Logger, Level as LoggerLevel } from '@nsalaun/ng-logger'; -import { PlatformProvider } from '../platform/platform'; -import { Platform } from 'ionic-angular'; +import { ChromeStorage } from './storage/chrome-storage'; +import { FileStorage } from './storage/file-storage'; describe('Storage Service', () => { beforeEach(() => { @@ -13,8 +15,8 @@ describe('Storage Service', () => { providers: [ PersistenceProvider, { provide: Logger, useValue: new Logger(LoggerLevel.DEBUG) }, - { provide: PlatformProvider }, - { provide: ISTORAGE, useClass: RamStorage, deps: [PlatformProvider, Logger] }, + { provide: ISTORAGE, useClass: RamStorage, deps: [Logger, Platform] }, + Platform, ] }); }); @@ -24,7 +26,7 @@ describe('Storage Service', () => { beforeEach(inject([PersistenceProvider], (pp: PersistenceProvider) => { service = pp; })); - it('should correctly perform a profile roundtrip', () => { + it('should correctly perform a profile roundtrip', (done) => { let p = { name: 'My profile' }; service.storeNewProfile(p) .catch((err) => expect(err).toBeNull) @@ -34,7 +36,8 @@ describe('Storage Service', () => { .then((profile) => { expect(typeof profile).toEqual('object'); expect(profile.name).toEqual('My profile'); - }); + }) + .then(done); }); it('should fail to create a profile when one already exists', () => { diff --git a/src/providers/persistence/persistence.ts b/src/providers/persistence/persistence.ts index 23c3e9186..c32e7dc6e 100644 --- a/src/providers/persistence/persistence.ts +++ b/src/providers/persistence/persistence.ts @@ -38,7 +38,8 @@ const Keys = { }; export let persistenceProviderFactory = (platform: PlatformProvider, log: Logger) => { - let storage = new RamStorage(platform, log); + // TODO: select appropriate storage service based on platform + let storage = new RamStorage(log); return new PersistenceProvider(storage, log); }; diff --git a/src/providers/persistence/storage/chrome-storage.ts b/src/providers/persistence/storage/chrome-storage.ts new file mode 100644 index 000000000..e8abb004e --- /dev/null +++ b/src/providers/persistence/storage/chrome-storage.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@angular/core'; +import { Logger } from '@nsalaun/ng-logger'; +import * as _ from 'lodash'; + +import { IStorage, KeyAlreadyExistsError } from './istorage'; + +@Injectable() +export class ChromeStorage implements IStorage { + ls: chrome.storage.StorageArea; + constructor(private log: Logger) { + if (!chrome.storage || !chrome.storage.local) throw new Error('Chrome storage not supported'); + this.ls = chrome.storage.local; + } + + get(k: string): Promise { + return new Promise(resolve => { + let v = this.ls.get(k, (v) => { + if (!v) return resolve(null); + if (!_.isString(v)) return resolve(v); + let parsed: any; + try { + parsed = JSON.parse(v); + } catch (e) { + } + resolve(parsed || v); + }); + }); + } + + set(k: string, v: any): Promise { + if (_.isObject(v)) { + v = JSON.stringify(v); + } + if (v && !_.isString(v)) { + v = v.toString(); + } + + let obj = {}; + obj[k] = v; + return new Promise(resolve => { + this.ls.set(obj, resolve); + }); + } + + remove(k: string): Promise { + return new Promise(resolve => { + this.ls.remove(k, resolve); + }); + } + + create(k: string, v: any): Promise { + return this.get(k).then((data) => { + if (data) throw new KeyAlreadyExistsError(); + this.set(k, v); + }); + } +} diff --git a/src/providers/persistence/storage/file-storage.ts b/src/providers/persistence/storage/file-storage.ts new file mode 100644 index 000000000..627c9b473 --- /dev/null +++ b/src/providers/persistence/storage/file-storage.ts @@ -0,0 +1,135 @@ +import { Injectable } from '@angular/core'; +import { Logger } from '@nsalaun/ng-logger'; +import * as _ from 'lodash'; +import { File, DirectoryEntry, FileEntry } from '@ionic-native/file'; +import { Platform } from 'ionic-angular'; +import { IStorage, KeyAlreadyExistsError } from './istorage'; + +@Injectable() +export class FileStorage implements IStorage { + fs: FileSystem; + dir: DirectoryEntry; + + constructor(private file: File, private platform: Platform, private log: Logger) { + } + + init(): Promise { + if (this.fs && this.dir) return Promise.resolve(); + + let onSuccess = (fs: FileSystem): Promise => { + console.log('File system started: ', fs.name, fs.root.name); + this.fs = fs; + return this.getDir().then(dir => { + if (!dir.nativeURL) return; + this.dir = dir; + this.log.debug("Got main dir:", dir.nativeURL); + }); + }; + + function onFailure(err: Error): Promise { + this.log.error('Could not init file system: ' + err.message); + return Promise.reject(err); + }; + + return this.platform.ready().then(() => { + window.requestFileSystem(1, 0, onSuccess, onFailure); + }); + } + + // See https://github.com/apache/cordova-plugin-file/#where-to-store-files + getDir(): Promise { + if (!this.file) { + return Promise.reject(new Error('Could not write on device storage')); + } + + var url = this.file.dataDirectory; + return this.file.resolveDirectoryUrl(url) + .catch(err => { + let msg = 'Could not resolve filesystem ' + url; + this.log.warn(msg, err); + throw err || new Error(msg); + }); + }; + + get(k: string): Promise { + let parseResult = (v: any): any => { + if (!v) return null; + if (!_.isString(v)) return v; + let parsed: any; + try { + parsed = JSON.parse(v); + } catch (e) { + } + return parsed || v; + }; + + return this.init() + .then(() => { + return this.file.getFile(this.dir, k, { create: false }); + }) + .then(fileEntry => { + if (!fileEntry) return; + return new Promise((resolve) => { + fileEntry.file(file => { + var reader = new FileReader(); + reader.onloadend = () => { + resolve(parseResult(reader.result)); + } + reader.readAsText(file); + }); + }); + }) + .catch(err => { + // Not found + if (err.code == 1) return; + else throw err; + }); + } + + set(k: string, v: any): Promise { + return this.init() + .then(() => { + return this.file.getFile(this.dir, k, { create: true }); + }) + .then(fileEntry => { + // Create a FileWriter object for our FileEntry (log.txt). + return new Promise((resolve, reject) => { + fileEntry.createWriter(fileWriter => { + fileWriter.onwriteend = (e) => { + this.log.info('Write completed:' + k); + resolve(); + }; + + fileWriter.onerror = (e) => { + this.log.error('Write failed', e); + reject(e); + }; + + if (_.isObject(v)) + v = JSON.stringify(v); + + if (v && !_.isString(v)) { + v = v.toString(); + } + + this.log.debug('Writing:', k, v); + fileWriter.write(v); + }, err => { + this.log.error('Could not create writer', err); + reject(err); + }); + }); + }); + }; + + remove(k: string): Promise { + return Promise.resolve(); + } + + create(k: string, v: any): Promise { + return this.get(k).then((data) => { + if (data) throw new KeyAlreadyExistsError(); + this.set(k, v); + }); + } +} diff --git a/src/providers/persistence/storage/local-storage.ts b/src/providers/persistence/storage/local-storage.ts index 1d232be5d..3c807fedf 100644 --- a/src/providers/persistence/storage/local-storage.ts +++ b/src/providers/persistence/storage/local-storage.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { PlatformProvider } from '../../platform/platform'; import { Logger } from '@nsalaun/ng-logger'; import * as _ from 'lodash'; @@ -8,13 +7,13 @@ import { IStorage, KeyAlreadyExistsError } from './istorage'; @Injectable() export class LocalStorage implements IStorage { ls: Storage; - constructor(private platform: PlatformProvider, private log: Logger) { + constructor(private log: Logger) { this.ls = (typeof window.localStorage !== "undefined") ? window.localStorage : null; if (!this.ls) throw new Error('localstorage not available'); } get(k: string): Promise { - return new Promise((resolve) => { + return new Promise(resolve => { let v = this.ls.getItem(k); if (!v) return resolve(null); if (!_.isString(v)) return resolve(v); @@ -28,7 +27,7 @@ export class LocalStorage implements IStorage { } set(k: string, v: any): Promise { - return Promise.resolve().then(() => { + return new Promise(resolve => { if (_.isObject(v)) { v = JSON.stringify(v); } @@ -37,12 +36,14 @@ export class LocalStorage implements IStorage { } this.ls.setItem(k, v); + resolve(); }); } remove(k: string): Promise { - return Promise.resolve().then(() => { + return new Promise(resolve => { this.ls.removeItem(k); + resolve(); }); } diff --git a/src/providers/persistence/storage/ram-storage.ts b/src/providers/persistence/storage/ram-storage.ts index 734779c7a..1952b8894 100644 --- a/src/providers/persistence/storage/ram-storage.ts +++ b/src/providers/persistence/storage/ram-storage.ts @@ -1,20 +1,25 @@ import { IStorage, KeyAlreadyExistsError } from './istorage'; -import { PlatformProvider } from '../../platform/platform'; import { Logger } from '@nsalaun/ng-logger'; export class RamStorage implements IStorage { hash = {}; - constructor(private platform: PlatformProvider, private log: Logger) { } + constructor(private log: Logger) { } get(k: string): Promise { return Promise.resolve(this.hash[k]); }; set(k: string, v: any): Promise { - return Promise.resolve().then(() => { this.hash[k] = v }); + return new Promise(resolve => { + this.hash[k] = v; + resolve(); + }); }; remove(k: string): Promise { - return Promise.resolve().then(() => { delete this.hash[k]; }); + return new Promise(resolve => { + delete this.hash[k]; + resolve(); + }); }; create(k: string, v: any): Promise { return this.get(k).then((data) => { diff --git a/tsconfig.json b/tsconfig.json index 68aca0754..9918e7f54 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,6 @@ "rewriteTsconfig": false }, "types": [ - "chrome", "lodash", "node" ]