feat: refactor Google Auth handling and improve media file management with enhanced error logging

This commit is contained in:
bobbert committed 2026-02-25 01:17:32 +00:00
1 parent ad0cc1a7f5
commit 99c0e1186c
3 files changed
+110 -35

No files matched your search

+86 -33
View File
@@ -1,7 +1,7 @@
import * as AuthSession from 'expo-auth-session'; import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser'; import * as WebBrowser from 'expo-web-browser';
import * as Application from 'expo-application'; 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 { AuthServiceBase } from './AuthServiceBase';
import { logError } from '../services/ErrorHandler'; import { logError } from '../services/ErrorHandler';
@@ -30,7 +30,7 @@ const SCOPES = [
const PICKER_API = 'https://photospicker.googleapis.com/v1'; 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`; const REDIRECT_URI = `${Application.applicationId}:/oauthredirect`;
@@ -274,32 +274,55 @@ class GoogleAUTH extends AuthServiceBase {
return { mediaItems: [], nextPageToken: null }; 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. * Normalise Picker API media items to the app's standard format.
* *
* NOTE: The Picker API's baseUrl requires an Authorization header to * The Picker API response shape per item:
* fetch the image. We store it in `remote_path` / `picker_base_url` * { id, mediaFile: { baseUrl, mimeType, filename, mediaFileMetadata: { ... } } }
* so the caller can download it to a local cache file for rendering. *
* baseUrl requires an Authorization header to fetch the actual image bytes.
*/ */
_normalisePickerItems(items) { _normalisePickerItems(items) {
return (items || []).map((item) => { if (!items?.length) return [];
const file = item.mediaFile || {};
const isVideo = file.mimeType?.startsWith('video'); 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 { return {
source_service: 'GOOGLE_PHOTOS', source_service: 'GOOGLE_PHOTOS',
source_id: item.id || file.id, source_id: item.id || file.id || `picker_${Date.now()}_${idx}`,
title: file.filename || 'Untitled', title: filename,
description: '', description: '',
// local_path is null until we download it with the auth token
local_path: null, local_path: null,
remote_path: file.baseUrl || '', remote_path: baseUrl || '',
picker_base_url: file.baseUrl || null, picker_base_url: baseUrl,
media_type: isVideo ? 'video' : 'photo', media_type: isVideo ? 'video' : 'photo',
mime_type: file.mimeType || null, mime_type: mimeType,
created_date: file.mediaFileMetadata?.creationTime || null, created_date: creationTime,
}; };
}); });
} }
@@ -318,9 +341,18 @@ class GoogleAUTH extends AuthServiceBase {
const token = await this.getAccessToken(); const token = await this.getAccessToken();
// Ensure cache directory exists // Ensure cache directory exists
const dirInfo = await FileSystem.getInfoAsync(CACHE_DIR); if (!CACHE_DIR.exists) {
if (!dirInfo.exists) { CACHE_DIR.create({ intermediates: true });
await FileSystem.makeDirectoryAsync(CACHE_DIR, { 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 = []; const results = [];
@@ -331,31 +363,52 @@ class GoogleAUTH extends AuthServiceBase {
} }
try { try {
// Use the source_id as a safe filename
const safeId = (item.source_id || 'photo').replace(/[^a-zA-Z0-9_-]/g, '_'); const safeId = (item.source_id || 'photo').replace(/[^a-zA-Z0-9_-]/g, '_');
const ext = item.mime_type?.includes('png') ? '.png' const ext = item.mime_type?.includes('png') ? '.png'
: item.mime_type?.includes('gif') ? '.gif' : item.mime_type?.includes('gif') ? '.gif'
: item.mime_type?.includes('webp') ? '.webp' : item.mime_type?.includes('webp') ? '.webp'
: '.jpg'; : '.jpg';
const localUri = `${CACHE_DIR}${safeId}${ext}`; const destFile = new File(CACHE_DIR, `${safeId}${ext}`);
// Skip download if already cached // Skip download if already cached and non-empty
const fileInfo = await FileSystem.getInfoAsync(localUri); if (destFile.exists && destFile.size > 1024) {
if (fileInfo.exists && fileInfo.size > 0) { results.push({ ...item, local_path: destFile.uri });
results.push({ ...item, local_path: localUri });
continue; continue;
} }
// Download with auth header — append =d to get the full-res download // The Picker API baseUrl is a direct download link when used with
const downloadUrl = `${item.picker_base_url}=d`; // the Authorization header. No suffix needed.
const download = await FileSystem.downloadAsync(downloadUrl, localUri, { console.log(`Downloading: ${item.title} (${item.picker_base_url.substring(0, 80)}…)`);
headers: { Authorization: `Bearer ${token}` },
});
if (download.status === 200) { const downloaded = await File.downloadFileAsync(
results.push({ ...item, local_path: download.uri }); 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 { } 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); results.push(item);
} }
} catch (err) { } catch (err) {
+6 -1
View File
@@ -54,7 +54,12 @@ const PhotoGrid = ({
activeOpacity={0.8} activeOpacity={0.8}
> >
{item.local_path ? ( {item.local_path ? (
<Image source={{ uri: item.local_path }} style={styles.gridImage} resizeMode="cover" /> <Image
source={{ uri: item.local_path }}
style={styles.gridImage}
resizeMode="cover"
onError={(e) => console.warn(`Image load failed [${item.source_service}]: ${item.local_path?.substring(0, 80)}`, e.nativeEvent?.error)}
/>
) : ( ) : (
<View style={[styles.placeholder, { backgroundColor: theme.borderLight }]}> <View style={[styles.placeholder, { backgroundColor: theme.borderLight }]}>
<Text style={{ fontSize: 28 }}>📷</Text> <Text style={{ fontSize: 28 }}>📷</Text>
+18 -1
View File
@@ -8,6 +8,7 @@ import DatabaseService from '../services/DatabaseService';
import DeviceMediaService from '../services/DeviceMediaService'; import DeviceMediaService from '../services/DeviceMediaService';
import GoogleAUTH from '../AUTH/GoogleAUTH'; import GoogleAUTH from '../AUTH/GoogleAUTH';
import NASAUTH from '../AUTH/NASAUTH'; import NASAUTH from '../AUTH/NASAUTH';
import { File } from 'expo-file-system';
import TopBar from '../components/TopBar'; import TopBar from '../components/TopBar';
import PhotoGrid from '../components/PhotoGrid'; import PhotoGrid from '../components/PhotoGrid';
import MediaViewer from '../components/MediaViewer'; import MediaViewer from '../components/MediaViewer';
@@ -132,7 +133,23 @@ const HomeScreen = ({ navigation, route }) => {
// ─── Previously imported Google Photos (stored in DB) ─── // ─── Previously imported Google Photos (stored in DB) ───
try { try {
const googleMedia = await DatabaseService.getMediaByService(userId, 'GOOGLE_PHOTOS'); 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) { } catch (err) {
console.error('DB Google media fetch error:', err); console.error('DB Google media fetch error:', err);
} }