feat: add metadata and preview (#292)
* chore: upload flow uses metadata object and has preview * chore: remove SwarmFile * feat: upload metadata and file preview * feat: add metadata and preview on download * fix: package the meta and preview files * fix: upload websites that are inside a folder (#296) * fix: upload websites that are inside a folder * docs: few comments to clarify what is going on * refactor: decrease local variables and fix state order to detect websites properly Co-authored-by: Cafe137 <aron@aronsoos.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export const META_FILE_NAME = '.swarmgatewaymeta.json'
|
||||
export const PREVIEW_FILE_NAME = '.swarmgatewaypreview.jpeg'
|
||||
export const PREVIEW_DIMENSIONS = { maxWidth: 250, maxHeight: 175 }
|
||||
@@ -1,99 +1,55 @@
|
||||
import { Box, Grid, Typography } from '@material-ui/core'
|
||||
import { Web } from '@material-ui/icons'
|
||||
import { ReactElement, useEffect, useState } from 'react'
|
||||
import { ReactElement } from 'react'
|
||||
import { File, Folder } from 'react-feather'
|
||||
import { FitImage } from '../../components/FitImage'
|
||||
import { detectIndexHtml, getAssetNameFromFiles, getHumanReadableFileSize } from '../../utils/file'
|
||||
import { SwarmFile } from '../../utils/SwarmFile'
|
||||
import { getHumanReadableFileSize } from '../../utils/file'
|
||||
import { AssetIcon } from './AssetIcon'
|
||||
import { shortenHash } from '../../utils/hash'
|
||||
|
||||
interface Props {
|
||||
assetName?: string
|
||||
files: SwarmFile[]
|
||||
previewUri?: string
|
||||
metadata?: Metadata
|
||||
}
|
||||
|
||||
// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
|
||||
|
||||
export function AssetPreview({ assetName, files }: Props): ReactElement {
|
||||
const [previewComponent, setPreviewComponent] = useState<ReactElement | undefined>(undefined)
|
||||
const [previewUri, setPreviewUri] = useState<string | undefined>(undefined)
|
||||
export function AssetPreview({ metadata, previewUri }: Props): ReactElement | null {
|
||||
let previewComponent = <File />
|
||||
let type = metadata?.type
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length === 1) {
|
||||
// single image
|
||||
if (files[0].type.startsWith('image/')) {
|
||||
files[0].arrayBuffer().then(value => {
|
||||
const blob = new Blob([value])
|
||||
setPreviewUri(URL.createObjectURL(blob))
|
||||
})
|
||||
// single non-image
|
||||
} else {
|
||||
setPreviewUri(undefined)
|
||||
setPreviewComponent(<AssetIcon icon={<File />} />)
|
||||
}
|
||||
// collection
|
||||
} else if (detectIndexHtml(files)) {
|
||||
setPreviewUri(undefined)
|
||||
setPreviewComponent(<AssetIcon icon={<Web />} />)
|
||||
} else {
|
||||
setPreviewUri(undefined)
|
||||
setPreviewComponent(<AssetIcon icon={<Folder />} />)
|
||||
}
|
||||
}, [files])
|
||||
|
||||
const getPrimaryText = () => {
|
||||
const name = getAssetNameFromFiles(files)
|
||||
|
||||
if (files.length === 1) {
|
||||
return 'Filename: ' + (assetName || name)
|
||||
}
|
||||
|
||||
return 'Folder name: ' + (assetName || name)
|
||||
if (metadata?.isWebsite) {
|
||||
previewComponent = <Web />
|
||||
type = 'Website'
|
||||
} else if (metadata?.type === 'folder') {
|
||||
previewComponent = <Folder />
|
||||
type = 'Folder'
|
||||
}
|
||||
|
||||
const getKind = () => {
|
||||
if (files.length === 1) {
|
||||
return files[0].type
|
||||
}
|
||||
|
||||
if (detectIndexHtml(files)) {
|
||||
return 'Website'
|
||||
}
|
||||
|
||||
return 'Folder'
|
||||
}
|
||||
|
||||
const isFolder = () => ['Folder', 'Website'].includes(getKind())
|
||||
|
||||
const getSize = () => {
|
||||
const bytes = files.reduce((total, item) => total + item.size, 0)
|
||||
|
||||
return getHumanReadableFileSize(bytes)
|
||||
}
|
||||
|
||||
const size = getSize()
|
||||
|
||||
return (
|
||||
<Box mb={4}>
|
||||
<Box bgcolor="background.paper">
|
||||
<Grid container direction="row">
|
||||
{previewComponent ? (
|
||||
previewComponent
|
||||
) : (
|
||||
{previewUri ? (
|
||||
<FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
|
||||
) : (
|
||||
<AssetIcon icon={previewComponent} />
|
||||
)}
|
||||
<Box p={2}>
|
||||
<Typography>{getPrimaryText()}</Typography>
|
||||
<Typography>Kind: {getKind()}</Typography>
|
||||
{size !== '0 bytes' && <Typography>Size: {size}</Typography>}
|
||||
{metadata?.hash && <Typography>Swarm Hash: {shortenHash(metadata.hash)}</Typography>}
|
||||
<Typography>
|
||||
{metadata?.type === 'folder' ? 'Folder Name' : 'Filename'}: {metadata?.name}
|
||||
</Typography>
|
||||
<Typography>Kind: {type}</Typography>
|
||||
{metadata?.size && <Typography>Size: {getHumanReadableFileSize(metadata.size)}</Typography>}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Box>
|
||||
{isFolder() && (
|
||||
{metadata?.type === 'folder' && metadata.count && (
|
||||
<Box mt={0.25} p={2} bgcolor="background.paper">
|
||||
<Grid container justifyContent="space-between" alignItems="center" direction="row">
|
||||
<Typography variant="subtitle2">Folder content</Typography>
|
||||
<Typography variant="subtitle2">{files.length} items</Typography>
|
||||
<Typography variant="subtitle2">{metadata.count} items</Typography>
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -4,21 +4,19 @@ import { ReactElement } from 'react'
|
||||
import { DocumentationText } from '../../components/DocumentationText'
|
||||
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||
import ExpandableListItemLink from '../../components/ExpandableListItemLink'
|
||||
import { detectIndexHtml } from '../../utils/file'
|
||||
import { SwarmFile } from '../../utils/SwarmFile'
|
||||
|
||||
interface Props {
|
||||
files: SwarmFile[]
|
||||
isWebsite?: boolean
|
||||
hash: string
|
||||
}
|
||||
|
||||
export function AssetSummary({ files, hash }: Props): ReactElement {
|
||||
export function AssetSummary({ isWebsite, hash }: Props): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Box mb={4}>
|
||||
<ExpandableListItemKey label="Swarm hash" value={hash} />
|
||||
<ExpandableListItemLink label="Share on Swarm Gateway" value={`https://gateway.ethswarm.org/access/${hash}`} />
|
||||
{detectIndexHtml(files) && (
|
||||
{isWebsite && (
|
||||
<ExpandableListItemLink
|
||||
label="BZZ Link"
|
||||
value={`https://${swarmCid.encodeManifestReference(hash).toString()}.bzz.link`}
|
||||
|
||||
+35
-16
@@ -4,17 +4,16 @@ import { saveAs } from 'file-saver'
|
||||
import JSZip from 'jszip'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||
import { Loading } from '../../components/Loading'
|
||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||
import config from '../../config'
|
||||
import { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants'
|
||||
import { Context as BeeContext } from '../../providers/Bee'
|
||||
import { Context as SettingsContext } from '../../providers/Settings'
|
||||
import { ROUTES } from '../../routes'
|
||||
import { convertBeeFileToBrowserFile, convertManifestToFiles } from '../../utils/file'
|
||||
import { shortenHash } from '../../utils/hash'
|
||||
import { determineHistoryName, HISTORY_KEYS, putHistory } from '../../utils/local-storage'
|
||||
import { SwarmFile } from '../../utils/SwarmFile'
|
||||
import { AssetPreview } from './AssetPreview'
|
||||
import { AssetSummary } from './AssetSummary'
|
||||
import { DownloadActionBar } from './DownloadActionBar'
|
||||
@@ -31,10 +30,11 @@ export function Share(): ReactElement {
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const [files, setFiles] = useState<SwarmFile[]>([])
|
||||
const [swarmEntries, setSwarmEntries] = useState<Record<string, string>>({})
|
||||
const [indexDocument, setIndexDocument] = useState<string | null>(null)
|
||||
const [notFound, setNotFound] = useState(false)
|
||||
const [preview, setPreview] = useState<string | undefined>(undefined)
|
||||
const [metadata, setMetadata] = useState<Metadata | undefined>()
|
||||
|
||||
async function prepare() {
|
||||
if (!beeApi || !status.all) {
|
||||
@@ -51,16 +51,37 @@ export function Share(): ReactElement {
|
||||
return
|
||||
}
|
||||
const entries = await manifestJs.getHashes(reference)
|
||||
setSwarmEntries(entries)
|
||||
const indexDocument = await manifestJs.getIndexDocumentPath(reference)
|
||||
setIndexDocument(indexDocument)
|
||||
|
||||
if (Object.keys(entries).length === 1) {
|
||||
const response = await beeApi.downloadFile(reference)
|
||||
setFiles([new SwarmFile(convertBeeFileToBrowserFile(response) as File)])
|
||||
} else {
|
||||
setFiles(convertManifestToFiles(entries))
|
||||
const previewFile = entries[PREVIEW_FILE_NAME]
|
||||
|
||||
delete entries[META_FILE_NAME]
|
||||
delete entries[PREVIEW_FILE_NAME]
|
||||
setSwarmEntries(entries)
|
||||
|
||||
const count = Object.keys(entries).length
|
||||
|
||||
let metadata: Metadata | undefined = {
|
||||
hash,
|
||||
size: 0,
|
||||
type: count > 1 ? 'folder' : 'unknown',
|
||||
name: reference,
|
||||
isWebsite: Boolean(indexDocument) && count > 1,
|
||||
count,
|
||||
}
|
||||
|
||||
try {
|
||||
const mtdt = await beeApi.downloadFile(reference, META_FILE_NAME)
|
||||
const remoteMetadata = mtdt.data.text()
|
||||
metadata = { ...metadata, ...(JSON.parse(remoteMetadata) as Metadata) }
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
|
||||
if (previewFile) {
|
||||
setPreview(`${config.BEE_API_HOST}/bzz/${reference}/${PREVIEW_FILE_NAME}`)
|
||||
}
|
||||
|
||||
setMetadata(metadata)
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
@@ -109,8 +130,6 @@ export function Share(): ReactElement {
|
||||
setDownloading(false)
|
||||
}
|
||||
|
||||
const assetName = shortenHash(reference)
|
||||
|
||||
if (!status.all) return <TroubleshootConnectionCard />
|
||||
|
||||
if (loading) {
|
||||
@@ -129,17 +148,17 @@ export function Share(): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Box mb={4}>
|
||||
<AssetPreview files={files} assetName={assetName} />
|
||||
<AssetPreview metadata={metadata} previewUri={preview} />
|
||||
</Box>
|
||||
<Box mb={4}>
|
||||
<AssetSummary files={files} hash={reference} />
|
||||
<AssetSummary isWebsite={metadata?.isWebsite} hash={reference} />
|
||||
</Box>
|
||||
<DownloadActionBar
|
||||
onOpen={onOpen}
|
||||
onCancel={onClose}
|
||||
onDownload={onDownload}
|
||||
onUpdateFeed={onUpdateFeed}
|
||||
hasIndexDocument={Boolean(indexDocument && files.length > 1)}
|
||||
hasIndexDocument={Boolean(metadata?.isWebsite)}
|
||||
loading={downloading}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Context as FileContext } from '../../providers/File'
|
||||
import { Context as SettingsContext } from '../../providers/Settings'
|
||||
import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps'
|
||||
import { ROUTES } from '../../routes'
|
||||
import { detectIndexHtml, getAssetNameFromFiles } from '../../utils/file'
|
||||
import { detectIndexHtml, getAssetNameFromFiles, packageFile } from '../../utils/file'
|
||||
import { persistIdentity, updateFeed } from '../../utils/identity'
|
||||
import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
|
||||
import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog'
|
||||
@@ -21,6 +21,7 @@ import { PostageStampSelector } from '../stamps/PostageStampSelector'
|
||||
import { AssetPreview } from './AssetPreview'
|
||||
import { StampPreview } from './StampPreview'
|
||||
import { UploadActionBar } from './UploadActionBar'
|
||||
import { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants'
|
||||
|
||||
export function Upload(): ReactElement {
|
||||
const [step, setStep] = useState(0)
|
||||
@@ -31,7 +32,7 @@ export function Upload(): ReactElement {
|
||||
|
||||
const { refresh } = useContext(StampsContext)
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
const { files, setFiles, uploadOrigin } = useContext(FileContext)
|
||||
const { files, setFiles, uploadOrigin, metadata, previewUri, previewBlob } = useContext(FileContext)
|
||||
const { identities, setIdentities } = useContext(IdentityContext)
|
||||
const { status } = useContext(BeeContext)
|
||||
|
||||
@@ -66,16 +67,63 @@ export function Upload(): ReactElement {
|
||||
}
|
||||
|
||||
const uploadFiles = (password?: string) => {
|
||||
if (!beeApi || !files.length || !stamp) {
|
||||
if (!beeApi || !files.length || !stamp || !metadata) {
|
||||
return
|
||||
}
|
||||
|
||||
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(files) || undefined
|
||||
let fls = files.map(packageFile) // Apart from packaging, this is needed to not modify the original files array as it can trigger effects
|
||||
let indexDocument: string | undefined = undefined // This means we assume it's folder
|
||||
|
||||
if (files.length === 1) indexDocument = files[0].name
|
||||
else if (files.length > 1) {
|
||||
const idx = detectIndexHtml(files)
|
||||
|
||||
// This is a website
|
||||
if (idx) {
|
||||
// The website is in some directory, remove it
|
||||
if (idx.commonPrefix) {
|
||||
const substrStart = idx.commonPrefix.length
|
||||
indexDocument = idx.indexPath.substr(substrStart)
|
||||
fls = fls.map(f => {
|
||||
const path = (f.path as string).substr(substrStart)
|
||||
|
||||
return { ...f, path, webkitRelativePath: path, fullPath: path }
|
||||
})
|
||||
} else {
|
||||
// The website is not packed in a directory
|
||||
indexDocument = idx.indexPath
|
||||
}
|
||||
}
|
||||
}
|
||||
const lastModified = files[0].lastModified
|
||||
|
||||
// We want to store only some metadata
|
||||
const mtd: SwarmMetadata = {
|
||||
name: metadata.name,
|
||||
size: metadata.size,
|
||||
}
|
||||
|
||||
// Type of the file only makes sense for a single file
|
||||
if (files.length === 1) mtd.type = metadata.type
|
||||
|
||||
const metafile = new File([JSON.stringify(mtd)], META_FILE_NAME, {
|
||||
type: 'application/json',
|
||||
lastModified,
|
||||
})
|
||||
fls.push(packageFile(metafile))
|
||||
|
||||
if (previewBlob) {
|
||||
const previewFile = new File([previewBlob], PREVIEW_FILE_NAME, {
|
||||
type: 'image/jpeg',
|
||||
lastModified,
|
||||
})
|
||||
fls.push(packageFile(previewFile))
|
||||
}
|
||||
|
||||
setUploading(true)
|
||||
|
||||
beeApi
|
||||
.uploadFiles(stamp.batchID, files as unknown as File[], { indexDocument })
|
||||
.uploadFiles(stamp.batchID, fls, { indexDocument })
|
||||
.then(hash => {
|
||||
putHistory(HISTORY_KEYS.UPLOAD_HISTORY, hash.reference, getAssetNameFromFiles(files))
|
||||
|
||||
@@ -121,7 +169,7 @@ export function Upload(): ReactElement {
|
||||
<Box mb={4}>
|
||||
<ProgressIndicator steps={['Preview', 'Add postage stamp', 'Upload to node']} index={step} />
|
||||
</Box>
|
||||
{(step === 0 || step === 2) && <AssetPreview files={files} />}
|
||||
{(step === 0 || step === 2) && <AssetPreview metadata={metadata} previewUri={previewUri} />}
|
||||
{step === 1 && (
|
||||
<>
|
||||
<Box mb={2}>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { SwarmButton } from '../../components/SwarmButton'
|
||||
import { Context, UploadOrigin } from '../../providers/File'
|
||||
import { ROUTES } from '../../routes'
|
||||
import { detectIndexHtml } from '../../utils/file'
|
||||
import { SwarmFile } from '../../utils/SwarmFile'
|
||||
|
||||
interface Props {
|
||||
uploadOrigin: UploadOrigin
|
||||
@@ -99,8 +98,8 @@ export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement {
|
||||
|
||||
const handleChange = (files?: File[]) => {
|
||||
if (files) {
|
||||
const swarmFiles = files.map(x => new SwarmFile(x))
|
||||
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(swarmFiles) || undefined
|
||||
const FilePaths = files as FilePath[]
|
||||
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(FilePaths) || undefined
|
||||
|
||||
if (files.length && strictWebsiteMode && !indexDocument) {
|
||||
enqueueSnackbar('To upload a website, there must be an index.html or index.htm in the root of the folder.', {
|
||||
@@ -111,7 +110,7 @@ export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement {
|
||||
return
|
||||
}
|
||||
|
||||
setFiles(swarmFiles)
|
||||
setFiles(FilePaths)
|
||||
|
||||
if (files.length) {
|
||||
setUploadOrigin(uploadOrigin)
|
||||
|
||||
+41
-6
@@ -1,17 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
||||
import { createContext, ReactChild, ReactElement, useState } from 'react'
|
||||
import { SwarmFile } from '../utils/SwarmFile'
|
||||
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
|
||||
import { getMetadata } from '../utils/file'
|
||||
import { resize } from '../utils/image'
|
||||
import { PREVIEW_DIMENSIONS } from '../constants'
|
||||
|
||||
export type UploadOrigin = { origin: 'UPLOAD' | 'FEED'; uuid?: string }
|
||||
|
||||
export const defaultUploadOrigin: UploadOrigin = { origin: 'UPLOAD' }
|
||||
|
||||
interface ContextInterface {
|
||||
files: SwarmFile[]
|
||||
setFiles: (files: SwarmFile[]) => void
|
||||
files: FilePath[]
|
||||
setFiles: (files: FilePath[]) => void
|
||||
uploadOrigin: UploadOrigin
|
||||
setUploadOrigin: (uploadOrigin: UploadOrigin) => void
|
||||
metadata?: Metadata
|
||||
previewUri?: string
|
||||
previewBlob?: Blob
|
||||
}
|
||||
|
||||
const initialValues: ContextInterface = {
|
||||
@@ -29,8 +34,38 @@ interface Props {
|
||||
}
|
||||
|
||||
export function Provider({ children }: Props): ReactElement {
|
||||
const [files, setFiles] = useState<SwarmFile[]>(initialValues.files)
|
||||
const [files, setFiles] = useState<FilePath[]>(initialValues.files)
|
||||
const [uploadOrigin, setUploadOrigin] = useState<UploadOrigin>(initialValues.uploadOrigin)
|
||||
const [metadata, setMetadata] = useState<Metadata | undefined>(undefined)
|
||||
const [previewUri, setPreviewUri] = useState<string | undefined>(undefined)
|
||||
const [previewBlob, setPreviewBlob] = useState<Blob | undefined>(undefined)
|
||||
|
||||
return <Context.Provider value={{ files, setFiles, uploadOrigin, setUploadOrigin }}>{children}</Context.Provider>
|
||||
useEffect(() => {
|
||||
setMetadata(getMetadata(files))
|
||||
|
||||
if (previewUri) {
|
||||
URL.revokeObjectURL(previewUri) // Clear the preview from memory
|
||||
setPreviewUri(undefined)
|
||||
setPreviewBlob(undefined)
|
||||
}
|
||||
|
||||
if (files.length !== 1 || !files[0].type.startsWith('image')) return
|
||||
|
||||
resize(files[0], PREVIEW_DIMENSIONS.maxWidth, PREVIEW_DIMENSIONS.maxHeight).then(blob => {
|
||||
setPreviewUri(URL.createObjectURL(blob)) // NOTE: Until it is cleared with URL.revokeObjectURL, the file stays allocated in memory
|
||||
setPreviewBlob(blob)
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (previewUri) {
|
||||
URL.revokeObjectURL(previewUri)
|
||||
}
|
||||
}
|
||||
}, [files]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ files, setFiles, uploadOrigin, setUploadOrigin, metadata, previewUri, previewBlob }}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
Vendored
+15
@@ -21,3 +21,18 @@ interface StatusEthereumConnectionHook extends StatusHookCommon {
|
||||
interface StatusTopologyHook extends StatusHookCommon {
|
||||
topology: Topology | null
|
||||
}
|
||||
|
||||
interface SwarmMetadata {
|
||||
size: number
|
||||
name: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
interface Metadata extends SwarmMetadata {
|
||||
type: string
|
||||
isWebsite: boolean
|
||||
count?: number
|
||||
hash?: string
|
||||
}
|
||||
|
||||
type FilePath = File & { path?: string; fullPath?: string }
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export class SwarmFile {
|
||||
public name: string
|
||||
public path: string
|
||||
public type: string
|
||||
public size: number
|
||||
public webkitRelativePath: string
|
||||
public arrayBuffer: () => Promise<ArrayBuffer>
|
||||
private data: Promise<ArrayBuffer>
|
||||
|
||||
constructor(file: File) {
|
||||
const path = Reflect.get(file, 'path') || file.webkitRelativePath || file.name
|
||||
this.path = path.startsWith('/') ? path.slice(1) : path
|
||||
this.webkitRelativePath = this.path
|
||||
this.name = file.name
|
||||
this.type = file.type
|
||||
this.size = file.size
|
||||
this.data = file.arrayBuffer()
|
||||
this.arrayBuffer = async () => {
|
||||
const data = await this.data
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-37
@@ -1,28 +1,32 @@
|
||||
import { FileData } from '@ethersphere/bee-js'
|
||||
import { SwarmFile } from './SwarmFile'
|
||||
|
||||
const indexHtmls = ['index.html', 'index.htm']
|
||||
|
||||
export function detectIndexHtml(files: SwarmFile[]): string | false {
|
||||
if (!files.length) {
|
||||
interface DetectedIndex {
|
||||
indexPath: string
|
||||
commonPrefix?: string
|
||||
}
|
||||
|
||||
export function detectIndexHtml(files: FilePath[]): DetectedIndex | false {
|
||||
const paths = files.map(getPath)
|
||||
|
||||
if (!paths.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const exactMatch = files.find(x => indexHtmls.includes(x.path))
|
||||
const exactMatch = paths.find(x => indexHtmls.includes(x))
|
||||
|
||||
if (exactMatch) {
|
||||
return exactMatch.name
|
||||
return { indexPath: exactMatch }
|
||||
}
|
||||
|
||||
const prefix = files[0].path.split('/')[0] + '/'
|
||||
const prefix = paths[0].split('/')[0] + '/'
|
||||
|
||||
const allStartWithSamePrefix = files.every(x => x.path.startsWith(prefix))
|
||||
const allStartWithSamePrefix = paths.every(x => x.startsWith(prefix))
|
||||
|
||||
if (allStartWithSamePrefix) {
|
||||
const match = files.find(x => indexHtmls.map(y => prefix + y).includes(x.path))
|
||||
const match = paths.find(x => indexHtmls.map(y => prefix + y).includes(x))
|
||||
|
||||
if (match) {
|
||||
return match.name
|
||||
return { indexPath: match, commonPrefix: prefix }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,38 +57,50 @@ export function getHumanReadableFileSize(bytes: number): string {
|
||||
return bytes + ' bytes'
|
||||
}
|
||||
|
||||
export function convertBeeFileToBrowserFile(file: FileData<ArrayBuffer>): Partial<File> {
|
||||
return {
|
||||
name: file.name,
|
||||
size: file.data.byteLength,
|
||||
type: file.contentType,
|
||||
arrayBuffer: () => new Promise(resolve => resolve(file.data)),
|
||||
}
|
||||
}
|
||||
|
||||
export function convertManifestToFiles(files: Record<string, string>): SwarmFile[] {
|
||||
return Object.entries(files).map(
|
||||
x =>
|
||||
({
|
||||
name: x[0],
|
||||
path: x[0],
|
||||
type: 'n/a',
|
||||
size: 0,
|
||||
webkitRelativePath: x[0],
|
||||
arrayBuffer: () => new Promise(resolve => resolve(new ArrayBuffer(0))),
|
||||
} as SwarmFile),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAssetNameFromFiles(files: SwarmFile[]): string {
|
||||
export function getAssetNameFromFiles(files: FilePath[]): string {
|
||||
if (files.length === 1) return files[0].name
|
||||
|
||||
if (files.length > 0) {
|
||||
const prefix = files[0].path.split('/')[0]
|
||||
const prefix = getPath(files[0]).split('/')[0]
|
||||
|
||||
// Only if all files have a common prefix we can use it as a folder name
|
||||
if (files.every(f => f.path.split('/')[0] === prefix)) return prefix
|
||||
if (files.every(f => getPath(f).split('/')[0] === prefix)) return prefix
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
export function getMetadata(files: FilePath[]): Metadata {
|
||||
const size = files.reduce((total, item) => total + item.size, 0)
|
||||
const isWebsite = Boolean(detectIndexHtml(files))
|
||||
const name = getAssetNameFromFiles(files)
|
||||
const type = files.length === 1 ? files[0].type : 'folder'
|
||||
const count = files.length
|
||||
|
||||
return { size, name, type, isWebsite, count }
|
||||
}
|
||||
|
||||
export function getPath(file: FilePath): string {
|
||||
return (file.path || file.webkitRelativePath || file.name).replace(/^\//g, '') // remove the starting slash
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that is needed to have correct directory structure as webkitRelativePath is read only
|
||||
*/
|
||||
export function packageFile(file: FilePath): FilePath {
|
||||
const path = getPath(file)
|
||||
|
||||
return {
|
||||
path: path,
|
||||
fullPath: path,
|
||||
webkitRelativePath: path,
|
||||
lastModified: file.lastModified,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
stream: file.stream,
|
||||
slice: file.slice,
|
||||
text: file.text,
|
||||
arrayBuffer: async () => await file.arrayBuffer(), // This is needed for successful upload and can not simply be { arrayBuffer: file.arrayBuffer }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
interface Dimensions {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dimensions of the image after resize
|
||||
*
|
||||
* @param imgWidth Current image width
|
||||
* @param imgHeight Current image height
|
||||
* @param maxWidth Desired max width
|
||||
* @param maxHeight Desired max height
|
||||
*
|
||||
* @returns Downscaled dimensions of the image to fit in the bounding box
|
||||
*/
|
||||
export function getDimensions(imgWidth: number, imgHeight: number, maxWidth?: number, maxHeight?: number): Dimensions {
|
||||
const ratioWidth = maxWidth ? imgWidth / maxWidth : 1
|
||||
const ratioHeight = maxHeight ? imgHeight / maxHeight : 1
|
||||
|
||||
const ratio = Math.max(ratioWidth, ratioHeight)
|
||||
|
||||
// No need to resize
|
||||
if (ratio <= 1) return { width: imgWidth, height: imgHeight }
|
||||
|
||||
return { width: imgWidth / ratio, height: imgHeight / ratio }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize image passed to fit in the bounding box defined with maxWidth and maxHeight.
|
||||
* Note that one or both of the bounding box dimensions may be omitted
|
||||
*
|
||||
* @param file Image file to be resized
|
||||
* @param maxWidth Maximal image width
|
||||
* @param maxHeight Maximal image height
|
||||
*
|
||||
* @returns Promise that resolves into the resized image blob
|
||||
*/
|
||||
export function resize(file: File, maxWidth?: number, maxHeight?: number): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const allowedTypes = [
|
||||
'image/bmp',
|
||||
'image/gif',
|
||||
'image/vnd.microsoft.icon',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/tiff',
|
||||
'image/webp',
|
||||
]
|
||||
|
||||
if (!file.size || !file.type || !allowedTypes.includes(file.type)) return reject('File not supported!')
|
||||
|
||||
try {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = event => {
|
||||
const src = event?.target?.result
|
||||
|
||||
if (!src || typeof src !== 'string') throw new Error('Failed to load the image source')
|
||||
|
||||
const img = new Image()
|
||||
img.src = src
|
||||
img.onload = () => {
|
||||
const dimensions = getDimensions(img.width, img.height, maxWidth, maxHeight)
|
||||
const elem = document.createElement('canvas')
|
||||
elem.width = dimensions.width
|
||||
elem.height = dimensions.height
|
||||
const ctx = elem.getContext('2d')
|
||||
|
||||
if (!ctx) throw new Error('Failed to create canvas context')
|
||||
|
||||
ctx.drawImage(img, 0, 0, elem.width, elem.height)
|
||||
ctx.canvas.toBlob(
|
||||
blob => {
|
||||
if (!blob) throw new Error('Failed to extract the blob from canvas')
|
||||
|
||||
resolve(blob)
|
||||
},
|
||||
'image/jpeg',
|
||||
1,
|
||||
)
|
||||
}
|
||||
}
|
||||
reader.onerror = error => reject(error)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user