Files
liberama/client/components/Reader/ServerStorage/ServerStorage.vue
2019-03-17 22:39:40 +07:00

345 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div></div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';
import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils';
const maxSetTries = 5;
export default @Component({
watch: {
serverStorageKey: function() {
this.serverStorageKeyChanged();
},
settings: function() {
this.debouncedSaveSettings();
},
profiles: function() {
this.saveProfiles();
},
currentProfile: function() {
this.currentProfileChanged();
},
},
})
class ServerStorage extends Vue {
created() {
this.commit = this.$store.commit;
this.prevServerStorageKey = null;
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()});
this.debouncedSaveSettings = _.debounce(() => {
this.saveSettings();
}, 500);
this.oldProfiles = {};
this.oldSettings = {};
}
async init() {
if (!this.serverStorageKey) {
//генерируем новый ключ
await this.generateNewServerStorageKey();
} else {
await this.serverStorageKeyChanged();
}
await this.currentProfileChanged();
}
async serverStorageKeyChanged() {
if (this.prevServerStorageKey != this.serverStorageKey) {
this.prevServerStorageKey = this.serverStorageKey;
this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
await this.loadProfiles();
this.checkCurrentProfile();
}
}
async currentProfileChanged() {
if (!this.currentProfile)
return;
await this.loadSettings();
}
get serverSyncEnabled() {
return this.$store.state.reader.serverSyncEnabled;
}
get settings() {
return this.$store.state.reader.settings;
}
get settingsRev() {
return this.$store.state.reader.settingsRev;
}
get serverStorageKey() {
return this.$store.state.reader.serverStorageKey;
}
get profiles() {
return this.$store.state.reader.profiles;
}
get profilesRev() {
return this.$store.state.reader.profilesRev;
}
get currentProfile() {
return this.$store.state.reader.currentProfile;
}
checkCurrentProfile() {
if (!this.profiles[this.currentProfile]) {
this.commit('reader/setCurrentProfile', '');
}
}
notifySuccessIfNeeded(rev1, rev2) {
if (rev1 != rev2)
this.$notify.success({message: 'Данные синхронизированы с сервером'});
}
warning(message) {
this.$notify.warning({message});
}
error(message) {
this.$notify.error({message});
}
async loadSettings() {
if (!this.serverSyncEnabled || !this.currentProfile)
return;
const setsId = `settings-${this.currentProfile}`;
let sets = await this.storageGet({[setsId]: {}});
if (sets.state == 'success') {
const oldRev = this.settingsRev[setsId] || 0;
sets = sets.items[setsId];
if (sets.rev == 0)
sets.data = {};
this.oldSettings = sets.data;
this.commit('reader/setSettings', sets.data);
this.commit('reader/setSettingsRev', {[setsId]: sets.rev});
this.notifySuccessIfNeeded(oldRev, sets.rev);
} else {
this.warning(`Неверный ответ сервера: ${sets.state}`);
}
}
async saveSettings() {
if (!this.serverSyncEnabled || !this.currentProfile || this.savingSettings)
return;
const diff = utils.getObjDiff(this.oldSettings, this.settings);
if (utils.isEmptyObjDiff(diff))
return;
this.savingSettings = true;
try {
const setsId = `settings-${this.currentProfile}`;
let result = {state: ''};
let tries = 0;
while (result.state != 'success' && tries < maxSetTries) {
const oldRev = this.settingsRev[setsId] || 0;
result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
if (result.state == 'reject') {
await this.loadSettings();
const newSettings = utils.applyObjDiff(this.settings, diff);
this.commit('reader/setSettings', newSettings);
}
tries++;
}
if (tries >= maxSetTries) {
this.commit('reader/setSettings', this.oldSettings);
this.error('Не удалось отправить данные на сервер');
} else {
this.oldSettings = this.settings;
this.commit('reader/setSettingsRev', {[setsId]: this.settingsRev[setsId] + 1});
}
} finally {
this.savingSettings = false;
}
}
async loadProfiles() {
if (!this.serverSyncEnabled)
return;
let prof = await this.storageGet({'profiles': {}});
if (prof.state == 'success') {
const oldRev = this.profilesRev;
prof = prof.items.profiles;
if (prof.rev == 0)
prof.data = {};
this.oldProfiles = prof.data;
this.commit('reader/setProfiles', prof.data);
this.commit('reader/setProfilesRev', prof.rev);
this.notifySuccessIfNeeded(oldRev, prof.rev);
} else {
this.warning(`Неверный ответ сервера: ${prof.state}`);
}
}
async saveProfiles() {
if (!this.serverSyncEnabled || this.savingProfiles)
return;
const diff = utils.getObjDiff(this.oldProfiles, this.profiles);
if (utils.isEmptyObjDiff(diff))
return;
this.savingProfiles = true;
try {
let result = {state: ''};
let tries = 0;
while (result.state != 'success' && tries < maxSetTries) {
result = await this.storageSet({'profiles': {rev: this.profilesRev + 1, data: this.profiles}});
if (result.state == 'reject') {
await this.loadProfiles();
const newProfiles = utils.applyObjDiff(this.profiles, diff);
this.commit('reader/setProfiles', newProfiles);
}
tries++;
}
if (tries >= maxSetTries) {
this.commit('reader/setProfiles', this.oldProfiles);
this.checkCurrentProfile();
this.error('Не удалось отправить данные на сервер');
} else {
this.oldProfiles = this.profiles;
this.commit('reader/setProfilesRev', this.profilesRev + 1);
}
} finally {
this.savingProfiles = false;
}
}
async generateNewServerStorageKey() {
const key = utils.toBase58(utils.randomArray(32));
this.commit('reader/setServerStorageKey', key);
await this.serverStorageKeyChanged();
}
async storageCheck(items) {
return await this.storageApi('check', items);
}
async storageGet(items) {
return await this.storageApi('get', items);
}
async storageSet(items, force) {
return await this.storageApi('set', items, force);
}
async storageApi(action, items, force) {
const request = {action, items};
if (force)
request.force = true;
const encodedRequest = await this.encodeStorageItems(request);
return await this.decodeStorageItems(await readerApi.storage(encodedRequest));
}
async encodeStorageItems(request) {
if (!this.hashedStorageKey)
throw new Error('hashedStorageKey is empty');
if (!_.isObject(request.items))
throw new Error('items is not an object');
let result = Object.assign({}, request);
let items = {};
for (const id of Object.keys(request.items)) {
const item = request.items[id];
if (request.action == 'set' && !_.isObject(item.data))
throw new Error('encodeStorageItems: data is not an object');
let encoded = Object.assign({}, item);
if (item.data) {
const comp = utils.pako.deflate(JSON.stringify(item.data), {level: 1});
let encrypted = null;
try {
encrypted = cryptoUtils.aesEncrypt(comp, this.serverStorageKey);
} catch (e) {
throw new Error('encrypt failed');
}
encoded.data = '1' + utils.toBase64(encrypted);
}
items[`${this.hashedStorageKey}.${utils.toBase58(id)}`] = encoded;
}
result.items = items;
return result;
}
async decodeStorageItems(response) {
if (!this.hashedStorageKey)
throw new Error('hashedStorageKey is empty');
let result = Object.assign({}, response);
let items = {};
if (response.items) {
if (!_.isObject(response.items))
throw new Error('items is not an object');
for (const id of Object.keys(response.items)) {
const item = response.items[id];
let decoded = Object.assign({}, item);
if (item.data) {
if (!_.isString(item.data) || !item.data.length)
throw new Error('decodeStorageItems: data is not a string');
if (item.data[0] !== '1')
throw new Error('decodeStorageItems: unknown data format');
const a = utils.fromBase64(item.data.substr(1));
let decrypted = null;
try {
decrypted = cryptoUtils.aesDecrypt(a, this.serverStorageKey);
} catch (e) {
throw new Error('decrypt failed');
}
decoded.data = JSON.parse(utils.pako.inflate(decrypted, {to: 'string'}));
}
const ids = id.split('.');
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
throw new Error(`decodeStorageItems: bad id - ${id}`);
items[utils.fromBase58(ids[1])] = decoded;
}
}
result.items = items;
return result;
}
}
//-----------------------------------------------------------------------------
</script>