Files
common/ipfs/client.go

241 lines
7.9 KiB
Go

// Package ipfs provides a high-level interface for interacting with an IPFS node.
package ipfs
import (
"bytes"
"context"
"fmt"
"io"
"github.com/ipfs/boxo/files"
"github.com/ipfs/boxo/path"
"github.com/ipfs/kubo/client/rpc"
iface "github.com/ipfs/kubo/core/coreiface"
"github.com/ipfs/kubo/core/coreiface/options"
)
// NodeStatus contains information about an IPFS node's status and connectivity.
type NodeStatus struct {
// PeerID is the unique identifier of the IPFS node
PeerID string
// Version is the version of the IPFS implementation
Version string
// PeerType describes the type of IPFS node (e.g., "kubo")
PeerType string
// ConnectedPeers is the number of peers currently connected to this node
ConnectedPeers int
}
// IPFSClient provides a high-level interface for interacting with an IPFS node.
// It abstracts the complexity of the underlying Kubo RPC API and provides
// convenient methods for common IPFS operations.
type IPFSClient interface {
// Add stores raw byte data in IPFS and returns the resulting CID.
// The data is stored as a single file node in the IPFS DAG.
Add(data []byte) (string, error)
// AddFile stores a File with metadata in IPFS and returns the resulting CID.
// The file's name and content are preserved in the IPFS node.
AddFile(file File) (string, error)
// AddFolder stores a directory structure in IPFS and returns the root CID.
// The folder and its contents are stored as a directory node in IPFS.
AddFolder(folder Folder) (string, error)
// Exists checks if content with the given CID exists in the IPFS network.
// It queries the local node's blockstore to verify availability.
Exists(cid string) (bool, error)
// Get retrieves the content of a file from IPFS using its CID.
// Returns the raw bytes of the file content.
Get(cid string) ([]byte, error)
// IsPinned checks if content is pinned on the local IPFS node using IPNS resolution.
// Returns true if the IPNS name can be successfully resolved.
IsPinned(ipns string) (bool, error)
// Ls lists the contents of a directory in IPFS using its CID.
// Returns the names of all entries in the directory.
Ls(cid string) ([]string, error)
// Pin ensures that content with the given CID is retained in the local IPFS node.
// The content will not be garbage collected and can be assigned a human-readable name.
Pin(cid string, name string) error
// Unpin removes the pin for content with the given CID.
// The content may be garbage collected if not pinned elsewhere.
Unpin(cid string) error
// NodeStatus returns information about the IPFS node including peer ID, version, and connectivity.
// Returns node identity, version information, and connected peer count.
NodeStatus() (*NodeStatus, error)
}
// ipfsClient implements the Client interface using the Kubo RPC API.
// It maintains a connection to a local IPFS node via HTTP API.
type ipfsClient struct {
api *rpc.HttpApi
}
// GetClient creates a new IPFS client connected to a local IPFS node.
// It establishes a connection to the local Kubo RPC API endpoint.
// Returns an error if the local IPFS node is not available or accessible.
func GetClient() (IPFSClient, error) {
api, err := rpc.NewLocalApi()
if err != nil {
return nil, err
}
return &ipfsClient{api: api}, nil
}
// Add stores raw byte data in IPFS and returns the resulting CID string.
// The data is wrapped in a BytesFile and added to the UnixFS layer.
func (c *ipfsClient) Add(data []byte) (string, error) {
file := files.NewBytesFile(data)
cidFile, err := c.api.Unixfs().Add(context.Background(), file)
if err != nil {
return "", err
}
return cidFile.String(), nil
}
// Get retrieves content from IPFS using the provided CID string.
// It resolves the CID path, fetches the content, and returns the raw bytes.
// Returns an error if the CID is invalid or the content cannot be retrieved.
func (c *ipfsClient) Get(cid string) ([]byte, error) {
p, err := path.NewPath(cid)
if err != nil {
return nil, err
}
node, err := c.api.Unixfs().Get(context.Background(), p)
if err != nil {
return nil, err
}
file, ok := node.(files.File)
if !ok {
return nil, fmt.Errorf("unexpected node type: %T", node)
}
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, file); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// IsPinned checks if an IPNS name can be resolved, indicating the content is available.
// This method uses IPNS resolution rather than direct pin checking.
// Returns false if the IPNS name cannot be resolved.
func (c *ipfsClient) IsPinned(ipns string) (bool, error) {
_, err := c.api.Name().Resolve(context.Background(), ipns)
if err != nil {
return false, nil
}
return true, nil
}
// Exists verifies if content with the given CID exists in the IPFS network.
// It checks the block statistics to determine availability.
// Returns false if the CID is invalid or the content is not found.
func (c *ipfsClient) Exists(cid string) (bool, error) {
p, err := path.NewPath(cid)
if err != nil {
return false, err
}
_, err = c.api.Block().Stat(context.Background(), p)
if err != nil {
return false, nil
}
return true, nil
}
// Pin adds content to the pin set, preventing it from being garbage collected.
// The content is identified by its CID and can be assigned a descriptive name.
// Returns an error if the CID is invalid or pinning fails.
func (c *ipfsClient) Pin(cid string, name string) error {
p, err := path.NewPath(cid)
if err != nil {
return err
}
return c.api.Pin().Add(context.Background(), p, options.Pin.Name(name))
}
// Unpin removes content from the pin set, allowing it to be garbage collected.
// The content is identified by its CID string.
// Returns an error if the CID is invalid or unpinning fails.
func (c *ipfsClient) Unpin(cid string) error {
p, err := path.NewPath(cid)
if err != nil {
return err
}
return c.api.Pin().Rm(context.Background(), p)
}
// Ls lists the contents of an IPFS directory using its CID.
// It returns the names of all entries in the directory.
// The listing is performed asynchronously using channels to handle large directories efficiently.
func (c *ipfsClient) Ls(cid string) ([]string, error) {
p, err := path.NewPath(cid)
if err != nil {
return nil, err
}
dirChan := make(chan iface.DirEntry)
files := make([]string, 0)
lsErr := make(chan error, 1)
go func() {
lsErr <- c.api.Unixfs().Ls(context.Background(), p, dirChan)
}()
for dirEnt := range dirChan {
files = append(files, dirEnt.Name)
}
if err := <-lsErr; err != nil {
return nil, err
}
return files, nil
}
// AddFile stores a File with its metadata in IPFS and returns the resulting CID.
// This method preserves the file's name and content structure when adding to IPFS.
// It's implemented as a method on the client to maintain consistency with the Client interface.
func (c *ipfsClient) AddFile(file File) (string, error) {
cidFile, err := c.api.Unixfs().Add(context.Background(), file)
if err != nil {
return "", err
}
return cidFile.String(), nil
}
// NodeStatus retrieves status information about the IPFS node including peer ID, version, and connectivity.
// It queries the node's identity, version, and connected peers to provide comprehensive status information.
func (c *ipfsClient) NodeStatus() (*NodeStatus, error) {
ctx := context.Background()
// Get node ID information using Key().Self() to get the node's own peer ID
nodeKey, err := c.api.Key().Self(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get node ID: %w", err)
}
// Get connected peers count
swarmPeers, err := c.api.Swarm().Peers(ctx)
connectedPeers := 0
if err == nil {
connectedPeers = len(swarmPeers)
}
// Since we successfully got the key information, we know the node is responsive
versionStr := "kubo-responsive"
if connectedPeers > 0 {
versionStr = "kubo-connected"
}
return &NodeStatus{
PeerID: nodeKey.Name(),
Version: versionStr,
PeerType: "kubo",
ConnectedPeers: connectedPeers,
}, nil
}