Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve ipfs media download when file its in folder #7752

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/shared/components/MediaDisplay.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
</script>

<div class="h-full w-full object-cover">
{#if htmlTag === ParentMimeType.Image}
{#if htmlTag === ParentMimeType.Image || htmlTag === ParentMimeType.Text}
<img {src} {alt} loading="lazy" class="w-full h-full object-cover" />
{:else if htmlTag === ParentMimeType.Video}
<video
Expand Down
7 changes: 3 additions & 4 deletions packages/shared/components/NftImageOrIconBox.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script lang="ts">
import { INft } from '@core/nfts'
import { NftSize } from 'shared/components/enums'
import { INft, ParentMimeType } from '@core/nfts'
import { MediaPlaceholder, NftMedia } from 'shared/components'
import { ParentMimeType } from '@core/nfts'
import { NftSize } from 'shared/components/enums'

export let nft: INft | null = null
export let size: NftSize = NftSize.Medium
Expand All @@ -18,7 +17,7 @@
class:medium={size === NftSize.Medium}
class:large={size === NftSize.Large}
>
{#if parentType === ParentMimeType.Image && nft}
{#if (parentType === ParentMimeType.Image && nft) || (parentType === ParentMimeType.Text && nft)}
<NftMedia {nft} {useCaching}>
<placeholder-wrapper
slot="placeholder"
Expand Down
12 changes: 11 additions & 1 deletion packages/shared/lib/core/nfts/actions/downloadNextNftInQueue.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Platform } from '@core/app'
import { get } from 'svelte/store'
import { downloadingNftId, nftDownloadQueue, removeNftFromDownloadQueue } from '../stores'
import { getIpfsUri, getIPFSHash } from '../utils'

export async function downloadNextNftInQueue(): Promise<void> {
const nextDownload = get(nftDownloadQueue)?.[0]
Expand All @@ -9,8 +10,17 @@ export async function downloadNextNftInQueue(): Promise<void> {
}

try {
const { downloadUrl, path, nft, accountIndex } = nextDownload
// eslint-disable-next-line prefer-const
let { downloadUrl, path, nft, accountIndex } = nextDownload
downloadingNftId.set(nft.id)
const ipfsHash = getIPFSHash(downloadUrl)
if (ipfsHash) {
const ipfsUri = await getIpfsUri({ hash: ipfsHash })
if (ipfsUri) {
downloadUrl = ipfsUri
}
}

await Platform.downloadNft(downloadUrl, path, nft.id, accountIndex)
} catch (error) {
downloadingNftId.set(undefined)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { allAccountNfts } from '../stores'
import { INft } from '../interfaces'
import { allAccountNfts } from '../stores'
import { getIpfsUri, getIPFSHash } from '../utils'

export function updateNftInAllAccountNfts(accountIndex: number, nftId: string, partialNft: Partial<INft>): void {
allAccountNfts.update((state) => {
Expand All @@ -8,6 +9,16 @@ export function updateNftInAllAccountNfts(accountIndex: number, nftId: string, p
}
const nft = state[accountIndex].find((_nft) => _nft.id === nftId)
if (nft) {
const downloadUrl = nft.downloadUrl
const ipfsHash = getIPFSHash(downloadUrl)
if (ipfsHash) {
void getIpfsUri({ hash: ipfsHash }).then((ipfsUri) => {
if (ipfsUri) {
nft.downloadUrl = ipfsUri
nft.composedUrl = ipfsUri
}
})
}
Object.assign(nft, { ...nft, ...partialNft })
}
return state
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/lib/core/nfts/utils/getIpfsHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function getIPFSHash(url?: string): string | undefined {
const ipfsPrefix = 'https://ipfs.io'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other gateways apart from ipfs.io, this isn't very robust imo


if (url?.includes(ipfsPrefix)) {
return url.slice(ipfsPrefix.length)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method now returns url with first 4 characters sliced ex. s://ipfs.io/ipfs....... instead of ipfs hash.

Copy link
Member

@rajivshah3 rajivshah3 Mar 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to use the URL module? Then we can also address @marc2332 's comment. If we have this URL:

https://cloudflare-ipfs.com/ipfs/bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/wiki/Vincent_van_Gogh.html

and we want to get the bafy... part I think we could do this:

const ipfsUrl = new URL("https://cloudflare-ipfs.com/ipfs/bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/wiki/Vincent_van_Gogh.html")
const path = ipfsUrl.pathname
return path.replace("/ipfs/", "").split("/")[0]

}
}
152 changes: 152 additions & 0 deletions packages/shared/lib/core/nfts/utils/getIpfsUri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
interface IIpfsLink {
Hash: string
Name: string
Size: number
Target: string
Type: number
Mode?: string
Mtime?: number
MtimeNsecs?: number
}

interface IIPfsEntry {
readonly type: 'dir' | 'file'
readonly cid: string
readonly name: string
readonly path: string
mode?: number
mtime?: {
secs: number
nsecs?: number
}
size: number
}

interface IIpfsObject {
Hash: string
Links: IIpfsLink[]
}

enum typeOfLink {
Dir = 'dir',
File = 'file',
}

const IPFS_ENDPOINT = 'https://ipfs.io'
const IPFS_PATH = '/api/v0/ls'
const IPFS_PREFIX = '/ipfs/'

export async function getIpfsUri(link: { path?: string; hash: string }): Promise<string | undefined> {
let ipfsLink = `${link.hash}${link.path ?? ''}`
try {
const ipfsEntry = await ls(ipfsLink)

if (ipfsEntry) {
if (ipfsEntry.type === 'dir') {
const path = `${link.path ?? ''}/${ipfsEntry.name}`
return await getIpfsUri({ hash: link.hash, path })
}
ipfsLink = `${ipfsLink}/${encodeURIComponent(ipfsEntry.name)}`
}
} catch (error) {
console.error('error', error)
}

return `${IPFS_ENDPOINT}${ipfsLink}`
}

async function ls(path: string): Promise<IIPfsEntry | undefined> {
let ipfsEntry: IIPfsEntry | undefined

try {
const baseUrl = IPFS_ENDPOINT
const method = 'get'
const payload = undefined
let headers = {}
const timeout = undefined

headers ??= {}

let controller: AbortController | undefined
let timerId: NodeJS.Timeout | undefined

if (timeout !== undefined) {
controller = new AbortController()
timerId = setTimeout(() => {
if (controller) {
controller.abort()
}
}, timeout)
}

try {
if (path.includes('ipfs')) {
const response = await fetch(`${baseUrl}${IPFS_PATH}?arg=/${path}`, {
method,
headers,
body: payload ? JSON.stringify(payload) : undefined,
signal: controller ? controller.signal : undefined,
})
const lsResponse = (await response.json()) as { Objects: IIpfsObject[] }
const result = lsResponse.Objects[0]
if (result) {
const links = result.Links
if (links.length > 0) {
ipfsEntry = mapLinkToIpfsEntry(links[0], path)
}
}
}
} catch (error) {
console.error('error', error)
} finally {
if (timerId) {
clearTimeout(timerId)
}
}
} catch (error) {
console.error('error', error)
}

return ipfsEntry
}

function mapLinkToIpfsEntry(link: IIpfsLink, path: string): IIPfsEntry {
const hash = link.Hash.startsWith(IPFS_PREFIX) ? link.Hash.slice(IPFS_PREFIX.length) : link.Hash
const entry: IIPfsEntry = {
name: link.Name,
path: path + (link.Name ? `/${link.Name}` : ''),
size: link.Size,
cid: hash,
type: typeOf(link),
}
if (link.Mode) {
entry.mode = Number.parseInt(link.Mode, 8)
}

if (link.Mtime !== undefined && link.Mtime !== null) {
entry.mtime = {
secs: link.Mtime,
}

if (link.MtimeNsecs !== undefined && link.MtimeNsecs !== null) {
entry.mtime.nsecs = link.MtimeNsecs
}
}

return entry
}

function typeOf(link: IIpfsLink): typeOfLink {
switch (link.Type) {
case 1:
case 5: {
return typeOfLink.Dir
}
case 2: {
return typeOfLink.File
}
default: {
return typeOfLink.File
}
}
}
6 changes: 4 additions & 2 deletions packages/shared/lib/core/nfts/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ export * from './buildNftFromNftOutput'
export * from './checkIfNftShouldBeDownloaded'
export * from './composeUrlFromNftUri'
export * from './convertAndFormatNftMetadata'
export * from './getSpendableStatusFromUnspentNftOutput'
export * from './fetchWithTimeout'
export * from './getIpfsHash'
export * from './getIpfsUri'
export * from './getParentMimeType'
export * from './getSpendableStatusFromUnspentNftOutput'
export * from './isNftOwnedByAnyAccount'
export * from './parseNftMetadata'
export * from './rewriteIpfsUri'
export * from './getParentMimeType'
Loading