ServerStorage.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. <template>
  2. <div class="hidden"></div>
  3. </template>
  4. <script>
  5. //-----------------------------------------------------------------------------
  6. import vueComponent from '../../vueComponent.js';
  7. import _ from 'lodash';
  8. import bookManager from '../share/bookManager';
  9. import readerApi from '../../../api/reader';
  10. import * as utils from '../../../share/utils';
  11. import * as cryptoUtils from '../../../share/cryptoUtils';
  12. import LockQueue from '../../../share/LockQueue';
  13. import localForage from 'localforage';
  14. const ssCacheStore = localForage.createInstance({
  15. name: 'ssCacheStore'
  16. });
  17. const componentOptions = {
  18. watch: {
  19. serverSyncEnabled: function() {
  20. if (this.inited)
  21. this.serverSyncEnabledChanged();
  22. },
  23. serverStorageKey: function() {
  24. if (this.inited)
  25. this.serverStorageKeyChanged(true);
  26. },
  27. settings: function() {
  28. this.debouncedSaveSettings();
  29. },
  30. profiles: function() {
  31. this.saveProfiles();
  32. },
  33. currentProfile: function() {
  34. this.currentProfileChanged(true);
  35. },
  36. libs: function() {
  37. this.debouncedSaveLibs();
  38. },
  39. },
  40. };
  41. class ServerStorage {
  42. _options = componentOptions;
  43. created() {
  44. this.inited = false;
  45. this.keyInited = false;
  46. this.commit = this.$store.commit;
  47. this.prevServerStorageKey = null;
  48. this.identity = utils.randomHexString(20);
  49. this.lock = new LockQueue(100);
  50. this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
  51. this.debouncedSaveSettings = _.debounce(() => {
  52. this.saveSettings();
  53. }, 500);
  54. this.debouncedSaveLibs = _.debounce(() => {
  55. this.saveLibs();
  56. }, 500);
  57. this.debouncedNotifySuccess = _.debounce(() => {
  58. this.success('Данные синхронизированы с сервером');
  59. }, 1000);
  60. this.oldProfiles = {};
  61. this.oldSettings = {};
  62. this.oldLibs = {};
  63. }
  64. async init() {
  65. try {
  66. this.cachedRecent = await ssCacheStore.getItem('recent');
  67. if (!this.cachedRecent)
  68. await this.cleanCachedRecent('cachedRecent');
  69. this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
  70. if (!this.cachedRecentPatch)
  71. await this.cleanCachedRecent('cachedRecentPatch');
  72. this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
  73. if (!this.cachedRecentMod)
  74. await this.cleanCachedRecent('cachedRecentMod');
  75. //подстраховка хранения ключа, восстановим из IndexedDB при проблемах в localStorage
  76. if (!this.serverStorageKey) {
  77. const key = await ssCacheStore.getItem('storageKey');
  78. if (key)
  79. this.commit('reader/setServerStorageKey', key);
  80. }
  81. if (!this.serverStorageKey) {
  82. //генерируем новый ключ
  83. await this.generateNewServerStorageKey();
  84. } else {
  85. await this.serverStorageKeyChanged();
  86. }
  87. } finally {
  88. this.inited = true;
  89. }
  90. }
  91. async setCachedRecent(value) {
  92. await ssCacheStore.setItem('recent', value);
  93. this.cachedRecent = value;
  94. }
  95. async setCachedRecentPatch(value) {
  96. await ssCacheStore.setItem('recent-patch', value);
  97. this.cachedRecentPatch = value;
  98. }
  99. async setCachedRecentMod(value) {
  100. await ssCacheStore.setItem('recent-mod', value);
  101. this.cachedRecentMod = value;
  102. }
  103. async cleanCachedRecent(whatToClean) {
  104. if (whatToClean == 'cachedRecent' || whatToClean == 'all')
  105. await this.setCachedRecent({rev: 0, data: {}});
  106. if (whatToClean == 'cachedRecentPatch' || whatToClean == 'all')
  107. await this.setCachedRecentPatch({rev: 0, data: {}});
  108. if (whatToClean == 'cachedRecentMod' || whatToClean == 'all')
  109. await this.setCachedRecentMod({rev: 0, data: {}});
  110. }
  111. async generateNewServerStorageKey() {
  112. const key = utils.toBase58(utils.randomArray(32));
  113. this.commit('reader/setServerStorageKey', key);
  114. //дождемся serverStorageKeyChanged, событие по watch не работает при this.inited == false
  115. await this.serverStorageKeyChanged(true);
  116. }
  117. async serverSyncEnabledChanged() {
  118. if (this.serverSyncEnabled) {
  119. this.prevServerStorageKey = null;
  120. if (!this.serverStorageKey) {
  121. //генерируем новый ключ
  122. await this.generateNewServerStorageKey();
  123. } else {
  124. await this.serverStorageKeyChanged(true);
  125. }
  126. }
  127. }
  128. async serverStorageKeyChanged(force) {
  129. if (this.prevServerStorageKey != this.serverStorageKey) {
  130. this.prevServerStorageKey = this.serverStorageKey;
  131. //сохраним ключ также в IndexedDB, чтобы была возможность восстановить при проблемах с localStorage
  132. await ssCacheStore.setItem('storageKey', this.serverStorageKey);
  133. this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
  134. this.keyInited = true;
  135. await this.loadProfiles(force);
  136. this.checkCurrentProfile();
  137. await this.currentProfileChanged(force);
  138. await this.loadLibs(force);
  139. if (force)
  140. await this.cleanCachedRecent('all');
  141. const loadSuccess = await this.loadRecent();
  142. if (loadSuccess && force) {
  143. await this.saveRecent();
  144. }
  145. }
  146. }
  147. async currentProfileChanged(force) {
  148. if (!this.currentProfile)
  149. return;
  150. await this.loadSettings(force);
  151. }
  152. get serverSyncEnabled() {
  153. return this.$store.state.reader.serverSyncEnabled;
  154. }
  155. get settings() {
  156. return this.$store.state.reader.settings;
  157. }
  158. get settingsRev() {
  159. return this.$store.state.reader.settingsRev;
  160. }
  161. get serverStorageKey() {
  162. return this.$store.state.reader.serverStorageKey;
  163. }
  164. get profiles() {
  165. return this.$store.state.reader.profiles;
  166. }
  167. get profilesRev() {
  168. return this.$store.state.reader.profilesRev;
  169. }
  170. get currentProfile() {
  171. return this.$store.state.reader.currentProfile;
  172. }
  173. get showServerStorageMessages() {
  174. return this.settings.showServerStorageMessages;
  175. }
  176. get libs() {
  177. return this.$store.state.reader.libs;
  178. }
  179. get libsRev() {
  180. return this.$store.state.reader.libsRev;
  181. }
  182. get offlineModeActive() {
  183. return this.$store.state.reader.offlineModeActive;
  184. }
  185. checkCurrentProfile() {
  186. if (!this.profiles[this.currentProfile]) {
  187. this.commit('reader/setCurrentProfile', '');
  188. }
  189. }
  190. success(message) {
  191. if (this.showServerStorageMessages)
  192. this.$root.notify.success(message);
  193. }
  194. warning(message) {
  195. if (this.showServerStorageMessages && !this.offlineModeActive)
  196. this.$root.notify.warning(message);
  197. }
  198. error(message) {
  199. if (this.showServerStorageMessages && !this.offlineModeActive) {
  200. this.errorMessageCounter = (this.errorMessageCounter ? this.errorMessageCounter + 1 : 1);
  201. const hint = (this.errorMessageCounter < 2 ? '' :
  202. '<div><br>Надоело это сообщение? Добавьте в настройках кнопку "Автономный режим" ' +
  203. '<i class="la la-unlink" style="font-size: 20px; color: white"></i> на панель инструментов и активируйте ее.</div>'
  204. );
  205. this.$root.notify.error(message + hint);
  206. }
  207. }
  208. async loadSettings(force = false, doNotifySuccess = true) {
  209. if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile)
  210. return;
  211. const setsId = `settings-${this.currentProfile}`;
  212. const oldRev = this.settingsRev[setsId] || 0;
  213. //проверим ревизию на сервере
  214. if (!force) {
  215. try {
  216. const revs = await this.storageCheck({[setsId]: {}});
  217. if (revs.state == 'success' && revs.items[setsId].rev == oldRev) {
  218. return;
  219. }
  220. } catch(e) {
  221. this.error(`Ошибка соединения с сервером: ${e.message}`);
  222. return;
  223. }
  224. }
  225. let sets = null;
  226. try {
  227. sets = await this.storageGet({[setsId]: {}});
  228. } catch(e) {
  229. this.error(`Ошибка соединения с сервером: ${e.message}`);
  230. return;
  231. }
  232. if (sets.state == 'success') {
  233. sets = sets.items[setsId];
  234. if (sets.rev == 0)
  235. sets.data = {};
  236. this.oldSettings = _.cloneDeep(sets.data);
  237. this.commit('reader/setSettings', sets.data);
  238. this.commit('reader/setSettingsRev', {[setsId]: sets.rev});
  239. if (doNotifySuccess)
  240. this.debouncedNotifySuccess();
  241. } else {
  242. this.warning(`Неверный ответ сервера: ${sets.state}`);
  243. }
  244. }
  245. async saveSettings() {
  246. if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile || this.savingSettings)
  247. return;
  248. const diff = utils.getObjDiff(this.oldSettings, this.settings);
  249. if (utils.isEmptyObjDiff(diff))
  250. return;
  251. this.savingSettings = true;
  252. try {
  253. const setsId = `settings-${this.currentProfile}`;
  254. let result = {state: ''};
  255. const oldRev = this.settingsRev[setsId] || 0;
  256. try {
  257. result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
  258. } catch(e) {
  259. this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
  260. }
  261. if (result.state == 'reject') {
  262. await this.loadSettings(true, false);
  263. this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
  264. } else if (result.state == 'success') {
  265. this.oldSettings = _.cloneDeep(this.settings);
  266. this.commit('reader/setSettingsRev', {[setsId]: this.settingsRev[setsId] + 1});
  267. }
  268. } finally {
  269. this.savingSettings = false;
  270. }
  271. }
  272. async loadProfiles(force = false, doNotifySuccess = true) {
  273. if (!this.keyInited || !this.serverSyncEnabled)
  274. return;
  275. const oldRev = this.profilesRev;
  276. //проверим ревизию на сервере
  277. if (!force) {
  278. try {
  279. const revs = await this.storageCheck({profiles: {}});
  280. if (revs.state == 'success' && revs.items.profiles.rev == oldRev) {
  281. return;
  282. }
  283. } catch(e) {
  284. this.error(`Ошибка соединения с сервером: ${e.message}`);
  285. return;
  286. }
  287. }
  288. let prof = null;
  289. try {
  290. prof = await this.storageGet({profiles: {}});
  291. } catch(e) {
  292. this.error(`Ошибка соединения с сервером: ${e.message}`);
  293. return;
  294. }
  295. if (prof.state == 'success') {
  296. prof = prof.items.profiles;
  297. if (prof.rev == 0)
  298. prof.data = {};
  299. this.oldProfiles = _.cloneDeep(prof.data);
  300. this.commit('reader/setProfiles', prof.data);
  301. this.commit('reader/setProfilesRev', prof.rev);
  302. this.checkCurrentProfile();
  303. if (doNotifySuccess)
  304. this.debouncedNotifySuccess();
  305. } else {
  306. this.warning(`Неверный ответ сервера: ${prof.state}`);
  307. }
  308. }
  309. async saveProfiles() {
  310. if (!this.keyInited || !this.serverSyncEnabled || this.savingProfiles)
  311. return;
  312. const diff = utils.getObjDiff(this.oldProfiles, this.profiles);
  313. if (utils.isEmptyObjDiff(diff))
  314. return;
  315. //обнуляются профили во время разработки при hotReload, подстраховка
  316. if (!this.$store.state.reader.allowProfilesSave) {
  317. console.error('Сохранение профилей не санкционировано');
  318. return;
  319. }
  320. this.savingProfiles = true;
  321. try {
  322. let result = {state: ''};
  323. try {
  324. result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
  325. } catch(e) {
  326. this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
  327. }
  328. if (result.state == 'reject') {
  329. await this.loadProfiles(true, false);
  330. this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
  331. } else if (result.state == 'success') {
  332. this.oldProfiles = _.cloneDeep(this.profiles);
  333. this.commit('reader/setProfilesRev', this.profilesRev + 1);
  334. }
  335. } finally {
  336. this.savingProfiles = false;
  337. }
  338. }
  339. async loadLibs(force = false, doNotifySuccess = true) {
  340. if (!this.keyInited || !this.serverSyncEnabled)
  341. return;
  342. const oldRev = this.libsRev;
  343. //проверим ревизию на сервере
  344. if (!force) {
  345. try {
  346. const revs = await this.storageCheck({libs: {}});
  347. if (revs.state == 'success' && revs.items.libs.rev == oldRev) {
  348. return;
  349. }
  350. } catch(e) {
  351. this.error(`Ошибка соединения с сервером: ${e.message}`);
  352. return;
  353. }
  354. }
  355. let libs = null;
  356. try {
  357. libs = await this.storageGet({libs: {}});
  358. } catch(e) {
  359. this.error(`Ошибка соединения с сервером: ${e.message}`);
  360. return;
  361. }
  362. if (libs.state == 'success') {
  363. libs = libs.items.libs;
  364. if (libs.rev == 0)
  365. libs.data = {};
  366. this.oldLibs = _.cloneDeep(libs.data);
  367. this.commit('reader/setLibs', libs.data);
  368. this.commit('reader/setLibsRev', libs.rev);
  369. if (doNotifySuccess)
  370. this.debouncedNotifySuccess();
  371. } else {
  372. this.warning(`Неверный ответ сервера: ${libs.state}`);
  373. }
  374. }
  375. async saveLibs() {
  376. if (!this.keyInited || !this.serverSyncEnabled || this.savingLibs)
  377. return;
  378. const diff = utils.getObjDiff(this.oldLibs, this.libs);
  379. if (utils.isEmptyObjDiff(diff))
  380. return;
  381. this.savingLibs = true;
  382. try {
  383. let result = {state: ''};
  384. try {
  385. result = await this.storageSet({libs: {rev: this.libsRev + 1, data: this.libs}});
  386. } catch(e) {
  387. this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
  388. }
  389. if (result.state == 'reject') {
  390. await this.loadLibs(true, false);
  391. this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
  392. } else if (result.state == 'success') {
  393. this.oldLibs = _.cloneDeep(this.libs);
  394. this.commit('reader/setLibsRev', this.libsRev + 1);
  395. }
  396. } finally {
  397. this.savingLibs = false;
  398. }
  399. }
  400. async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
  401. if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
  402. return;
  403. this.loadingRecent = true;
  404. try {
  405. //проверим ревизию на сервере
  406. let query = {recent: {}, recentPatch: {}, recentMod: {}};
  407. let revs = null;
  408. if (!skipRevCheck) {
  409. try {
  410. revs = await this.storageCheck(query);
  411. if (revs.state == 'success') {
  412. if (revs.items.recent.rev != this.cachedRecent.rev) {
  413. //no changes
  414. } else if (revs.items.recentPatch.rev != this.cachedRecentPatch.rev) {
  415. query = {recentPatch: {}, recentMod: {}};
  416. } else if (revs.items.recentMod.rev != this.cachedRecentMod.rev) {
  417. query = {recentMod: {}};
  418. } else
  419. return true;
  420. }
  421. } catch(e) {
  422. this.error(`Ошибка соединения с сервером: ${e.message}`);
  423. return;
  424. }
  425. }
  426. let recent = null;
  427. try {
  428. recent = await this.storageGet(query);
  429. } catch(e) {
  430. this.error(`Ошибка соединения с сервером: ${e.message}`);
  431. return;
  432. }
  433. if (recent.state == 'success') {
  434. let newRecent = recent.items.recent;
  435. let newRecentPatch = recent.items.recentPatch;
  436. let newRecentMod = recent.items.recentMod;
  437. if (!newRecent) {
  438. newRecent = _.cloneDeep(this.cachedRecent);
  439. }
  440. if (!newRecentPatch) {
  441. newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
  442. }
  443. if (!newRecentMod) {
  444. newRecentMod = _.cloneDeep(this.cachedRecentMod);
  445. }
  446. if (newRecent.rev == 0) newRecent.data = {};
  447. if (newRecentPatch.rev == 0) newRecentPatch.data = {};
  448. if (newRecentMod.rev == 0) newRecentMod.data = {};
  449. let result = Object.assign({}, newRecent.data, newRecentPatch.data);
  450. const md = newRecentMod.data;
  451. if (md.key && result[md.key])
  452. result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
  453. /*if (!bookManager.loaded) {
  454. this.warning('Ожидание загрузки списка книг перед синхронизацией');
  455. while (!bookManager.loaded) await utils.sleep(100);
  456. }*/
  457. if (newRecent.rev != this.cachedRecent.rev)
  458. await this.setCachedRecent(newRecent);
  459. if (newRecentPatch.rev != this.cachedRecentPatch.rev)
  460. await this.setCachedRecentPatch(newRecentPatch);
  461. if (newRecentMod.rev != this.cachedRecentMod.rev)
  462. await this.setCachedRecentMod(newRecentMod);
  463. await bookManager.setRecent(result);
  464. } else {
  465. this.warning(`Неверный ответ сервера: ${recent.state}`);
  466. return;
  467. }
  468. if (doNotifySuccess)
  469. this.debouncedNotifySuccess();
  470. } finally {
  471. this.loadingRecent = false;
  472. }
  473. return true;
  474. }
  475. async saveRecent(itemKeys, recurse) {
  476. while (!this.inited)
  477. await utils.sleep(100);
  478. if (!this.keyInited || !this.serverSyncEnabled)
  479. return;
  480. let needRecurseCall = false;
  481. await this.lock.get();
  482. try {
  483. const bm = bookManager;
  484. let needSaveRecent = false;
  485. let needSaveRecentPatch = false;
  486. let needSaveRecentMod = false;
  487. //newRecentMod
  488. let newRecentMod = {};
  489. let oneItemKey = null;
  490. if (itemKeys && itemKeys.length == 1)
  491. oneItemKey = itemKeys[0];
  492. if (oneItemKey && this.cachedRecentPatch.data[oneItemKey] && this.prevItemKey == oneItemKey) {
  493. newRecentMod = _.cloneDeep(this.cachedRecentMod);
  494. newRecentMod.rev++;
  495. newRecentMod.data.key = oneItemKey;
  496. newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[oneItemKey], bm.recent[oneItemKey]);
  497. needSaveRecentMod = true;
  498. }
  499. this.prevItemKey = oneItemKey;
  500. //newRecentPatch
  501. let newRecentPatch = {};
  502. if (itemKeys && !needSaveRecentMod) {
  503. newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
  504. newRecentPatch.rev++;
  505. for (const key of itemKeys) {
  506. newRecentPatch.data[key] = _.cloneDeep(bm.recent[key]);
  507. }
  508. const applyMod = this.cachedRecentMod.data;
  509. if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
  510. newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
  511. newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
  512. needSaveRecentPatch = true;
  513. needSaveRecentMod = true;
  514. }
  515. //newRecent
  516. let newRecent = {};
  517. if (!itemKeys || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
  518. newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
  519. newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
  520. newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
  521. needSaveRecent = true;
  522. needSaveRecentPatch = true;
  523. needSaveRecentMod = true;
  524. }
  525. //query
  526. let query = {};
  527. if (needSaveRecent) {
  528. query = {recent: newRecent, recentPatch: newRecentPatch, recentMod: newRecentMod};
  529. } else if (needSaveRecentPatch) {
  530. query = {recentPatch: newRecentPatch, recentMod: newRecentMod};
  531. } else {
  532. query = {recentMod: newRecentMod};
  533. }
  534. //сохранение
  535. let result = {state: ''};
  536. try {
  537. result = await this.storageSet(query);
  538. } catch(e) {
  539. this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
  540. }
  541. if (result.state == 'reject') {
  542. const res = await this.loadRecent(false, false);
  543. if (res)
  544. this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
  545. if (!recurse && itemKeys) {
  546. needRecurseCall = true;
  547. }
  548. } else if (result.state == 'success') {
  549. if (needSaveRecent && newRecent.rev)
  550. await this.setCachedRecent(newRecent);
  551. if (needSaveRecentPatch && newRecentPatch.rev)
  552. await this.setCachedRecentPatch(newRecentPatch);
  553. if (needSaveRecentMod && newRecentMod.rev)
  554. await this.setCachedRecentMod(newRecentMod);
  555. } else {
  556. this.prevItemKey = null;
  557. }
  558. } finally {
  559. this.lock.ret();
  560. }
  561. if (needRecurseCall)
  562. await this.saveRecent(itemKeys, true);
  563. }
  564. async storageCheck(items) {
  565. return await this.storageApi('check', items);
  566. }
  567. async storageGet(items) {
  568. return await this.storageApi('get', items);
  569. }
  570. async storageSet(items, force) {
  571. return await this.storageApi('set', items, force);
  572. }
  573. async storageApi(action, items, force) {
  574. const request = {action, identity: this.identity, items};
  575. if (force)
  576. request.force = true;
  577. const encodedRequest = await this.encodeStorageItems(request);
  578. return await this.decodeStorageItems(await readerApi.storage(encodedRequest));
  579. }
  580. async encodeStorageItems(request) {
  581. if (!this.hashedStorageKey)
  582. throw new Error('hashedStorageKey is empty');
  583. if (!_.isObject(request.items))
  584. throw new Error('items is not an object');
  585. let result = Object.assign({}, request);
  586. let items = {};
  587. for (const id of Object.keys(request.items)) {
  588. const item = request.items[id];
  589. if (request.action == 'set' && !_.isObject(item.data))
  590. throw new Error('encodeStorageItems: data is not an object');
  591. let encoded = Object.assign({}, item);
  592. if (item.data) {
  593. const comp = utils.pako.deflate(JSON.stringify(item.data), {level: 1});
  594. let encrypted = null;
  595. try {
  596. encrypted = cryptoUtils.aesEncrypt(comp, this.serverStorageKey);
  597. } catch (e) {
  598. throw new Error('encrypt failed');
  599. }
  600. encoded.data = '1' + utils.toBase64(encrypted);
  601. }
  602. items[`${this.hashedStorageKey}.${utils.toBase58(id)}`] = encoded;
  603. }
  604. result.items = items;
  605. return result;
  606. }
  607. async decodeStorageItems(response) {
  608. if (!this.hashedStorageKey)
  609. throw new Error('hashedStorageKey is empty');
  610. let result = Object.assign({}, response);
  611. let items = {};
  612. if (response.items) {
  613. if (!_.isObject(response.items))
  614. throw new Error('items is not an object');
  615. for (const id of Object.keys(response.items)) {
  616. const item = response.items[id];
  617. let decoded = Object.assign({}, item);
  618. if (item.data) {
  619. if (!_.isString(item.data) || !item.data.length)
  620. throw new Error('decodeStorageItems: data is not a string');
  621. if (item.data[0] !== '1')
  622. throw new Error('decodeStorageItems: unknown data format');
  623. const a = utils.fromBase64(item.data.substr(1));
  624. let decrypted = null;
  625. try {
  626. decrypted = cryptoUtils.aesDecrypt(a, this.serverStorageKey);
  627. } catch (e) {
  628. throw new Error('decrypt failed');
  629. }
  630. decoded.data = JSON.parse(utils.pako.inflate(decrypted, {to: 'string'}));
  631. }
  632. const ids = id.split('.');
  633. if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
  634. throw new Error(`decodeStorageItems: bad id - ${id}`);
  635. items[utils.fromBase58(ids[1]).toString()] = decoded;
  636. }
  637. }
  638. result.items = items;
  639. return result;
  640. }
  641. }
  642. export default vueComponent(ServerStorage);
  643. //-----------------------------------------------------------------------------
  644. </script>