259 lines
7.0 KiB
Go
259 lines
7.0 KiB
Go
![]() |
package ipfs
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"log"
|
||
|
"mime/multipart"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// PinataPinFileURL is the Pinata API endpoint for pinning files
|
||
|
PinataPinFileURL = "https://api.pinata.cloud/pinning/pinFileToIPFS"
|
||
|
|
||
|
// PinataPinJSONURL is the Pinata API endpoint for pinning JSON
|
||
|
PinataPinJSONURL = "https://api.pinata.cloud/pinning/pinJSONToIPFS"
|
||
|
|
||
|
// PinataTimeout is the timeout for Pinata API calls
|
||
|
PinataTimeout = 30 * time.Second
|
||
|
)
|
||
|
|
||
|
// IPFSService handles interactions with IPFS via Pinata
|
||
|
type IPFSService struct {
|
||
|
apiKey string
|
||
|
apiSecret string
|
||
|
jwt string
|
||
|
client *http.Client
|
||
|
}
|
||
|
|
||
|
// PinataResponse represents a response from the Pinata API
|
||
|
type PinataResponse struct {
|
||
|
IpfsHash string `json:"IpfsHash"`
|
||
|
PinSize int `json:"PinSize"`
|
||
|
Timestamp string `json:"Timestamp"`
|
||
|
}
|
||
|
|
||
|
// PinataMetadata represents metadata for a file pinned to IPFS
|
||
|
type PinataMetadata struct {
|
||
|
Name string `json:"name"`
|
||
|
KeyValues map[string]string `json:"keyvalues"`
|
||
|
}
|
||
|
|
||
|
// PinataJSONRequest represents a request to pin JSON to IPFS
|
||
|
type PinataJSONRequest struct {
|
||
|
PinataMetadata PinataMetadata `json:"pinataMetadata"`
|
||
|
PinataContent interface{} `json:"pinataContent"`
|
||
|
}
|
||
|
|
||
|
// NewIPFSService creates a new IPFS service
|
||
|
func NewIPFSService() (*IPFSService, error) {
|
||
|
apiKey := os.Getenv("PINATA_API_KEY")
|
||
|
apiSecret := os.Getenv("PINATA_API_SECRET")
|
||
|
jwt := os.Getenv("PINATA_JWT")
|
||
|
|
||
|
if (apiKey == "" || apiSecret == "") && jwt == "" {
|
||
|
return nil, errors.New("either PINATA_API_KEY and PINATA_API_SECRET or PINATA_JWT must be set")
|
||
|
}
|
||
|
|
||
|
client := &http.Client{
|
||
|
Timeout: PinataTimeout,
|
||
|
}
|
||
|
|
||
|
return &IPFSService{
|
||
|
apiKey: apiKey,
|
||
|
apiSecret: apiSecret,
|
||
|
jwt: jwt,
|
||
|
client: client,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// PinFile uploads a file to IPFS via Pinata
|
||
|
func (s *IPFSService) PinFile(filePath string, metadata map[string]string) (string, error) {
|
||
|
file, err := os.Open(filePath)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to open file: %v", err)
|
||
|
}
|
||
|
defer file.Close()
|
||
|
|
||
|
return s.PinFileContent(file, filepath.Base(filePath), metadata)
|
||
|
}
|
||
|
|
||
|
// PinFileContent uploads file content to IPFS via Pinata
|
||
|
func (s *IPFSService) PinFileContent(fileContent io.Reader, fileName string, metadata map[string]string) (string, error) {
|
||
|
body := &bytes.Buffer{}
|
||
|
writer := multipart.NewWriter(body)
|
||
|
|
||
|
// Add the file
|
||
|
part, err := writer.CreateFormFile("file", fileName)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to create form file: %v", err)
|
||
|
}
|
||
|
|
||
|
_, err = io.Copy(part, fileContent)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to copy file content: %v", err)
|
||
|
}
|
||
|
|
||
|
// Add metadata if provided
|
||
|
if metadata != nil && len(metadata) > 0 {
|
||
|
metadataJSON, err := json.Marshal(PinataMetadata{
|
||
|
Name: fileName,
|
||
|
KeyValues: metadata,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to marshal metadata: %v", err)
|
||
|
}
|
||
|
|
||
|
err = writer.WriteField("pinataMetadata", string(metadataJSON))
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to write metadata field: %v", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
err = writer.Close()
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to close multipart writer: %v", err)
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequest("POST", PinataPinFileURL, body)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to create request: %v", err)
|
||
|
}
|
||
|
|
||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||
|
|
||
|
// Set authentication headers
|
||
|
if s.jwt != "" {
|
||
|
req.Header.Set("Authorization", "Bearer "+s.jwt)
|
||
|
} else {
|
||
|
req.Header.Set("pinata_api_key", s.apiKey)
|
||
|
req.Header.Set("pinata_secret_api_key", s.apiSecret)
|
||
|
}
|
||
|
|
||
|
resp, err := s.client.Do(req)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to send request: %v", err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
body, _ := io.ReadAll(resp.Body)
|
||
|
return "", fmt.Errorf("failed to pin file: %s, status: %d", string(body), resp.StatusCode)
|
||
|
}
|
||
|
|
||
|
var pinataResp PinataResponse
|
||
|
err = json.NewDecoder(resp.Body).Decode(&pinataResp)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to decode response: %v", err)
|
||
|
}
|
||
|
|
||
|
return pinataResp.IpfsHash, nil
|
||
|
}
|
||
|
|
||
|
// PinJSON uploads JSON data to IPFS via Pinata
|
||
|
func (s *IPFSService) PinJSON(data interface{}, name string, metadata map[string]string) (string, error) {
|
||
|
pinataMetadata := PinataMetadata{
|
||
|
Name: name,
|
||
|
KeyValues: metadata,
|
||
|
}
|
||
|
|
||
|
pinataRequest := PinataJSONRequest{
|
||
|
PinataMetadata: pinataMetadata,
|
||
|
PinataContent: data,
|
||
|
}
|
||
|
|
||
|
jsonData, err := json.Marshal(pinataRequest)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to marshal JSON: %v", err)
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequest("POST", PinataPinJSONURL, bytes.NewBuffer(jsonData))
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to create request: %v", err)
|
||
|
}
|
||
|
|
||
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
||
|
// Set authentication headers
|
||
|
if s.jwt != "" {
|
||
|
req.Header.Set("Authorization", "Bearer "+s.jwt)
|
||
|
} else {
|
||
|
req.Header.Set("pinata_api_key", s.apiKey)
|
||
|
req.Header.Set("pinata_secret_api_key", s.apiSecret)
|
||
|
}
|
||
|
|
||
|
resp, err := s.client.Do(req)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to send request: %v", err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
body, _ := io.ReadAll(resp.Body)
|
||
|
return "", fmt.Errorf("failed to pin JSON: %s, status: %d", string(body), resp.StatusCode)
|
||
|
}
|
||
|
|
||
|
var pinataResp PinataResponse
|
||
|
err = json.NewDecoder(resp.Body).Decode(&pinataResp)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("failed to decode response: %v", err)
|
||
|
}
|
||
|
|
||
|
return pinataResp.IpfsHash, nil
|
||
|
}
|
||
|
|
||
|
// GetIPFSGatewayURL returns a public gateway URL for an IPFS hash
|
||
|
func (s *IPFSService) GetIPFSGatewayURL(ipfsHash string) string {
|
||
|
// Use Pinata gateway or other public gateways
|
||
|
return fmt.Sprintf("https://gateway.pinata.cloud/ipfs/%s", ipfsHash)
|
||
|
}
|
||
|
|
||
|
// MockIPFSService is a mock implementation for testing and development
|
||
|
type MockIPFSService struct{}
|
||
|
|
||
|
// NewMockIPFSService creates a new mock IPFS service
|
||
|
func NewMockIPFSService() *MockIPFSService {
|
||
|
return &MockIPFSService{}
|
||
|
}
|
||
|
|
||
|
// PinFile mocks uploading a file to IPFS
|
||
|
func (s *MockIPFSService) PinFile(filePath string, metadata map[string]string) (string, error) {
|
||
|
log.Printf("Mock pinning file: %s with metadata: %v", filePath, metadata)
|
||
|
|
||
|
// Generate a deterministic hash based on the file path for testing
|
||
|
hashBase := strings.ReplaceAll(filepath.Base(filePath), ".", "")
|
||
|
if len(hashBase) > 10 {
|
||
|
hashBase = hashBase[:10]
|
||
|
}
|
||
|
|
||
|
ipfsHash := fmt.Sprintf("Qm%s123456789abcdef", hashBase)
|
||
|
return ipfsHash, nil
|
||
|
}
|
||
|
|
||
|
// PinJSON mocks uploading JSON data to IPFS
|
||
|
func (s *MockIPFSService) PinJSON(data interface{}, name string, metadata map[string]string) (string, error) {
|
||
|
log.Printf("Mock pinning JSON with name: %s and metadata: %v", name, metadata)
|
||
|
|
||
|
// Generate a deterministic hash based on the name for testing
|
||
|
hashBase := name
|
||
|
if len(hashBase) > 10 {
|
||
|
hashBase = hashBase[:10]
|
||
|
}
|
||
|
|
||
|
ipfsHash := fmt.Sprintf("Qm%s123456789abcdef", hashBase)
|
||
|
return ipfsHash, nil
|
||
|
}
|
||
|
|
||
|
// GetIPFSGatewayURL mocks a public gateway URL for an IPFS hash
|
||
|
func (s *MockIPFSService) GetIPFSGatewayURL(ipfsHash string) string {
|
||
|
return fmt.Sprintf("https://mock-gateway.pinata.cloud/ipfs/%s", ipfsHash)
|
||
|
}
|