mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-18 09:41:47 +00:00
127 lines
3.7 KiB
TypeScript
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;
|
|
}
|