Files
topola-viewer/src/datasource/google_drive.ts
2026-06-23 18:53:21 +02:00

127 lines
3.7 KiB
TypeScript

import type {ParsedQuery} from 'query-string';
import {TopolaData} from '../util/gedcom_util';
import {DataSource, DataSourceEnum, SourceSelection} from './data_source';
import {
GOOGLE_DRIVE_CACHE_KEY_PREFIX,
googleDriveService,
} from './google_drive_service';
import {loadAndPrepareFile} from './load_data';
/** Specification for Google Drive data source. */
export interface GoogleDriveSourceSpec {
source: DataSourceEnum.GOOGLE_DRIVE;
/** The unique ID of the file in Google Drive. */
fileId: string;
}
/** Error thrown when Google Drive authentication or authorization fails or is missing. */
export class GoogleDriveAuthError extends Error {
constructor(message?: string) {
super(message);
this.name = 'GoogleDriveAuthError';
// Set the prototype explicitly.
Object.setPrototypeOf(this, GoogleDriveAuthError.prototype);
}
}
/** Data source representing family tree data loaded from Google Drive. */
export class GoogleDriveDataSource implements DataSource<GoogleDriveSourceSpec> {
isNewData(
newSource: SourceSelection<GoogleDriveSourceSpec>,
oldSource: SourceSelection<GoogleDriveSourceSpec>,
_data?: TopolaData,
): boolean {
return newSource.spec.fileId !== oldSource.spec.fileId;
}
async loadData(
source: SourceSelection<GoogleDriveSourceSpec>,
onProgress?: (status: string) => void,
): Promise<TopolaData> {
const fileId = source.spec.fileId;
const cacheKey = `${GOOGLE_DRIVE_CACHE_KEY_PREFIX}${fileId}`;
// 1. Check sessionStorage cache
try {
const cachedData = sessionStorage.getItem(cacheKey);
if (cachedData) {
return JSON.parse(cachedData);
}
} catch (e) {
console.warn('Failed to load data from session storage: ' + e);
}
// 2. Get the active access token
const token = googleDriveService.getAccessToken();
if (!token) {
throw new GoogleDriveAuthError(
'No active Google Drive access token found.',
);
}
// 3. Fetch file content from Google Drive API
const response = await window
.fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
.catch((error) => {
throw new Error(
`Network error fetching file from Google Drive: ${(error as Error).message}`,
);
});
// 4. Inspect response status
if (
response.status === 401 ||
response.status === 403 ||
response.status === 404
) {
throw new GoogleDriveAuthError(
`Access denied or file not found (HTTP ${response.status}).`,
);
}
if (response.status !== 200) {
throw new Error(
`Failed to fetch file from Google Drive (HTTP ${response.status}): ${response.statusText}`,
);
}
// 5. Parse and prepare the file content
const blob = await response.blob();
return loadAndPrepareFile(blob, cacheKey, onProgress);
}
}
export const GOOGLE_DRIVE_REDIRECT_KEYS = ['state'];
export function handleGoogleDriveRedirect(
mergedParams: ParsedQuery,
): {redirectPath: string; fileId: string} | null {
const stateParam = mergedParams.state;
if (typeof stateParam === 'string') {
try {
const parsedState = JSON.parse(stateParam);
if (
parsedState &&
parsedState.action === 'open' &&
Array.isArray(parsedState.ids) &&
parsedState.ids.length > 0
) {
return {
redirectPath: '/view',
fileId: parsedState.ids[0],
};
}
} catch (err) {
// Silently catch JSON parsing errors for state parameters not meant for us
console.warn(
'Google Drive state query parameter JSON parsing failed or action mismatch:',
err,
);
}
}
return null;
}