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 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) {