sync.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { getClientConfig } from "../config/client";
  2. import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
  3. import { createPersistStore } from "../utils/store";
  4. import {
  5. AppState,
  6. getLocalAppState,
  7. GetStoreState,
  8. mergeAppState,
  9. setLocalAppState,
  10. } from "../utils/sync";
  11. import { downloadAs, readFromFile } from "../utils";
  12. import { showToast } from "../components/ui-lib";
  13. import Locale from "../locales";
  14. import { createSyncClient, ProviderType } from "../utils/cloud";
  15. export interface WebDavConfig {
  16. server: string;
  17. username: string;
  18. password: string;
  19. }
  20. const isApp = !!getClientConfig()?.isApp;
  21. export type SyncStore = GetStoreState<typeof useSyncStore>;
  22. const DEFAULT_SYNC_STATE = {
  23. provider: ProviderType.WebDAV,
  24. useProxy: true,
  25. proxyUrl: ApiPath.Cors as string,
  26. webdav: {
  27. endpoint: "",
  28. username: "",
  29. password: "",
  30. },
  31. upstash: {
  32. endpoint: "",
  33. username: STORAGE_KEY,
  34. apiKey: "",
  35. },
  36. lastSyncTime: 0,
  37. lastProvider: "",
  38. };
  39. export const useSyncStore = createPersistStore(
  40. DEFAULT_SYNC_STATE,
  41. (set, get) => ({
  42. cloudSync() {
  43. const config = get()[get().provider];
  44. return Object.values(config).every((c) => c.toString().length > 0);
  45. },
  46. markSyncTime() {
  47. set({ lastSyncTime: Date.now(), lastProvider: get().provider });
  48. },
  49. export() {
  50. const state = getLocalAppState();
  51. const datePart = isApp
  52. ? `${new Date().toLocaleDateString().replace(/\//g, "_")} ${new Date()
  53. .toLocaleTimeString()
  54. .replace(/:/g, "_")}`
  55. : new Date().toLocaleString();
  56. const fileName = `Backup-${datePart}.json`;
  57. downloadAs(JSON.stringify(state), fileName);
  58. },
  59. async import() {
  60. const rawContent = await readFromFile();
  61. try {
  62. const remoteState = JSON.parse(rawContent) as AppState;
  63. const localState = getLocalAppState();
  64. mergeAppState(localState, remoteState);
  65. setLocalAppState(localState);
  66. location.reload();
  67. } catch (e) {
  68. console.error("[Import]", e);
  69. showToast(Locale.Settings.Sync.ImportFailed);
  70. }
  71. },
  72. getClient() {
  73. const provider = get().provider;
  74. const client = createSyncClient(provider, get());
  75. return client;
  76. },
  77. async sync() {
  78. const localState = getLocalAppState();
  79. const provider = get().provider;
  80. const config = get()[provider];
  81. const client = this.getClient();
  82. try {
  83. const remoteState = await client.get(config.username);
  84. if (!remoteState || remoteState === "") {
  85. await client.set(config.username, JSON.stringify(localState));
  86. console.log(
  87. "[Sync] Remote state is empty, using local state instead.",
  88. );
  89. return;
  90. } else {
  91. const parsedRemoteState = JSON.parse(
  92. await client.get(config.username),
  93. ) as AppState;
  94. mergeAppState(localState, parsedRemoteState);
  95. setLocalAppState(localState);
  96. }
  97. } catch (e) {
  98. console.log("[Sync] failed to get remote state", e);
  99. throw e;
  100. }
  101. await client.set(config.username, JSON.stringify(localState));
  102. this.markSyncTime();
  103. },
  104. async check() {
  105. const client = this.getClient();
  106. return await client.check();
  107. },
  108. }),
  109. {
  110. name: StoreKey.Sync,
  111. version: 1.2,
  112. migrate(persistedState, version) {
  113. const newState = persistedState as typeof DEFAULT_SYNC_STATE;
  114. if (version < 1.1) {
  115. newState.upstash.username = STORAGE_KEY;
  116. }
  117. if (version < 1.2) {
  118. if (
  119. (persistedState as typeof DEFAULT_SYNC_STATE).proxyUrl ===
  120. "/api/cors/"
  121. ) {
  122. newState.proxyUrl = "";
  123. }
  124. }
  125. return newState as any;
  126. },
  127. },
  128. );