Implement Go client SDK
Add complete Go SDK implementation with all 13 API functions: - ExecuteCode: Execute code synchronously - ExecuteAsync: Execute code asynchronously - GetJob: Get job status - WaitForJob: Poll job with exponential backoff - CancelJob: Cancel running job - ListJobs: List all jobs - GetLanguages: Get supported languages (1-hour cache) - DetectLanguage: Detect language from filename extension - SessionSnapshot: Create session snapshot (NEW) - ServiceSnapshot: Create service snapshot (NEW) - ListSnapshots: List all snapshots (NEW) - RestoreSnapshot: Restore from snapshot (NEW) - DeleteSnapshot: Delete snapshot (NEW) Features: - Standard Go conventions (PascalCase exports) - 4-tier credentials resolution (ResolveCredentials) - HMAC-SHA256 using crypto/hmac and crypto/sha256 - Exponential backoff with time.Sleep - Languages cache in ~/.unsandbox/languages.json (1-hour TTL) - Comprehensive error handling with custom CredentialsError type - Full documentation comments for all exported functions
This commit is contained in:
parent
a5f7532fb2
commit
1eb28e2c04
1 changed files with 645 additions and 0 deletions
645
clients/go/sync/src/un.go
Normal file
645
clients/go/sync/src/un.go
Normal file
|
|
@ -0,0 +1,645 @@
|
|||
/*
|
||||
PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
|
||||
unsandbox.com Go SDK (Synchronous)
|
||||
|
||||
Library Usage:
|
||||
import "un"
|
||||
|
||||
// Create credentials
|
||||
creds, err := un.ResolveCredentials("", "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Execute code synchronously
|
||||
result, err := un.ExecuteCode(creds, "python", `print("hello")`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Execute asynchronously
|
||||
jobID, err := un.ExecuteAsync(creds, "javascript", `console.log("hello")`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for job completion with exponential backoff
|
||||
result, err := un.WaitForJob(creds, jobID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// List all jobs
|
||||
jobs, err := un.ListJobs(creds)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Get supported languages
|
||||
languages, err := un.GetLanguages(creds)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Detect language from filename
|
||||
lang := un.DetectLanguage("script.py") // Returns "python"
|
||||
|
||||
// Snapshot operations (NEW)
|
||||
snapshotID, err := un.SessionSnapshot(creds, sessionID, "my_snapshot", false)
|
||||
snapshots, err := un.ListSnapshots(creds)
|
||||
result, err := un.RestoreSnapshot(creds, snapshotID)
|
||||
err = un.DeleteSnapshot(creds, snapshotID)
|
||||
|
||||
Authentication Priority (4-tier):
|
||||
1. Function arguments (publicKey, secretKey)
|
||||
2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
||||
4. Local directory (./accounts.csv, line 0 by default)
|
||||
|
||||
Format: public_key,secret_key (one per line)
|
||||
Account selection: UNSANDBOX_ACCOUNT=N env var (0-based index)
|
||||
|
||||
Request Authentication (HMAC-SHA256):
|
||||
Authorization: Bearer <public_key> (identifies account)
|
||||
X-Timestamp: <unix_seconds> (replay prevention)
|
||||
X-Signature: HMAC-SHA256(secret_key, msg) (proves secret + body integrity)
|
||||
|
||||
Message format: "timestamp:METHOD:path:body"
|
||||
- timestamp: seconds since epoch
|
||||
- METHOD: GET, POST, DELETE, etc. (uppercase)
|
||||
- path: e.g., "/execute", "/jobs/123"
|
||||
- body: JSON payload (empty string for GET/DELETE)
|
||||
|
||||
Languages Cache:
|
||||
- Cached in ~/.unsandbox/languages.json
|
||||
- TTL: 1 hour
|
||||
- Updated on successful API calls
|
||||
*/
|
||||
|
||||
package un
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
APIBase = "https://api.unsandbox.com"
|
||||
LanguagesCacheTTL = 3600 // 1 hour
|
||||
)
|
||||
|
||||
var (
|
||||
PollDelaysMs = []int{300, 450, 700, 900, 650, 1600, 2000}
|
||||
|
||||
// Language detection mapping (file extension -> language)
|
||||
LanguageMap = map[string]string{
|
||||
"py": "python",
|
||||
"js": "javascript",
|
||||
"ts": "typescript",
|
||||
"rb": "ruby",
|
||||
"php": "php",
|
||||
"pl": "perl",
|
||||
"sh": "bash",
|
||||
"r": "r",
|
||||
"R": "r",
|
||||
"lua": "lua",
|
||||
"go": "go",
|
||||
"rs": "rust",
|
||||
"c": "c",
|
||||
"cpp": "cpp",
|
||||
"cc": "cpp",
|
||||
"cxx": "cpp",
|
||||
"java": "java",
|
||||
"kt": "kotlin",
|
||||
"m": "objc",
|
||||
"cs": "csharp",
|
||||
"fs": "fsharp",
|
||||
"hs": "haskell",
|
||||
"ml": "ocaml",
|
||||
"clj": "clojure",
|
||||
"scm": "scheme",
|
||||
"ss": "scheme",
|
||||
"erl": "erlang",
|
||||
"ex": "elixir",
|
||||
"exs": "elixir",
|
||||
"jl": "julia",
|
||||
"d": "d",
|
||||
"nim": "nim",
|
||||
"zig": "zig",
|
||||
"v": "v",
|
||||
"cr": "crystal",
|
||||
"dart": "dart",
|
||||
"groovy": "groovy",
|
||||
"f90": "fortran",
|
||||
"f95": "fortran",
|
||||
"lisp": "commonlisp",
|
||||
"lsp": "commonlisp",
|
||||
"cob": "cobol",
|
||||
"tcl": "tcl",
|
||||
"raku": "raku",
|
||||
"pro": "prolog",
|
||||
"p": "prolog",
|
||||
"4th": "forth",
|
||||
"forth": "forth",
|
||||
"fth": "forth",
|
||||
}
|
||||
)
|
||||
|
||||
// CredentialsError is raised when credentials cannot be found or are invalid
|
||||
type CredentialsError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *CredentialsError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// Credentials holds the public and secret API keys
|
||||
type Credentials struct {
|
||||
PublicKey string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
// getUnsandboxDir returns ~/.unsandbox directory path, creating if necessary
|
||||
func getUnsandboxDir() (string, error) {
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dir := filepath.Join(user.HomeDir, ".unsandbox")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// loadCredentialsFromCsv loads credentials from CSV file (public_key,secret_key per line)
|
||||
func loadCredentialsFromCsv(csvPath string, accountIndex int) *Credentials {
|
||||
data, err := os.ReadFile(csvPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
currentIndex := 0
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if currentIndex == accountIndex {
|
||||
parts := strings.Split(line, ",")
|
||||
if len(parts) >= 2 {
|
||||
return &Credentials{
|
||||
PublicKey: strings.TrimSpace(parts[0]),
|
||||
SecretKey: strings.TrimSpace(parts[1]),
|
||||
}
|
||||
}
|
||||
}
|
||||
currentIndex++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveCredentials resolves credentials from 4-tier priority system.
|
||||
//
|
||||
// Priority:
|
||||
// 1. Function arguments (publicKey, secretKey non-empty)
|
||||
// 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
// 3. ~/.unsandbox/accounts.csv
|
||||
// 4. ./accounts.csv
|
||||
func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
||||
// Tier 1: Function arguments
|
||||
if publicKey != "" && secretKey != "" {
|
||||
return &Credentials{
|
||||
PublicKey: publicKey,
|
||||
SecretKey: secretKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Tier 2: Environment variables
|
||||
envPk := os.Getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
envSk := os.Getenv("UNSANDBOX_SECRET_KEY")
|
||||
if envPk != "" && envSk != "" {
|
||||
return &Credentials{
|
||||
PublicKey: envPk,
|
||||
SecretKey: envSk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Determine account index
|
||||
accountIndex := 0
|
||||
if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" {
|
||||
var err error
|
||||
accountIndex, err = strconv.Atoi(envAccount)
|
||||
if err != nil {
|
||||
accountIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 3: ~/.unsandbox/accounts.csv
|
||||
unsandboxDir, err := getUnsandboxDir()
|
||||
if err == nil {
|
||||
if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), accountIndex); creds != nil {
|
||||
return creds, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 4: ./accounts.csv
|
||||
if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil {
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
return nil, &CredentialsError{
|
||||
Message: "No credentials found. Please provide via:\n" +
|
||||
" 1. Function arguments (publicKey, secretKey)\n" +
|
||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
||||
" 3. ~/.unsandbox/accounts.csv\n" +
|
||||
" 4. ./accounts.csv",
|
||||
}
|
||||
}
|
||||
|
||||
// signRequest signs a request using HMAC-SHA256
|
||||
//
|
||||
// Message format: "timestamp:METHOD:path:body"
|
||||
// Returns: 64-character hex string
|
||||
func signRequest(secretKey string, timestamp int64, method, path string, body []byte) string {
|
||||
bodyStr := ""
|
||||
if body != nil {
|
||||
bodyStr = string(body)
|
||||
}
|
||||
message := fmt.Sprintf("%d:%s:%s:%s", timestamp, method, path, bodyStr)
|
||||
|
||||
h := hmac.New(sha256.New, []byte(secretKey))
|
||||
h.Write([]byte(message))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// makeRequest makes an authenticated HTTP request to the API
|
||||
func makeRequest(method, path string, creds *Credentials, data interface{}) (map[string]interface{}, error) {
|
||||
url := APIBase + path
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
var body []byte
|
||||
var err error
|
||||
if data != nil {
|
||||
body, err = json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
signature := signRequest(creds.SecretKey, timestamp, method, path, body)
|
||||
|
||||
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", creds.PublicKey))
|
||||
req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
|
||||
req.Header.Set("X-Signature", signature)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "un-go/2.0")
|
||||
|
||||
client := &http.Client{Timeout: 120 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getLanguagesCachePath returns path to languages cache file
|
||||
func getLanguagesCachePath() (string, error) {
|
||||
unsandboxDir, err := getUnsandboxDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(unsandboxDir, "languages.json"), nil
|
||||
}
|
||||
|
||||
// loadLanguagesCache loads languages from cache if valid (< 1 hour old)
|
||||
func loadLanguagesCache() ([]string, bool) {
|
||||
cachePath, err := getLanguagesCachePath()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(cachePath)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
stat, err := os.Stat(cachePath)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ageSeconds := int(time.Since(stat.ModTime()).Seconds())
|
||||
if ageSeconds >= LanguagesCacheTTL {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var cacheData map[string]interface{}
|
||||
if err := json.Unmarshal(data, &cacheData); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if langs, ok := cacheData["languages"].([]interface{}); ok {
|
||||
result := make([]string, len(langs))
|
||||
for i, lang := range langs {
|
||||
if s, ok := lang.(string); ok {
|
||||
result[i] = s
|
||||
}
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// saveLanguagesCache saves languages to cache
|
||||
func saveLanguagesCache(languages []string) {
|
||||
cachePath, err := getLanguagesCachePath()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cacheData := map[string]interface{}{
|
||||
"languages": languages,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cacheData, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = os.WriteFile(cachePath, data, 0600)
|
||||
}
|
||||
|
||||
// ExecuteCode executes code synchronously (blocks until completion)
|
||||
func ExecuteCode(creds *Credentials, language, code string) (map[string]interface{}, error) {
|
||||
data := map[string]string{
|
||||
"language": language,
|
||||
"code": code,
|
||||
}
|
||||
|
||||
response, err := makeRequest("POST", "/execute", creds, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we got a job_id, poll until completion
|
||||
if jobID, ok := response["job_id"].(string); ok {
|
||||
if status, ok := response["status"].(string); ok && (status == "pending" || status == "running") {
|
||||
return WaitForJob(creds, jobID)
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ExecuteAsync executes code asynchronously (returns immediately with job_id)
|
||||
func ExecuteAsync(creds *Credentials, language, code string) (string, error) {
|
||||
data := map[string]string{
|
||||
"language": language,
|
||||
"code": code,
|
||||
}
|
||||
|
||||
response, err := makeRequest("POST", "/execute", creds, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if jobID, ok := response["job_id"].(string); ok {
|
||||
return jobID, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no job_id in response")
|
||||
}
|
||||
|
||||
// GetJob gets current status/result of a job (single poll, no waiting)
|
||||
func GetJob(creds *Credentials, jobID string) (map[string]interface{}, error) {
|
||||
return makeRequest("GET", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
|
||||
}
|
||||
|
||||
// WaitForJob waits for job completion with exponential backoff polling
|
||||
//
|
||||
// Polling delays (ms): [300, 450, 700, 900, 650, 1600, 2000, ...]
|
||||
// Cumulative: 300, 750, 1450, 2350, 3000, 4600, 6600ms+
|
||||
func WaitForJob(creds *Credentials, jobID string) (map[string]interface{}, error) {
|
||||
pollCount := 0
|
||||
|
||||
for {
|
||||
// Sleep before polling
|
||||
delayIdx := pollCount
|
||||
if delayIdx >= len(PollDelaysMs) {
|
||||
delayIdx = len(PollDelaysMs) - 1
|
||||
}
|
||||
time.Sleep(time.Duration(PollDelaysMs[delayIdx]) * time.Millisecond)
|
||||
pollCount++
|
||||
|
||||
response, err := GetJob(creds, jobID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status, ok := response["status"].(string); ok {
|
||||
if status == "completed" || status == "failed" || status == "timeout" || status == "cancelled" {
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Still running, continue polling
|
||||
}
|
||||
}
|
||||
|
||||
// CancelJob cancels a running job
|
||||
func CancelJob(creds *Credentials, jobID string) (map[string]interface{}, error) {
|
||||
return makeRequest("DELETE", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
|
||||
}
|
||||
|
||||
// ListJobs lists all jobs for the authenticated account
|
||||
func ListJobs(creds *Credentials) ([]map[string]interface{}, error) {
|
||||
response, err := makeRequest("GET", "/jobs", creds, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if jobs, ok := response["jobs"].([]interface{}); ok {
|
||||
result := make([]map[string]interface{}, len(jobs))
|
||||
for i, job := range jobs {
|
||||
if m, ok := job.(map[string]interface{}); ok {
|
||||
result[i] = m
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return []map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
// GetLanguages gets list of supported programming languages
|
||||
//
|
||||
// Results are cached for 1 hour in ~/.unsandbox/languages.json
|
||||
func GetLanguages(creds *Credentials) ([]string, error) {
|
||||
// Try cache first
|
||||
if cached, ok := loadLanguagesCache(); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
response, err := makeRequest("GET", "/languages", creds, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var languages []string
|
||||
if langs, ok := response["languages"].([]interface{}); ok {
|
||||
for _, lang := range langs {
|
||||
if s, ok := lang.(string); ok {
|
||||
languages = append(languages, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
saveLanguagesCache(languages)
|
||||
return languages, nil
|
||||
}
|
||||
|
||||
// DetectLanguage detects programming language from filename extension
|
||||
//
|
||||
// Args:
|
||||
// filename: Filename to detect language from (e.g., "script.py")
|
||||
//
|
||||
// Returns:
|
||||
// Language identifier (e.g., "python") or empty string if unknown
|
||||
//
|
||||
// Examples:
|
||||
// DetectLanguage("hello.py") // -> "python"
|
||||
// DetectLanguage("script.js") // -> "javascript"
|
||||
// DetectLanguage("main.go") // -> "go"
|
||||
// DetectLanguage("unknown") // -> ""
|
||||
func DetectLanguage(filename string) string {
|
||||
if !strings.Contains(filename, ".") {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(filename, ".")
|
||||
ext := strings.ToLower(parts[len(parts)-1])
|
||||
|
||||
if lang, ok := LanguageMap[ext]; ok {
|
||||
return lang
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// SessionSnapshot creates a snapshot of a session (NEW)
|
||||
func SessionSnapshot(creds *Credentials, sessionID, name string, hot bool) (string, error) {
|
||||
data := map[string]interface{}{
|
||||
"session_id": sessionID,
|
||||
"hot": hot,
|
||||
}
|
||||
if name != "" {
|
||||
data["name"] = name
|
||||
}
|
||||
|
||||
response, err := makeRequest("POST", "/snapshots", creds, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if snapshotID, ok := response["snapshot_id"].(string); ok {
|
||||
return snapshotID, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no snapshot_id in response")
|
||||
}
|
||||
|
||||
// ServiceSnapshot creates a snapshot of a service (NEW)
|
||||
func ServiceSnapshot(creds *Credentials, serviceID, name string, hot bool) (string, error) {
|
||||
data := map[string]interface{}{
|
||||
"service_id": serviceID,
|
||||
"hot": hot,
|
||||
}
|
||||
if name != "" {
|
||||
data["name"] = name
|
||||
}
|
||||
|
||||
response, err := makeRequest("POST", "/snapshots", creds, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if snapshotID, ok := response["snapshot_id"].(string); ok {
|
||||
return snapshotID, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no snapshot_id in response")
|
||||
}
|
||||
|
||||
// ListSnapshots lists all snapshots (NEW)
|
||||
func ListSnapshots(creds *Credentials) ([]map[string]interface{}, error) {
|
||||
response, err := makeRequest("GET", "/snapshots", creds, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if snapshots, ok := response["snapshots"].([]interface{}); ok {
|
||||
result := make([]map[string]interface{}, len(snapshots))
|
||||
for i, snapshot := range snapshots {
|
||||
if m, ok := snapshot.(map[string]interface{}); ok {
|
||||
result[i] = m
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return []map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
// RestoreSnapshot restores a snapshot (NEW)
|
||||
func RestoreSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
|
||||
return makeRequest("POST", fmt.Sprintf("/snapshots/%s/restore", snapshotID), creds, map[string]interface{}{})
|
||||
}
|
||||
|
||||
// DeleteSnapshot deletes a snapshot (NEW)
|
||||
func DeleteSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
|
||||
return makeRequest("DELETE", fmt.Sprintf("/snapshots/%s", snapshotID), creds, nil)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue