From 99c0e1186ceedb26d24445e230705e9d97b510f8 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Wed, 25 Feb 2026 01:17:32 +0000 Subject: [PATCH] feat: refactor Google Auth handling and improve media file management with enhanced error logging --- src/AUTH/GoogleAUTH.js | 119 ++++++++++++++++++++++++++---------- src/components/PhotoGrid.js | 7 ++- src/screens/HomeScreen.js | 19 +++++- 3 files changed, 110 insertions(+), 35 deletions(-) diff --git a/src/AUTH/GoogleAUTH.js b/src/AUTH/GoogleAUTH.js index c8bb260..ae7f206 100644 --- a/src/AUTH/GoogleAUTH.js +++ b/src/AUTH/GoogleAUTH.js @@ -1,7 +1,7 @@ import * as AuthSession from 'expo-auth-session'; import * as WebBrowser from 'expo-web-browser'; import * as Application from 'expo-application'; -import * as FileSystem from 'expo-file-system'; +import { File, Directory, Paths } from 'expo-file-system'; import { AuthServiceBase } from './AuthServiceBase'; import { logError } from '../services/ErrorHandler'; @@ -30,7 +30,7 @@ const SCOPES = [ const PICKER_API = 'https://photospicker.googleapis.com/v1'; -const CACHE_DIR = `${FileSystem.cacheDirectory}google_photos/`; +const CACHE_DIR = new Directory(Paths.cache, 'google_photos'); const REDIRECT_URI = `${Application.applicationId}:/oauthredirect`; @@ -274,32 +274,55 @@ class GoogleAUTH extends AuthServiceBase { return { mediaItems: [], nextPageToken: null }; } - return await res.json(); + const data = await res.json(); + + // Debug: log raw response structure + if (data.mediaItems?.length) { + console.log('Picker raw item[0]:', JSON.stringify(data.mediaItems[0], null, 2)); + } else { + console.log('Picker returned no mediaItems. Keys:', Object.keys(data)); + } + + return data; } /** * Normalise Picker API media items to the app's standard format. * - * NOTE: The Picker API's baseUrl requires an Authorization header to - * fetch the image. We store it in `remote_path` / `picker_base_url` - * so the caller can download it to a local cache file for rendering. + * The Picker API response shape per item: + * { id, mediaFile: { baseUrl, mimeType, filename, mediaFileMetadata: { ... } } } + * + * baseUrl requires an Authorization header to fetch the actual image bytes. */ _normalisePickerItems(items) { - return (items || []).map((item) => { - const file = item.mediaFile || {}; - const isVideo = file.mimeType?.startsWith('video'); + if (!items?.length) return []; + + return items.map((item, idx) => { + // The item may have mediaFile nested, or may be flat + const file = item.mediaFile || item; + const baseUrl = file.baseUrl || item.baseUrl || null; + const filename = file.filename || item.filename || 'Untitled'; + const mimeType = file.mimeType || item.mimeType || null; + const isVideo = mimeType?.startsWith('video'); + const creationTime = file.mediaFileMetadata?.creationTime + || item.mediaFileMetadata?.creationTime + || null; + + if (idx === 0) { + console.log(`Normalising item[0]: id=${item.id}, baseUrl=${baseUrl?.substring(0, 60)}…, filename=${filename}, mime=${mimeType}`); + } + return { source_service: 'GOOGLE_PHOTOS', - source_id: item.id || file.id, - title: file.filename || 'Untitled', + source_id: item.id || file.id || `picker_${Date.now()}_${idx}`, + title: filename, description: '', - // local_path is null until we download it with the auth token local_path: null, - remote_path: file.baseUrl || '', - picker_base_url: file.baseUrl || null, + remote_path: baseUrl || '', + picker_base_url: baseUrl, media_type: isVideo ? 'video' : 'photo', - mime_type: file.mimeType || null, - created_date: file.mediaFileMetadata?.creationTime || null, + mime_type: mimeType, + created_date: creationTime, }; }); } @@ -318,9 +341,18 @@ class GoogleAUTH extends AuthServiceBase { const token = await this.getAccessToken(); // Ensure cache directory exists - const dirInfo = await FileSystem.getInfoAsync(CACHE_DIR); - if (!dirInfo.exists) { - await FileSystem.makeDirectoryAsync(CACHE_DIR, { intermediates: true }); + if (!CACHE_DIR.exists) { + CACHE_DIR.create({ intermediates: true }); + } else { + // Clean up stale files from previous failed downloads (< 1KB = likely error pages) + try { + const entries = CACHE_DIR.list(); + for (const entry of entries) { + if (entry instanceof File && entry.size < 1024) { + entry.delete(); + } + } + } catch (_) { /* ignore cleanup errors */ } } const results = []; @@ -331,31 +363,52 @@ class GoogleAUTH extends AuthServiceBase { } try { - // Use the source_id as a safe filename const safeId = (item.source_id || 'photo').replace(/[^a-zA-Z0-9_-]/g, '_'); const ext = item.mime_type?.includes('png') ? '.png' : item.mime_type?.includes('gif') ? '.gif' : item.mime_type?.includes('webp') ? '.webp' : '.jpg'; - const localUri = `${CACHE_DIR}${safeId}${ext}`; + const destFile = new File(CACHE_DIR, `${safeId}${ext}`); - // Skip download if already cached - const fileInfo = await FileSystem.getInfoAsync(localUri); - if (fileInfo.exists && fileInfo.size > 0) { - results.push({ ...item, local_path: localUri }); + // Skip download if already cached and non-empty + if (destFile.exists && destFile.size > 1024) { + results.push({ ...item, local_path: destFile.uri }); continue; } - // Download with auth header — append =d to get the full-res download - const downloadUrl = `${item.picker_base_url}=d`; - const download = await FileSystem.downloadAsync(downloadUrl, localUri, { - headers: { Authorization: `Bearer ${token}` }, - }); + // The Picker API baseUrl is a direct download link when used with + // the Authorization header. No suffix needed. + console.log(`Downloading: ${item.title} (${item.picker_base_url.substring(0, 80)}…)`); - if (download.status === 200) { - results.push({ ...item, local_path: download.uri }); + const downloaded = await File.downloadFileAsync( + item.picker_base_url, + destFile, + { headers: { Authorization: `Bearer ${token}` }, idempotent: true } + ); + + console.log(` → uri=${downloaded.uri}, size=${downloaded.size}`); + + if (downloaded.exists && downloaded.size > 1024) { + results.push({ ...item, local_path: downloaded.uri }); + } else if (downloaded.exists && downloaded.size > 0) { + // Suspiciously small — check if it's an error page + try { + const peek = await downloaded.text(); + if (peek.startsWith('<') || peek.startsWith('{')) { + logError('Download Picker Photo', + `Got error page for ${item.title}: ${peek.substring(0, 150)}`); + downloaded.delete(); + results.push(item); // no local_path + } else { + results.push({ ...item, local_path: downloaded.uri }); + } + } catch (_) { + // Can't read as text = probably binary = probably an image + results.push({ ...item, local_path: downloaded.uri }); + } } else { - logError('Download Picker Photo', `HTTP ${download.status} for ${item.title}`); + logError('Download Picker Photo', `Empty file for ${item.title}`); + if (downloaded.exists) downloaded.delete(); results.push(item); } } catch (err) { diff --git a/src/components/PhotoGrid.js b/src/components/PhotoGrid.js index 4b56f3a..c7b252e 100644 --- a/src/components/PhotoGrid.js +++ b/src/components/PhotoGrid.js @@ -54,7 +54,12 @@ const PhotoGrid = ({ activeOpacity={0.8} > {item.local_path ? ( - + console.warn(`Image load failed [${item.source_service}]: ${item.local_path?.substring(0, 80)}`, e.nativeEvent?.error)} + /> ) : ( 📷 diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index 49099f0..a615533 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -8,6 +8,7 @@ import DatabaseService from '../services/DatabaseService'; import DeviceMediaService from '../services/DeviceMediaService'; import GoogleAUTH from '../AUTH/GoogleAUTH'; import NASAUTH from '../AUTH/NASAUTH'; +import { File } from 'expo-file-system'; import TopBar from '../components/TopBar'; import PhotoGrid from '../components/PhotoGrid'; import MediaViewer from '../components/MediaViewer'; @@ -132,7 +133,23 @@ const HomeScreen = ({ navigation, route }) => { // ─── Previously imported Google Photos (stored in DB) ─── try { const googleMedia = await DatabaseService.getMediaByService(userId, 'GOOGLE_PHOTOS'); - if (googleMedia?.length) newMedia.push(...googleMedia); + if (googleMedia?.length) { + // Validate cached files still exist — clear stale paths + const validated = googleMedia.map((item) => { + if (item.local_path) { + try { + const f = new File(item.local_path); + if (!f.exists || f.size === 0) { + return { ...item, local_path: null }; + } + } catch (_) { + return { ...item, local_path: null }; + } + } + return item; + }); + newMedia.push(...validated); + } } catch (err) { console.error('DB Google media fetch error:', err); }