Skip to content
Merged
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
150 changes: 150 additions & 0 deletions web/api/webrpc/content.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package webrpc

import (
"context"

"github.com/ipfs/go-cid"
"github.com/multiformats/go-multicodec"
"github.com/multiformats/go-multihash"
"golang.org/x/xerrors"

"github.com/filecoin-project/go-state-types/abi"

"github.com/filecoin-project/curio/lib/commcidv2"
)

// ContentInfo represents information about content location
type ContentInfo struct {
PieceCID string `json:"piece_cid"`
Offset uint64 `json:"offset"`
Size uint64 `json:"size"`

Err string `json:"err"`
}

// FindContentByCID finds content by CID
func (a *WebRPC) FindContentByCID(ctx context.Context, cs string) ([]ContentInfo, error) {
cid, err := cid.Parse(cs)
if err != nil {
return nil, err
}

if commcidv2.IsPieceCidV2(cid) || IsCidV1PieceCid(cid) {
_, pcid2, err := a.maybeUpgradePieceCid(ctx, cid)
if err != nil {
return nil, xerrors.Errorf("failed to upgrade piece cid: %w", err)
}
return []ContentInfo{
{
PieceCID: pcid2.String(),
Offset: 0,
Size: 0,
},
}, nil
}

mh := cid.Hash()

offsets, err := a.deps.IndexStore.PiecesContainingMultihash(ctx, mh)
if err != nil {
return nil, xerrors.Errorf("pieces containing multihash %s: %w", mh, err)
}

var res []ContentInfo
for _, offset := range offsets {
off, err := a.deps.IndexStore.GetOffset(ctx, offset.PieceCidV2, mh)
if err != nil {
_, pcid2, err := a.maybeUpgradePieceCid(ctx, offset.PieceCidV2)
if err != nil {
return nil, xerrors.Errorf("failed to upgrade piece cid: %w", err)
}
res = append(res, ContentInfo{
PieceCID: pcid2.String(),
Offset: off,
Size: uint64(offset.BlockSize),
Err: err.Error(),
})
continue
}

_, pcid2, err := a.maybeUpgradePieceCid(ctx, offset.PieceCidV2)
if err != nil {
return nil, xerrors.Errorf("failed to upgrade piece cid: %w", err)
}
res = append(res, ContentInfo{
PieceCID: pcid2.String(),
Offset: off,
Size: uint64(offset.BlockSize),
})
}

return res, nil
}

func (a *WebRPC) maybeUpgradePieceCid(ctx context.Context, c cid.Cid) (bool, cid.Cid, error) {
if commcidv2.IsPieceCidV2(c) {
return true, c, nil
}

if !IsCidV1PieceCid(c) {
return false, c, nil
}

// Lookup piece_cid in market_piece_deal (always v1), get raw_size and piece_length

// raw_size = if mpd.raw_size == 0, then Padded(piece_length).Unpadded() else mpd.raw_size
var rawSize uint64
var pieceLength uint64

err := a.deps.DB.QueryRow(ctx, `
SELECT COALESCE(raw_size, 0), piece_length
FROM market_piece_deal
WHERE piece_cid = $1
`, c.String()).Scan(&rawSize, &pieceLength)
if err != nil {
return false, c, xerrors.Errorf("failed to lookup piece info: %w", err)
}

if rawSize == 0 {
rawSize = uint64(abi.PaddedPieceSize(pieceLength).Unpadded())
}

pcid2, err := commcidv2.PieceCidV2FromV1(c, rawSize)
if err != nil {
return false, c, err
}

return true, pcid2, nil
}

func IsCidV1PieceCid(c cid.Cid) bool {
decoded, err := multihash.Decode(c.Hash())
if err != nil {
return false
}

filCodec := multicodec.Code(c.Type())
filMh := multicodec.Code(decoded.Code)

// Check if it's a valid Filecoin commitment type
switch filCodec {
case multicodec.FilCommitmentUnsealed:
if filMh != multicodec.Sha2_256Trunc254Padded {
return false
}
/* case multicodec.FilCommitmentSealed:
if filMh != multicodec.PoseidonBls12_381A2Fc1 {
return false
} */
default:
// Neither unsealed nor sealed commitment
return false
}

// Commitments must be exactly 32 bytes
if len(decoded.Digest) != 32 {
return false
}

return true
}
125 changes: 125 additions & 0 deletions web/static/pages/content/content.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js';
import RPCCall from '/lib/jsonrpc.mjs';

