You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			151 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			TypeScript
		
	
		
		
			
		
	
	
			151 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			TypeScript
		
	
| 
											9 months ago
										 | import { getClientConfig } from "../config/client"; | ||
|  | import { ApiPath, STORAGE_KEY, StoreKey } from "../constant"; | ||
|  | import { createPersistStore } from "../utils/store"; | ||
|  | import { | ||
|  |   AppState, | ||
|  |   getLocalAppState, | ||
|  |   GetStoreState, | ||
|  |   mergeAppState, | ||
|  |   setLocalAppState, | ||
|  | } from "../utils/sync"; | ||
|  | import { downloadAs, readFromFile } from "../utils"; | ||
|  | import { showToast } from "../components/ui-lib"; | ||
|  | import Locale from "../locales"; | ||
|  | import { createSyncClient, ProviderType } from "../utils/cloud"; | ||
|  | 
 | ||
|  | export interface WebDavConfig { | ||
|  |   server: string; | ||
|  |   username: string; | ||
|  |   password: string; | ||
|  | } | ||
|  | 
 | ||
|  | const isApp = !!getClientConfig()?.isApp; | ||
|  | export type SyncStore = GetStoreState<typeof useSyncStore>; | ||
|  | 
 | ||
|  | const DEFAULT_SYNC_STATE = { | ||
|  |   provider: ProviderType.WebDAV, | ||
|  |   useProxy: true, | ||
|  |   proxyUrl: ApiPath.Cors as string, | ||
|  | 
 | ||
|  |   webdav: { | ||
|  |     endpoint: "", | ||
|  |     username: "", | ||
|  |     password: "", | ||
|  |   }, | ||
|  | 
 | ||
|  |   upstash: { | ||
|  |     endpoint: "", | ||
|  |     username: STORAGE_KEY, | ||
|  |     apiKey: "", | ||
|  |   }, | ||
|  | 
 | ||
|  |   lastSyncTime: 0, | ||
|  |   lastProvider: "", | ||
|  | }; | ||
|  | 
 | ||
|  | export const useSyncStore = createPersistStore( | ||
|  |   DEFAULT_SYNC_STATE, | ||
|  |   (set, get) => ({ | ||
|  |     cloudSync() { | ||
|  |       const config = get()[get().provider]; | ||
|  |       return Object.values(config).every((c) => c.toString().length > 0); | ||
|  |     }, | ||
|  | 
 | ||
|  |     markSyncTime() { | ||
|  |       set({ lastSyncTime: Date.now(), lastProvider: get().provider }); | ||
|  |     }, | ||
|  | 
 | ||
|  |     export() { | ||
|  |       const state = getLocalAppState(); | ||
|  |       const datePart = isApp | ||
|  |         ? `${new Date().toLocaleDateString().replace(/\//g, "_")} ${new Date() | ||
|  |             .toLocaleTimeString() | ||
|  |             .replace(/:/g, "_")}`
 | ||
|  |         : new Date().toLocaleString(); | ||
|  | 
 | ||
|  |       const fileName = `Backup-${datePart}.json`; | ||
|  |       downloadAs(JSON.stringify(state), fileName); | ||
|  |     }, | ||
|  | 
 | ||
|  |     async import() { | ||
|  |       const rawContent = await readFromFile(); | ||
|  | 
 | ||
|  |       try { | ||
|  |         const remoteState = JSON.parse(rawContent) as AppState; | ||
|  |         const localState = getLocalAppState(); | ||
|  |         mergeAppState(localState, remoteState); | ||
|  |         setLocalAppState(localState); | ||
|  |         location.reload(); | ||
|  |       } catch (e) { | ||
|  |         console.error("[Import]", e); | ||
|  |         showToast(Locale.Settings.Sync.ImportFailed); | ||
|  |       } | ||
|  |     }, | ||
|  | 
 | ||
|  |     getClient() { | ||
|  |       const provider = get().provider; | ||
|  |       const client = createSyncClient(provider, get()); | ||
|  |       return client; | ||
|  |     }, | ||
|  | 
 | ||
|  |     async sync() { | ||
|  |       const localState = getLocalAppState(); | ||
|  |       const provider = get().provider; | ||
|  |       const config = get()[provider]; | ||
|  |       const client = this.getClient(); | ||
|  | 
 | ||
|  |       try { | ||
|  |         const remoteState = await client.get(config.username); | ||
|  |         if (!remoteState || remoteState === "") { | ||
|  |           await client.set(config.username, JSON.stringify(localState)); | ||
|  |           console.log( | ||
|  |             "[Sync] Remote state is empty, using local state instead.", | ||
|  |           ); | ||
|  |           return; | ||
|  |         } else { | ||
|  |           const parsedRemoteState = JSON.parse( | ||
|  |             await client.get(config.username), | ||
|  |           ) as AppState; | ||
|  |           mergeAppState(localState, parsedRemoteState); | ||
|  |           setLocalAppState(localState); | ||
|  |         } | ||
|  |       } catch (e) { | ||
|  |         console.log("[Sync] failed to get remote state", e); | ||
|  |         throw e; | ||
|  |       } | ||
|  | 
 | ||
|  |       await client.set(config.username, JSON.stringify(localState)); | ||
|  | 
 | ||
|  |       this.markSyncTime(); | ||
|  |     }, | ||
|  | 
 | ||
|  |     async check() { | ||
|  |       const client = this.getClient(); | ||
|  |       return await client.check(); | ||
|  |     }, | ||
|  |   }), | ||
|  |   { | ||
|  |     name: StoreKey.Sync, | ||
|  |     version: 1.2, | ||
|  | 
 | ||
|  |     migrate(persistedState, version) { | ||
|  |       const newState = persistedState as typeof DEFAULT_SYNC_STATE; | ||
|  | 
 | ||
|  |       if (version < 1.1) { | ||
|  |         newState.upstash.username = STORAGE_KEY; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (version < 1.2) { | ||
|  |         if ( | ||
|  |           (persistedState as typeof DEFAULT_SYNC_STATE).proxyUrl === | ||
|  |           "/api/cors/" | ||
|  |         ) { | ||
|  |           newState.proxyUrl = ""; | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       return newState as any; | ||
|  |     }, | ||
|  |   }, | ||
|  | ); |