synor/sdk/go/storage/storage.go
Gulshan Yadav 59a7123535 feat(sdk): implement Phase 1 SDKs for Wallet, RPC, and Storage
Implements comprehensive SDK support for three core services across
four programming languages (JavaScript/TypeScript, Python, Go, Rust).

## New SDKs

### Wallet SDK
- Key management (create, import, export)
- Transaction signing
- Message signing and verification
- Balance and UTXO queries
- Stealth address support

### RPC SDK
- Block and transaction queries
- Chain state information
- Fee estimation
- Mempool information
- WebSocket subscriptions for real-time updates

### Storage SDK
- Content upload and download
- Pinning operations
- CAR file support
- Directory management
- Gateway URL generation

## Shared Infrastructure

- JSON Schema definitions for all 11 services
- Common type definitions (Address, Amount, UTXO, etc.)
- Unified error handling patterns
- Builder patterns for configuration

## Package Updates

- JavaScript: Updated to @synor/sdk with module exports
- Python: Updated to synor-sdk with websockets dependency
- Go: Added gorilla/websocket dependency
- Rust: Added base64, urlencoding, multipart support

## Fixes

- Fixed Tensor Default trait implementation
- Fixed ProcessorType enum casing
2026-01-27 00:46:24 +05:30

673 lines
16 KiB
Go

