File size: 6,195 Bytes
e0405f2
 
 
 
 
 
 
 
 
 
 
20a9726
 
 
 
 
 
 
e0405f2
 
 
 
 
 
 
20a9726
e0405f2
 
 
 
 
 
 
 
20a9726
e0405f2
 
 
 
 
 
 
20a9726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0405f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b5010b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0405f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20a9726
 
 
 
 
 
 
e0405f2
20a9726
e0405f2
 
 
 
 
 
 
20a9726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0405f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import * as TaskManager from 'expo-task-manager';
import * as BackgroundFetch from 'expo-background-fetch';
import * as MediaLibrary from 'expo-media-library';
import * as FileSystem from 'expo-file-system/legacy';
import * as Network from 'expo-network';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SYNC_ON_CELLULAR_KEY } from '../(tabs)/settings';

export const BACKGROUND_SYNC_TASK = 'BACKGROUND_SYNC_TASK';
export const BACKGROUND_SYNC_ENABLED_KEY = '@setting_background_sync';

export const BACKEND_URL = 'https://dpv007-keystone.hf.space';

export const getToken = async () => {
  return await AsyncStorage.getItem('jwt_token');
};

export const SYNCED_ASSETS_KEY = '@synced_assets_v2';

export type SyncCallbacks = {
  onProgress?: (current: number, total: number) => void;
  onStatusChange?: (status: 'idle' | 'syncing' | 'done', itemsLeft: number) => void;
  onInterrupted?: (reason: string) => void;
};

export const getSyncedList = async (): Promise<string[]> => {
  try {
    const v = await AsyncStorage.getItem(SYNCED_ASSETS_KEY);
    return v ? JSON.parse(v) : [];
  } catch {
    return [];
  }
};

export const addSyncedId = async (id: string, list: string[]): Promise<string[]> => {
  const next = [...list, id];
  try {
    await AsyncStorage.setItem(SYNCED_ASSETS_KEY, JSON.stringify(next));
  } catch {}
  return next;
};

/**
 * Upload a single asset to the backend using the stored JWT.
 */
const uploadAsset = async (asset: MediaLibrary.Asset): Promise<boolean> => {
  try {
    const token = await getToken();
    if (!token) return false;

    const info = await MediaLibrary.getAssetInfoAsync(asset);
    const uri = info.localUri || asset.uri;

    const res = await FileSystem.uploadAsync(`${BACKEND_URL}/upload`, uri, {
      httpMethod: 'POST',
      uploadType: FileSystem.FileSystemUploadType.MULTIPART,
      fieldName: 'file',
      parameters: {
        creation_time: asset.creationTime.toString(),
        token: token,
      },
    });

    return res.status === 200 || res.status === 201;
  } catch (error) {
    console.error(`Error uploading asset ${asset.id}:`, error);
    return false;
  }
};

/**
 * Core sync logic used by both foreground UI and background fetch.
 */
export const runSyncCycle = async (callbacks?: SyncCallbacks) => {
  // 1. Check network
  const netInfo = await Network.getNetworkStateAsync();
  if (!netInfo.isConnected) {
    callbacks?.onInterrupted?.('Offline');
    return false;
  }

  // 2. Check cellular settings
  if (netInfo.type === Network.NetworkStateType.CELLULAR) {
    const syncOnCellular = await AsyncStorage.getItem(SYNC_ON_CELLULAR_KEY);
    if (syncOnCellular !== 'true') {
      callbacks?.onInterrupted?.('Waiting for Wi-Fi');
      return false;
    }
  }

  // 3. Fetch assets
  let media: MediaLibrary.PagedInfo<MediaLibrary.Asset>;
  try {
    // For background sync, we might not have permissions (though usually we do if UI was opened)
    const perms = await MediaLibrary.getPermissionsAsync();
    if (!perms.granted) {
      callbacks?.onInterrupted?.('No permissions');
      return false;
    }

    let localAssets: MediaLibrary.Asset[] = [];
    let hasNextPage = true;
    let after: string | undefined = undefined;
    
    while (hasNextPage) {
      const mediaPage = await MediaLibrary.getAssetsAsync({
        first: 1500,
        after,
        sortBy: [MediaLibrary.SortBy.creationTime],
        mediaType: [MediaLibrary.MediaType.photo, MediaLibrary.MediaType.video],
      });
      localAssets.push(...mediaPage.assets);
      hasNextPage = mediaPage.hasNextPage;
      after = mediaPage.endCursor;
      if (localAssets.length >= 10000) break; // Safe limit
    }
    media = { assets: localAssets } as any;
  } catch (e) {
    callbacks?.onInterrupted?.('Media Library Error');
    return false;
  }

  // 4. Determine what needs syncing
  let synced = await getSyncedList();
  const toSync = media.assets.filter(a => !synced.includes(a.id));

  if (!toSync.length) {
    callbacks?.onStatusChange?.('done', 0);
    return true; // Nothing to sync
  }

  callbacks?.onStatusChange?.('syncing', toSync.length);
  let ok = 0;

  // 5. Upload loop
  for (let i = 0; i < toSync.length; i++) {
    callbacks?.onProgress?.(i + 1, toSync.length);
    const asset = toSync[i];
    
    const success = await uploadAsset(asset);
    if (success) {
      synced = await addSyncedId(asset.id, synced);
      ok++;
      callbacks?.onStatusChange?.('syncing', toSync.length - ok);
    } else {
      callbacks?.onInterrupted?.('Sync Interrupted');
      return false;
    }
  }

  callbacks?.onStatusChange?.('done', 0);
  return true;
};

export const deleteFromCloud = async (cloudId: string): Promise<boolean> => {
  try {
    const token = await getToken();
    if (!token) return false;
    
    const res = await fetch(`${BACKEND_URL}/photos/delete/${cloudId}?token=${token}`, { method: 'DELETE' });
    return res.ok;
  } catch (e) {
    console.error('Failed to delete from cloud', e);
    return false;
  }
};

export const downloadToDevice = async (cloudUri: string, filename: string): Promise<boolean> => {
  try {
    const fileUri = `${FileSystem.documentDirectory}${filename}`;
    const { uri } = await FileSystem.downloadAsync(cloudUri, fileUri);
    const asset = await MediaLibrary.createAssetAsync(uri);
    return !!asset;
  } catch (e) {
    console.error('Failed to download to device', e);
    return false;
  }
};

// ─── Background Task Definition ─────────────────────────────────────
TaskManager.defineTask(BACKGROUND_SYNC_TASK, async () => {
  try {
    const isBgSyncEnabled = await AsyncStorage.getItem(BACKGROUND_SYNC_ENABLED_KEY);
    if (isBgSyncEnabled !== 'true') {
      return BackgroundFetch.BackgroundFetchResult.NoData;
    }

    const success = await runSyncCycle();
    return success 
      ? BackgroundFetch.BackgroundFetchResult.NewData 
      : BackgroundFetch.BackgroundFetchResult.Failed;
  } catch (error) {
    console.error('Background sync failed:', error);
    return BackgroundFetch.BackgroundFetchResult.Failed;
  }
});