245 lines
7.5 KiB
Vue
245 lines
7.5 KiB
Vue
<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: {
|
||
profiles: function() {
|
||
this.saveProfiles();
|
||
},
|
||
},
|
||
})
|
||
class ServerStorage extends Vue {
|
||
created() {
|
||
this.commit = this.$store.commit;
|
||
}
|
||
|
||
async init() {
|
||
if (!this.serverStorageKey) {
|
||
//генерируем новый ключ
|
||
this.generateNewServerStorageKey();
|
||
}
|
||
this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
|
||
|
||
await this.loadProfiles();
|
||
this.checkCurrentProfile();
|
||
}
|
||
|
||
get serverSyncEnabled() {
|
||
return this.$store.state.reader.serverSyncEnabled;
|
||
}
|
||
|
||
get settings() {
|
||
return this.$store.state.reader.settings;
|
||
}
|
||
|
||
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 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.commit('reader/setProfiles', prof.data);
|
||
this.commit('reader/setProfilesRev', prof.rev);
|
||
|
||
this.oldProfiles = this.profiles;
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
generateNewServerStorageKey() {
|
||
const key = utils.toBase58(utils.randomArray(32));
|
||
this.commit('reader/setServerStorageKey', key);
|
||
}
|
||
|
||
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>
|