sync.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import { getClientConfig } from "../config/client";
  2. import { Updater } from "../typing";
  3. import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
  4. import { createPersistStore } from "../utils/store";
  5. import {
  6. AppState,
  7. getLocalAppState,
  8. GetStoreState,
  9. mergeAppState,
  10. setLocalAppState,
  11. } from "../utils/sync";
  12. import { downloadAs, readFromFile } from "../utils";
  13. import { showToast } from "../components/ui-lib";
  14. import Locale from "../locales";
  15. import { createSyncClient, ProviderType } from "../utils/cloud";
  16. import { corsPath } from "../utils/cors";
  17. export interface WebDavConfig {
  18. server: string;
  19. username: string;
  20. password: string;
  21. }
  22. const isApp = !!getClientConfig()?.isApp;
  23. export type SyncStore = GetStoreState<typeof useSyncStore>;
  24. const DEFAULT_SYNC_STATE = {
  25. provider: ProviderType.WebDAV,
  26. useProxy: true,
  27. proxyUrl: corsPath(ApiPath.Cors),
  28. webdav: {
  29. endpoint: "",
  30. username: "",
  31. password: "",
  32. },
  33. upstash: {
  34. endpoint: "",
  35. username: STORAGE_KEY,
  36. apiKey: "",
  37. },
  38. lastSyncTime: 0,
  39. lastProvider: "",
  40. };
  41. export const useSyncStore = createPersistStore(
  42. DEFAULT_SYNC_STATE,
  43. (set, get) => ({
  44. cloudSync() {
  45. const config = get()[get().provider];
  46. return Object.values(config).every((c) => c.toString().length > 0);
  47. },
  48. markSyncTime() {
  49. set({ lastSyncTime: Date.now(), lastProvider: get().provider });
  50. },
  51. export() {
  52. const state = getLocalAppState();
  53. const datePart = isApp
  54. ? `${new Date().toLocaleDateString().replace(/\//g, "_")} ${new Date()
  55. .toLocaleTimeString()
  56. .replace(/:/g, "_")}`
  57. : new Date().toLocaleString();
  58. const fileName = `Backup-${datePart}.json`;
  59. downloadAs(JSON.stringify(state), fileName);
  60. },
  61. async import() {
  62. const rawContent = await readFromFile();
  63. try {
  64. const remoteState = JSON.parse(rawContent) as AppState;
  65. const localState = getLocalAppState();
  66. mergeAppState(localState, remoteState);
  67. setLocalAppState(localState);
  68. location.reload();
  69. } catch (e) {
  70. console.error("[Import]", e);
  71. showToast(Locale.Settings.Sync.ImportFailed);
  72. }
  73. },
  74. getClient() {
  75. const provider = get().provider;
  76. const client = createSyncClient(provider, get());
  77. return client;
  78. },
  79. async sync() {
  80. const localState = getLocalAppState();
  81. const provider = get().provider;
  82. const config = get()[provider];
  83. const client = this.getClient();
  84. try {
  85. const remoteState = await client.get(config.username);
  86. if (!remoteState || remoteState === "") {
  87. await client.set(config.username, JSON.stringify(localState));
  88. console.log("[Sync] Remote state is empty, using local state instead.");
  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. );
  129. ```
  130. **Output:**
  131. <!-- prettier-ignore -->
  132. ```tsx
  133. import { getClientConfig } from "../config/client";
  134. import { Updater } from "../typing";
  135. import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
  136. import { createPersistStore } from "../utils/store";
  137. import {
  138. AppState,
  139. getLocalAppState,
  140. GetStoreState,
  141. mergeAppState,
  142. setLocalAppState,
  143. } from "../utils/sync";
  144. import { downloadAs, readFromFile } from "../utils";
  145. import { showToast } from "../components/ui-lib";
  146. import Locale from "../locales";
  147. import { createSyncClient, ProviderType } from "../utils/cloud";
  148. import { corsPath } from "../utils/cors";
  149. export interface WebDavConfig {
  150. server: string;
  151. username: string;
  152. password: string;
  153. }
  154. const isApp = !!getClientConfig()?.isApp;
  155. export type SyncStore = GetStoreState<typeof useSyncStore>;
  156. const DEFAULT_SYNC_STATE = {
  157. provider: ProviderType.WebDAV,
  158. useProxy: true,
  159. proxyUrl: corsPath(ApiPath.Cors),
  160. webdav: {
  161. endpoint: "",
  162. username: "",
  163. password: "",
  164. },
  165. upstash: {
  166. endpoint: "",
  167. username: STORAGE_KEY,
  168. apiKey: "",
  169. },
  170. lastSyncTime: 0,
  171. lastProvider: "",
  172. };
  173. export const useSyncStore = createPersistStore(
  174. DEFAULT_SYNC_STATE,
  175. (set, get) => ({
  176. cloudSync() {
  177. const config = get()[get().provider];
  178. return Object.values(config).every((c) => c.toString().length > 0);
  179. },
  180. markSyncTime() {
  181. set({ lastSyncTime: Date.now(), lastProvider: get().provider });
  182. },
  183. export() {
  184. const state = getLocalAppState();
  185. const datePart = isApp
  186. ? `${new Date().toLocaleDateString().replace(/\//g, "_")} ${new Date()
  187. .toLocaleTimeString()
  188. .replace(/:/g, "_")}`
  189. : new Date().toLocaleString();
  190. const fileName = `Backup-${datePart}.json`;
  191. downloadAs(JSON.stringify(state), fileName);
  192. },
  193. async import() {
  194. const rawContent = await readFromFile();
  195. try {
  196. const remoteState = JSON.parse(rawContent) as AppState;
  197. const localState = getLocalAppState();
  198. mergeAppState(localState, remoteState);
  199. setLocalAppState(localState);
  200. location.reload();
  201. } catch (e) {
  202. console.error("[Import]", e);
  203. showToast(Locale.Settings.Sync.ImportFailed);
  204. }
  205. },
  206. getClient() {
  207. const provider = get().provider;
  208. const client = createSyncClient(provider, get());
  209. return client;
  210. },
  211. async sync() {
  212. const localState = getLocalAppState();
  213. const provider = get().provider;
  214. const config = get()[provider];
  215. const client = this.getClient();
  216. try {
  217. const remoteState = await client.get(config.username);
  218. if (!remoteState || remoteState === "") {
  219. console.log(
  220. "[Sync] Remote state is empty, using local state instead.",
  221. );
  222. return;
  223. } else {
  224. const parsedRemoteState = JSON.parse(
  225. await client.get(config.username),
  226. ) as AppState;
  227. mergeAppState(localState, parsedRemoteState);
  228. setLocalAppState(localState);
  229. }
  230. } catch (e) {
  231. console.log("[Sync] failed to get remote state", e);
  232. throw e;
  233. }
  234. await client.set(config.username, JSON.stringify(localState));
  235. console.log("client set", localState);
  236. this.markSyncTime();
  237. },
  238. async check() {
  239. const client = this.getClient();
  240. return await client.check();
  241. },
  242. }),
  243. {
  244. name: StoreKey.Sync,
  245. version: 1.2,
  246. migrate(persistedState, version) {
  247. const newState = persistedState as typeof DEFAULT_SYNC_STATE;
  248. if (version < 1.1) {
  249. newState.upstash.username = STORAGE_KEY;
  250. }
  251. if (version < 1.2) {
  252. if (
  253. (persistedState as typeof DEFAULT_SYNC_STATE).proxyUrl ===
  254. "/api/cors/"
  255. ) {
  256. newState.proxyUrl = "";
  257. }
  258. }
  259. return newState as any;
  260. },
  261. },
  262. );