Co-authored-by: ConnorYoh <[email protected]>
Co-authored-by: Connor Yoh <[email protected]>
Co-authored-by: EthanHealy01 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
Anthony Stirling
2026-03-25 11:00:40 +00:00
committed by GitHub
co-authored by ConnorYoh Connor Yoh EthanHealy01 Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent 47cad0a131
commit 28613caf8a
181 changed files with 25715 additions and 124 deletions
@@ -0,0 +1,256 @@
import { useEffect, useMemo, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import apiClient from '@app/services/apiClient';
import { useAuth } from '@app/auth/UseSession';
import { useFileActions } from '@app/contexts/FileContext';
import { useNavigationActions } from '@app/contexts/NavigationContext';
import { alert } from '@app/components/toast';
import type { StirlingFile } from '@app/types/fileContext';
import type { FileId } from '@app/types/file';
import { fileStorage } from '@app/services/fileStorage';
import {
getShareBundleEntryRootId,
isZipBundle,
loadShareBundleEntries,
parseContentDispositionFilename,
} from '@app/services/shareBundleUtils';
interface ShareLinkLoaderProps {
token: string;
}
interface ShareLinkMetadata {
shareToken?: string;
fileId?: number;
fileName?: string;
owner?: string | null;
ownedByCurrentUser?: boolean;
accessRole?: string | null;
expiresAt?: string;
}
export default function ShareLinkLoader({ token }: ShareLinkLoaderProps) {
const { actions } = useFileActions();
const { actions: navActions } = useNavigationActions();
const { user, loading: authLoading } = useAuth();
const navigate = useNavigate();
const { t } = useTranslation();
const handledTokenRef = useRef<string | null>(null);
const normalizedToken = useMemo(() => token.trim(), [token]);
const isAuthenticated = Boolean(user);
useEffect(() => {
if (!normalizedToken) {
return;
}
if (handledTokenRef.current === normalizedToken) {
return;
}
handledTokenRef.current = normalizedToken;
const abortController = new AbortController();
const { signal } = abortController;
const loadSharedFile = async () => {
try {
let shareMetadata: ShareLinkMetadata | null = null;
try {
const metadataResponse = await apiClient.get<ShareLinkMetadata>(
`/api/v1/storage/share-links/${normalizedToken}/metadata`,
{ suppressErrorToast: true, skipAuthRedirect: true, signal }
);
shareMetadata = metadataResponse.data;
} catch {
shareMetadata = null;
}
const response = await apiClient.get(`/api/v1/storage/share-links/${normalizedToken}`, {
responseType: 'blob',
suppressErrorToast: true,
skipAuthRedirect: true,
signal,
});
if (signal.aborted) return;
const contentType =
(response.headers && (response.headers['content-type'] || response.headers['Content-Type'])) ||
'';
const disposition =
(response.headers &&
(response.headers['content-disposition'] || response.headers['Content-Disposition'])) ||
'';
const filename = parseContentDispositionFilename(disposition) || 'shared-file';
const blob = response.data as Blob;
const contentTypeValue = contentType || blob.type;
if (isZipBundle(contentTypeValue, filename)) {
const bundle = await loadShareBundleEntries(blob);
if (bundle) {
const { manifest, rootOrder, sortedEntries, files } = bundle;
const stirlingFiles = await actions.addFilesWithOptions(files, {
selectFiles: false,
autoUnzip: false,
skipAutoUnzip: false,
allowDuplicates: true,
});
if (signal.aborted) return;
const idMap = new Map<string, FileId>();
for (let i = 0; i < stirlingFiles.length; i += 1) {
idMap.set(sortedEntries[i].logicalId, stirlingFiles[i].fileId as FileId);
}
const rootIdMap = new Map<string, FileId>();
for (const rootLogicalId of rootOrder) {
const mappedId = idMap.get(rootLogicalId);
if (mappedId) {
rootIdMap.set(rootLogicalId, mappedId);
}
}
for (let i = 0; i < sortedEntries.length; i += 1) {
const entry = sortedEntries[i];
const newId = idMap.get(entry.logicalId);
if (!newId) continue;
const parentId = entry.parentLogicalId
? idMap.get(entry.parentLogicalId)
: undefined;
const rootId =
rootIdMap.get(getShareBundleEntryRootId(manifest, entry)) ||
idMap.get(manifest.rootLogicalId) ||
newId;
const sharedUpdates = {
remoteStorageId: shareMetadata?.fileId,
remoteOwnerUsername: shareMetadata?.owner ?? undefined,
remoteOwnedByCurrentUser: false,
remoteAccessRole: shareMetadata?.accessRole ?? undefined,
remoteSharedViaLink: true,
remoteHasShareLinks: false,
remoteShareToken: shareMetadata?.shareToken || normalizedToken,
};
actions.updateStirlingFileStub(newId, {
versionNumber: entry.versionNumber,
originalFileId: rootId,
parentFileId: parentId,
toolHistory: entry.toolHistory,
isLeaf: entry.isLeaf,
...sharedUpdates,
});
await fileStorage.updateFileMetadata(newId, {
versionNumber: entry.versionNumber,
originalFileId: rootId,
parentFileId: parentId,
toolHistory: entry.toolHistory,
isLeaf: entry.isLeaf,
...sharedUpdates,
});
}
const selectedIds: FileId[] = [];
for (const rootId of rootOrder) {
const rootEntries = sortedEntries.filter(
(entry) => getShareBundleEntryRootId(manifest, entry) === rootId
);
const latestEntry = rootEntries[rootEntries.length - 1];
if (!latestEntry) {
continue;
}
const latestId = idMap.get(latestEntry.logicalId);
if (latestId) {
selectedIds.push(latestId);
}
}
if (selectedIds.length > 0) {
actions.setSelectedFiles(selectedIds);
}
navActions.setWorkbench('viewer');
navigate('/', { replace: true });
return;
}
}
const file = new File([blob], filename, { type: contentTypeValue || blob.type });
const stirlingFiles = await actions.addFilesWithOptions([file], {
selectFiles: true,
autoUnzip: false,
skipAutoUnzip: false,
});
if (signal.aborted) return;
if (stirlingFiles.length > 0) {
const ids = stirlingFiles.map((stirlingFile: StirlingFile) => stirlingFile.fileId);
actions.setSelectedFiles(ids);
const sharedUpdates = {
remoteStorageId: shareMetadata?.fileId,
remoteOwnerUsername: shareMetadata?.owner ?? undefined,
remoteOwnedByCurrentUser: false,
remoteAccessRole: shareMetadata?.accessRole ?? undefined,
remoteSharedViaLink: true,
remoteHasShareLinks: false,
remoteShareToken: shareMetadata?.shareToken || normalizedToken,
};
for (const fileId of ids) {
actions.updateStirlingFileStub(fileId, sharedUpdates);
await fileStorage.updateFileMetadata(fileId, sharedUpdates);
}
}
navActions.setWorkbench('viewer');
navigate('/', { replace: true });
} catch (error: any) {
if (signal.aborted) return;
const status = error?.response?.status;
if (status === 401 || status === 403) {
if (!isAuthenticated && !authLoading) {
alert({
alertType: 'warning',
title: t('storageShare.requiresLogin', 'This shared file requires login.'),
expandable: false,
durationMs: 4000,
});
navigate('/login', {
replace: true,
state: { from: { pathname: `/share/${normalizedToken}` } },
});
return;
}
alert({
alertType: 'warning',
title: t(
'storageShare.accessDenied',
'You do not have access to this shared file. Ask the owner to share it with you.'
),
expandable: false,
durationMs: 4500,
});
navigate('/', { replace: true });
} else if (status === 404 || status === 410) {
alert({
alertType: 'error',
title: t('storageShare.expiredTitle', 'Link expired'),
expandable: false,
durationMs: 4000,
});
navigate('/', { replace: true });
} else {
alert({
alertType: 'error',
title: t('storageShare.loadFailed', 'Unable to open shared file.'),
expandable: false,
durationMs: 4000,
});
}
}
};
loadSharedFile();
return () => {
abortController.abort();
};
}, [normalizedToken, actions, navActions, navigate, t, isAuthenticated, authLoading]);
return null;
}