import { Bytes } from '@ethersphere/bee-js' import { Box, Typography } from '@mui/material' import { saveAs } from 'file-saver' import JSZip from 'jszip' import { useSnackbar } from 'notistack' import { ReactElement, useContext, useEffect, useRef, useState } from 'react' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { HistoryHeader } from '../../components/HistoryHeader' import { Loading } from '../../components/Loading' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import { META_FILE_NAME } from '../../constants' import { Context as BeeContext } from '../../providers/Bee' import { Context as SettingsContext } from '../../providers/Settings' import { ROUTES } from '../../routes' import { determineHistoryName, LocalStorageKeys, putHistory } from '../../utils/localStorage' import { loadManifest } from '../../utils/manifest' import { AssetPreview, getType } from './AssetPreview' import { AssetSummary } from './AssetSummary' import { AssetSyncing } from './AssetSyncing' import { DownloadActionBar } from './DownloadActionBar' export function Share(): ReactElement { const { apiUrl, beeApi } = useContext(SettingsContext) const { status } = useContext(BeeContext) const { hash } = useParams() const location = useLocation() const fromUpload = Boolean(location.state?.fromUpload) const navigate = useNavigate() const { enqueueSnackbar } = useSnackbar() const [loading, setLoading] = useState(true) const [downloading, setDownloading] = useState(false) const [swarmEntries, setSwarmEntries] = useState>({}) const [indexDocument, setIndexDocument] = useState(null) const [notFound, setNotFound] = useState(false) const [preview, setPreview] = useState(undefined) const [metadata, setMetadata] = useState() const isMountedRef = useRef(true) function applyFallbackMetadata(entries: Record, indexDocument: string | null) { const count = Object.keys(entries).length const isVideo = Boolean(indexDocument && /.*\.(mp4|webm|ogv)$/i.test(indexDocument)) const isAudio = Boolean(indexDocument && /.*\.(mp3|ogg|oga|wav|webm|m4a|aac|flac)$/i.test(indexDocument)) const isImage = Boolean(indexDocument && /.*\.(jpg|jpeg|png|gif|webp|svg|ico)$/i.test(indexDocument)) if (isImage || isVideo || isAudio) { setPreview(`${apiUrl}/bzz/${hash}`) } setMetadata({ hash, type: count > 1 ? 'folder' : getType(), name: indexDocument || hash || '', count, isWebsite: Boolean(indexDocument && /.*\.html?$/i.test(indexDocument)), isVideo, isAudio, isImage, }) } async function prepare() { if (!beeApi || !status.all || !hash) { return } try { const manifest = await loadManifest(beeApi, hash) const entries = manifest.collectAndMap() delete entries[META_FILE_NAME] if (!isMountedRef.current) return setSwarmEntries(entries) const docsMetadata = manifest.getDocsMetadata() // needed in catch block, shadows the outer variable const indexDocument = docsMetadata.indexDocument if (!isMountedRef.current) return setIndexDocument(indexDocument) try { const remoteMetadata = await beeApi.downloadFile(hash, META_FILE_NAME) const formattedMetadata = remoteMetadata.data.toJSON() as Metadata if (formattedMetadata.isVideo || formattedMetadata.isAudio || formattedMetadata.isImage) { if (!isMountedRef.current) return setPreview(`${apiUrl}/bzz/${hash}`) } if (!isMountedRef.current) return setMetadata({ ...formattedMetadata, hash }) } catch { // if metadata is not available or invalid go with the default one if (!isMountedRef.current) return applyFallbackMetadata(entries, indexDocument) } } catch { if (!isMountedRef.current) return setNotFound(true) enqueueSnackbar('The specified hash does not contain valid content.', { variant: 'error' }) return } } function onOpen() { if (metadata?.isImage) { const imgUrl = `${apiUrl}/bzz/${hash}` const safeName = (metadata?.name ?? hash).replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`) const html = `${safeName}${safeName}` const blob = new Blob([html], { type: 'text/html' }) const blobUrl = URL.createObjectURL(blob) window.open(blobUrl, '_blank', 'noopener,noreferrer') } else { window.open(`${apiUrl}/bzz/${hash}/`, '_blank', 'noopener,noreferrer') } } function onClose() { if (navigate.length > 0) { // There is at least one different route in browser history that we can return to navigate(-1) } else { // This is the first page user opened, navigate to upload page instead of going back navigate(ROUTES.UPLOAD) } } function onUpdateFeed() { if (!hash) { // eslint-disable-next-line no-console console.error('hash is invalid') return } navigate(ROUTES.ACCOUNT_FEEDS_UPDATE.replace(':hash', hash)) } useEffect(() => { isMountedRef.current = true return () => { isMountedRef.current = false } }, []) useEffect(() => { setLoading(true) prepare().finally(() => { if (!isMountedRef.current) return setLoading(false) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [hash]) function downloadFailedWithError(err: unknown) { const msg = Error: {err instanceof Error ? err.message : String(err)} enqueueSnackbar(msg, { variant: 'error' }) setDownloading(false) } async function onDownload() { if (!beeApi || !hash) { // eslint-disable-next-line no-console console.error('hash is invalid') return } putHistory(LocalStorageKeys.downloadHistory, hash, determineHistoryName(hash, indexDocument)) setDownloading(true) if (Object.keys(swarmEntries).length === 1) { const singleFileName = Object.keys(swarmEntries)[0] const singleFileHash = Object.values(swarmEntries)[0] let fileData: Bytes try { fileData = await beeApi.downloadData(singleFileHash) } catch (err) { // eslint-disable-next-line no-console console.error('Failed to download file: ', err) downloadFailedWithError(err) return } const dataArray = fileData.toUint8Array() const arrayBuffer = new ArrayBuffer(dataArray.length) const view = new Uint8Array(arrayBuffer) view.set(dataArray) const blob = new Blob([arrayBuffer], { type: metadata?.type || 'application/octet-stream' }) saveAs(blob, metadata?.name || singleFileName || hash) } else { const zip = new JSZip() for (const [path, hash] of Object.entries(swarmEntries)) { try { zip.file(path, (await beeApi.downloadData(hash)).toUint8Array()) } catch (err) { // eslint-disable-next-line no-console console.error('Failed to download files: ', err) downloadFailedWithError(err) return } } let content: Blob try { content = await zip.generateAsync({ type: 'blob' }) } catch (err) { // eslint-disable-next-line no-console console.error('Failed to compress file: ', err) downloadFailedWithError(err) return } saveAs(content, hash + '.zip') } if (!isMountedRef.current) return setDownloading(false) } if (!status.all) return if (loading) { return } if (notFound) { return ( <> Not Found The specified hash is not found. ) } return ( <> {fromUpload && ( )} ) }