// Package storage provides a Go SDK for Synor Storage operations.
//
// Decentralized storage, pinning, and content retrieval on the Synor network.
//
// Example:
//
// client := storage.NewClient("your-api-key")
// result, err := client.Upload(ctx, []byte("Hello, World!"), nil)
// fmt.Println("CID:", result.CID)
//
// // Get gateway URL
// gateway := client.GetGatewayURL(result.CID, "")
// fmt.Println("URL:", gateway.URL)
package storage
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"time"
)
// Version of the SDK.
const Version = "0.1.0"
// Default endpoints.
const (
DefaultEndpoint = "https://storage.synor.cc/api/v1"
DefaultGateway = "https://gateway.synor.cc"
)
// PinStatus represents the status of a pin.
type PinStatus string
const (
Queued PinStatus = "queued"
Pinning PinStatus = "pinning"
Pinned PinStatus = "pinned"
Failed PinStatus = "failed"
Unpinned PinStatus = "unpinned"
)
// HashAlgorithm represents a hash algorithm.
type HashAlgorithm string
const (
SHA256 HashAlgorithm = "sha2-256"
BLAKE3 HashAlgorithm = "blake3"
)
// EntryType represents a directory entry type.
type EntryType string
const (
FileType EntryType = "file"
DirectoryType EntryType = "directory"
)
// MatchType represents a pin matching type.
type MatchType string
const (
Exact MatchType = "exact"
IExact MatchType = "iexact"
Partial MatchType = "partial"
IPartial MatchType = "ipartial"
)
// Config holds client configuration.
type Config struct {
APIKey string
Endpoint string
Gateway string
PinningService string
ChunkSize int
Timeout time.Duration
Debug bool
}
// DefaultConfig returns a default configuration.
func DefaultConfig(apiKey string) Config {
return Config{
APIKey: apiKey,
Endpoint: DefaultEndpoint,
Gateway: DefaultGateway,
ChunkSize: 262144, // 256KB
Timeout: 30 * time.Second,
Debug: false,
}
}
// Client is the Synor Storage client.
type Client struct {
config Config
httpClient *http.Client
}
// NewClient creates a new client with the given API key.
func NewClient(apiKey string) *Client {
return NewClientWithConfig(DefaultConfig(apiKey))
}
// NewClientWithConfig creates a new client with custom configuration.
func NewClientWithConfig(config Config) *Client {
return &Client{
config: config,
httpClient: &http.Client{
Timeout: config.Timeout,
},
}
}
// UploadOptions contains options for uploading content.
type UploadOptions struct {
Pin bool
WrapWithDirectory bool
CIDVersion int
HashAlgorithm HashAlgorithm
}
// UploadResponse contains the result of an upload.
type UploadResponse struct {
CID string `json:"cid"`
Size int64 `json:"size"`
Name string `json:"name,omitempty"`
Hash string `json:"hash,omitempty"`
}
// DownloadOptions contains options for downloading content.
type DownloadOptions struct {
Offset int64
Length int64
}
// Pin represents pin information.
type Pin struct {
CID string `json:"cid"`
Status PinStatus `json:"status"`
Name string `json:"name,omitempty"`
Size int64 `json:"size,omitempty"`
CreatedAt int64 `json:"createdAt,omitempty"`
ExpiresAt int64 `json:"expiresAt,omitempty"`
Delegates []string `json:"delegates,omitempty"`
}
// PinRequest contains options for pinning content.
type PinRequest struct {
CID string `json:"cid"`
Name string `json:"name,omitempty"`
Duration int64 `json:"duration,omitempty"`
Origins []string `json:"origins,omitempty"`
}
// ListPinsOptions contains options for listing pins.
type ListPinsOptions struct {
Status []PinStatus
Match MatchType
Name string
Limit int
Offset int
}
// ListPinsResponse contains the result of listing pins.
type ListPinsResponse struct {
Pins []Pin `json:"pins"`
Total int `json:"total"`
HasMore bool `json:"hasMore"`
}
// GatewayURL represents a gateway URL.
type GatewayURL struct {
URL string
CID string
Path string
}
// CarBlock represents a CAR block.
type CarBlock struct {
CID string `json:"cid"`
Data string `json:"data"`
Size int64 `json:"size,omitempty"`
}
// CarFile represents a CAR file.
type CarFile struct {
Version int `json:"version"`
Roots []string `json:"roots"`
Blocks []CarBlock `json:"blocks,omitempty"`
Size int64 `json:"size,omitempty"`
}
// FileEntry represents a file entry for directory creation.
type FileEntry struct {
Name string
Content []byte
CID string
}
// DirectoryEntry represents a directory entry.
type DirectoryEntry struct {
Name string `json:"name"`
CID string `json:"cid"`
Size int64 `json:"size,omitempty"`
Type EntryType `json:"type"`
}
// ImportCarResponse contains the result of importing a CAR file.
type ImportCarResponse struct {
Roots []string `json:"roots"`
BlocksImported int `json:"blocksImported"`
}
// StorageStats represents storage statistics.
type StorageStats struct {
TotalSize int64 `json:"totalSize"`
PinCount int `json:"pinCount"`
Bandwidth struct {
Upload int64 `json:"upload"`
Download int64 `json:"download"`
} `json:"bandwidth,omitempty"`
}
// Upload uploads content to storage.
func (c *Client) Upload(ctx context.Context, data []byte, opts *UploadOptions) (*UploadResponse, error) {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
part, err := writer.CreateFormFile("file", "file")
if err != nil {
return nil, fmt.Errorf("failed to create form file: %w", err)
}
if _, err := part.Write(data); err != nil {
return nil, fmt.Errorf("failed to write data: %w", err)
}
if err := writer.Close(); err != nil {
return nil, fmt.Errorf("failed to close writer: %w", err)
}
params := url.Values{}
if opts != nil {
if opts.Pin {
params.Set("pin", "true")
}
if opts.WrapWithDirectory {
params.Set("wrapWithDirectory", "true")
}
if opts.CIDVersion != 0 {
params.Set("cidVersion", fmt.Sprintf("%d", opts.CIDVersion))
}
if opts.HashAlgorithm != "" {
params.Set("hashAlgorithm", string(opts.HashAlgorithm))
}
}
path := "/upload"
if len(params) > 0 {
path += "?" + params.Encode()
}
reqURL := c.config.Endpoint + path
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, &buf)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
req.Header.Set("Content-Type", writer.FormDataContentType())
if c.config.Debug {
fmt.Printf("[SynorStorage] POST %s\n", path)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, c.parseError(resp)
}
var result UploadResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &result, nil
}
// Download downloads content by CID.
func (c *Client) Download(ctx context.Context, cid string, opts *DownloadOptions) ([]byte, error) {
params := url.Values{}
if opts != nil {
if opts.Offset > 0 {
params.Set("offset", fmt.Sprintf("%d", opts.Offset))
}
if opts.Length > 0 {
params.Set("length", fmt.Sprintf("%d", opts.Length))
}
}
path := fmt.Sprintf("/content/%s", cid)
if len(params) > 0 {
path += "?" + params.Encode()
}
reqURL := c.config.Endpoint + path
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, c.parseError(resp)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
return data, nil
}
// DownloadReader returns a reader for downloading content.
func (c *Client) DownloadReader(ctx context.Context, cid string, opts *DownloadOptions) (io.ReadCloser, error) {
params := url.Values{}
if opts != nil {
if opts.Offset > 0 {
params.Set("offset", fmt.Sprintf("%d", opts.Offset))
}
if opts.Length > 0 {
params.Set("length", fmt.Sprintf("%d", opts.Length))
}
}
path := fmt.Sprintf("/content/%s/stream", cid)
if len(params) > 0 {
path += "?" + params.Encode()
}
reqURL := c.config.Endpoint + path
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
if resp.StatusCode >= 400 {
defer resp.Body.Close()
return nil, c.parseError(resp)
}
return resp.Body, nil
}
// Pin pins content by CID.
func (c *Client) Pin(ctx context.Context, req PinRequest) (*Pin, error) {
var result Pin
if err := c.request(ctx, "POST", "/pins", req, &result); err != nil {
return nil, err
}
return &result, nil
}
// Unpin unpins content by CID.
func (c *Client) Unpin(ctx context.Context, cid string) error {
return c.request(ctx, "DELETE", "/pins/"+cid, nil, nil)
}
// GetPinStatus gets the pin status for a CID.
func (c *Client) GetPinStatus(ctx context.Context, cid string) (*Pin, error) {
var result Pin
if err := c.request(ctx, "GET", "/pins/"+cid, nil, &result); err != nil {
return nil, err
}
return &result, nil
}
// ListPins lists pins.
func (c *Client) ListPins(ctx context.Context, opts *ListPinsOptions) (*ListPinsResponse, error) {
path := "/pins"
if opts != nil {
params := url.Values{}
if len(opts.Status) > 0 {
var statuses []string
for _, s := range opts.Status {
statuses = append(statuses, string(s))
}
params.Set("status", joinStrings(statuses, ","))
}
if opts.Match != "" {
params.Set("match", string(opts.Match))
}
if opts.Name != "" {
params.Set("name", opts.Name)
}
if opts.Limit > 0 {
params.Set("limit", fmt.Sprintf("%d", opts.Limit))
}
if opts.Offset > 0 {
params.Set("offset", fmt.Sprintf("%d", opts.Offset))
}
if len(params) > 0 {
path += "?" + params.Encode()
}
}
var result ListPinsResponse
if err := c.request(ctx, "GET", path, nil, &result); err != nil {
return nil, err
}
return &result, nil
}
// GetGatewayURL returns the gateway URL for content.
func (c *Client) GetGatewayURL(cid string, path string) GatewayURL {
fullPath := "/" + cid
if path != "" {
fullPath += "/" + path
}
return GatewayURL{
URL: c.config.Gateway + "/ipfs" + fullPath,
CID: cid,
Path: path,
}
}
// CreateCar creates a CAR file from files.
func (c *Client) CreateCar(ctx context.Context, files []FileEntry) (*CarFile, error) {
type fileData struct {
Name string `json:"name"`
Content string `json:"content,omitempty"`
CID string `json:"cid,omitempty"`
}
var entries []fileData
for _, f := range files {
entry := fileData{Name: f.Name}
if len(f.Content) > 0 {
entry.Content = base64.StdEncoding.EncodeToString(f.Content)
}
if f.CID != "" {
entry.CID = f.CID
}
entries = append(entries, entry)
}
var result CarFile
if err := c.request(ctx, "POST", "/car/create", map[string]interface{}{"files": entries}, &result); err != nil {
return nil, err
}
return &result, nil
}
// ImportCar imports a CAR file.
func (c *Client) ImportCar(ctx context.Context, carData []byte, pin bool) (*ImportCarResponse, error) {
encoded := base64.StdEncoding.EncodeToString(carData)
body := map[string]interface{}{
"car": encoded,
"pin": pin,
}
var result ImportCarResponse
if err := c.request(ctx, "POST", "/car/import", body, &result); err != nil {
return nil, err
}
return &result, nil
}
// ExportCar exports content as a CAR file.
func (c *Client) ExportCar(ctx context.Context, cid string) ([]byte, error) {
reqURL := c.config.Endpoint + "/car/export/" + cid
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, c.parseError(resp)
}
return io.ReadAll(resp.Body)
}
// CreateDirectory creates a directory from files.
func (c *Client) CreateDirectory(ctx context.Context, files []FileEntry) (*UploadResponse, error) {
type fileData struct {
Name string `json:"name"`
Content string `json:"content,omitempty"`
CID string `json:"cid,omitempty"`
}
var entries []fileData
for _, f := range files {
entry := fileData{Name: f.Name}
if len(f.Content) > 0 {
entry.Content = base64.StdEncoding.EncodeToString(f.Content)
}
if f.CID != "" {
entry.CID = f.CID
}
entries = append(entries, entry)
}
var result UploadResponse
if err := c.request(ctx, "POST", "/directory", map[string]interface{}{"files": entries}, &result); err != nil {
return nil, err
}
return &result, nil
}
// ListDirectory lists directory contents.
func (c *Client) ListDirectory(ctx context.Context, cid string, path string) ([]DirectoryEntry, error) {
apiPath := fmt.Sprintf("/directory/%s", cid)
if path != "" {
apiPath += "?path=" + url.QueryEscape(path)
}
var resp struct {
Entries []DirectoryEntry `json:"entries"`
}
if err := c.request(ctx, "GET", apiPath, nil, &resp); err != nil {
return nil, err
}
return resp.Entries, nil
}
// GetStats gets storage statistics.
func (c *Client) GetStats(ctx context.Context) (*StorageStats, error) {
var result StorageStats
if err := c.request(ctx, "GET", "/stats", nil, &result); err != nil {
return nil, err
}
return &result, nil
}
// Exists checks if content exists.
func (c *Client) Exists(ctx context.Context, cid string) (bool, error) {
reqURL := c.config.Endpoint + "/content/" + cid
req, err := http.NewRequestWithContext(ctx, "HEAD", reqURL, nil)
if err != nil {
return false, err
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return false, nil
}
defer resp.Body.Close()
return resp.StatusCode == 200, nil
}
// GetMetadata gets content metadata.
func (c *Client) GetMetadata(ctx context.Context, cid string) (map[string]interface{}, error) {
var result map[string]interface{}
if err := c.request(ctx, "GET", "/content/"+cid+"/metadata", nil, &result); err != nil {
return nil, err
}
return result, nil
}
func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
reqURL := c.config.Endpoint + path
var bodyReader io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
bodyReader = bytes.NewReader(bodyBytes)
}
req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
req.Header.Set("Content-Type", "application/json")
if c.config.Debug {
fmt.Printf("[SynorStorage] %s %s\n", method, path)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return c.parseError(resp)
}
if result != nil && resp.StatusCode != 204 {
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
}
return nil
}
func (c *Client) parseError(resp *http.Response) error {
var errResp struct {
Message string `json:"message"`
Code string `json:"code"`
}
json.NewDecoder(resp.Body).Decode(&errResp)
return &StorageError{
Message: errResp.Message,
StatusCode: resp.StatusCode,
Code: errResp.Code,
}
}
func joinStrings(strs []string, sep string) string {
if len(strs) == 0 {
return ""
}
result := strs[0]
for i := 1; i < len(strs); i++ {
result += sep + strs[i]
}
return result
}
// StorageError represents an API error.
type StorageError struct {
Message string
StatusCode int
Code string
}
func (e *StorageError) Error() string {
return fmt.Sprintf("storage: %s (status %d)", e.Message, e.StatusCode)
}