class ContentPage extends LitElement {
static properties = {
searchCid: { type: String },
results: { type: Array },
loading: { type: Boolean },
error: { type: String }
};

constructor() {
super();
this.searchCid = '';
this.results = [];
this.loading = false;
this.error = '';
}

handleInput(e) {
this.searchCid = e.target.value;
}

async handleFind() {
if (!this.searchCid.trim()) {
this.error = 'Please enter a CID';
return;
}

this.loading = true;
this.error = '';
this.results = [];

try {
const results = await RPCCall('FindContentByCID', [this.searchCid.trim()]);
this.results = results || [];
if (this.results.length === 0) {
this.error = 'No content found for this CID';
}
} catch (err) {
console.error('Error finding content:', err);
this.error = `Error: ${err.message || err}`;
} finally {
this.loading = false;
}
}

render() {
return html`
<link
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="/ux/main.css" onload="document.body.style.visibility = 'initial'">

<div class="container">
<h2>Find CID</h2>
<div class="search-container">
<input
autofocus
type="text"
placeholder="Enter CID (baf...)"
.value="${this.searchCid}"
@input="${this.handleInput}"
@keypress="${(e) => e.key === 'Enter' && this.handleFind()}"
/>
<button
class="btn btn-primary"
@click="${this.handleFind}"
?disabled="${this.loading}"
>
${this.loading ? 'Searching...' : 'Find'}
</button>
</div>

${this.error ? html`
<div class="alert alert-danger">${this.error}</div>
` : ''}

${this.results.length > 0 ? html`
<h3>Results</h3>
<table class="table table-dark table-striped">
<thead>
<tr>
<th>Piece CID</th>
<th>Offset</th>
<th>Size</th>
</tr>
</thead>
<tbody>
${this.results.map(item => html`
<tr>
<td><a href="/pages/piece/?id=${item.piece_cid}">${item.piece_cid}</a></td>
<td>${item.err ? html`<span class="text-danger">${item.err}</span>` : item.offset}</td>
<td>${this.formatBytes(item.size)}</td>
</tr>
`)}
</tbody>
</table>
` : ''}
</div>
`;
}

formatBytes(bytes) {
if (!bytes) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

static styles = css`
.search-container {
display: grid;
grid-template-columns: 1fr auto;
grid-column-gap: 0.75rem;
margin-bottom: 1rem;
}
`;
}

customElements.define('content-page', ContentPage);
26 changes: 26 additions & 0 deletions web/static/pages/content/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Content</title>
<script type="module" src="/ux/curio-ux.mjs"></script>
<script type="module" src="/ux/components/Drawer.mjs"></script>
<script type="module" src="content.mjs"></script>
</head>
<body style="visibility:hidden; background:rgb(11, 22, 34)" data-bs-theme="dark">
<curio-ux>
<div class="page" style="margin-left: 20px; margin-right: 10px">
<section class="section">
<div class="row">
<h1>Content</h1>
</div>
<div class="row">
<div class="col-md-auto" style="max-width: 95%">
<content-page></content-page>
</div>
</div>
</section>
</div>
</curio-ux>
</body>
</html>

8 changes: 8 additions & 0 deletions web/static/ux/curio-ux.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,14 @@ class CurioUX extends LitElement {
<span>Wallets</span>
</a>
</li>
<li>
<a href="/pages/content/" class="nav-link text-white ${active=='/pages/content/'? 'active':''}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi me-2 bi-box" viewBox="0 0 16 16">
<path d="M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464z"/>
</svg>
<span>Content</span>
</a>
</li>
<li>
<a href="https://docs.curiostorage.org/" target="_blank" class="nav-link text-white">
<svg class="bi me-2 bi-book-half" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
Expand Down