aenvs/internal/cli/push.go

147 lines
3.8 KiB
Go

package cli
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
"github.com/user/aevs/internal/archiver"
"github.com/user/aevs/internal/config"
"github.com/user/aevs/internal/storage"
"github.com/user/aevs/internal/types"
)
var (
pushConfig string
pushDryRun bool
)
var pushCmd = &cobra.Command{
Use: "push",
Short: "Push env files to storage",
Long: `Upload local .env files to cloud storage.`,
RunE: runPush,
}
func init() {
pushCmd.Flags().StringVarP(&pushConfig, "config", "c", config.DefaultProjectConfigFile, "path to project config")
pushCmd.Flags().BoolVar(&pushDryRun, "dry-run", false, "show what would be uploaded without uploading")
}
func runPush(cmd *cobra.Command, args []string) error {
// Load global config
globalCfg, err := config.LoadGlobalConfig()
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("no storage configured; run 'aevs config' first")
}
return fmt.Errorf("failed to load global config: %w", err)
}
// Load project config
projectCfg, err := config.LoadProjectConfig(pushConfig)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("no aevs.yaml found; run 'aevs init' first")
}
return fmt.Errorf("failed to load project config: %w", err)
}
currentDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
// Check that all files exist
var fileSizes []int64
for _, file := range projectCfg.Files {
fullPath := filepath.Join(currentDir, file)
info, err := os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("file not found: %s", file)
}
return fmt.Errorf("failed to stat file %s: %w", file, err)
}
fileSizes = append(fileSizes, info.Size())
}
// Create archive
archive, archiveSize, err := archiver.Create(projectCfg.Files, currentDir)
if err != nil {
return fmt.Errorf("failed to create archive: %w", err)
}
if pushDryRun {
fmt.Println()
fmt.Println("Dry run - no changes will be made")
fmt.Println()
fmt.Println("Would upload:")
for i, file := range projectCfg.Files {
fmt.Printf(" %s (%d bytes)\n", file, fileSizes[i])
}
fmt.Println()
fmt.Printf("Target: s3://%s/%s/%s\n", globalCfg.Storage.Bucket, projectCfg.Project, config.ArchiveFileName)
return nil
}
// Create storage client
s3Storage, err := storage.NewS3Storage(&globalCfg.Storage)
if err != nil {
return fmt.Errorf("failed to create storage client: %w", err)
}
ctx := context.Background()
fmt.Println()
fmt.Printf("Pushing %q to storage...\n", projectCfg.Project)
fmt.Println()
// Upload archive
archiveKey := fmt.Sprintf("%s/%s", projectCfg.Project, config.ArchiveFileName)
if err := s3Storage.Upload(ctx, archiveKey, archive, archiveSize); err != nil {
return fmt.Errorf("failed to upload archive: %w", err)
}
// Print file list
for i, file := range projectCfg.Files {
fmt.Printf(" ✓ %s (%d bytes)\n", file, fileSizes[i])
}
fmt.Println()
// Get hostname
hostname, _ := os.Hostname()
if hostname == "" {
hostname = "unknown"
}
// Create and upload metadata
metadata := types.Metadata{
UpdatedAt: time.Now(),
Files: projectCfg.Files,
Machine: hostname,
SizeBytes: archiveSize,
}
metadataJSON, err := json.Marshal(metadata)
if err != nil {
return fmt.Errorf("failed to marshal metadata: %w", err)
}
metadataKey := fmt.Sprintf("%s/%s", projectCfg.Project, config.MetadataFileName)
if err := s3Storage.Upload(ctx, metadataKey, bytes.NewReader(metadataJSON), int64(len(metadataJSON))); err != nil {
return fmt.Errorf("failed to upload metadata: %w", err)
}
fmt.Printf("Uploaded to s3://%s/%s\n", globalCfg.Storage.Bucket, archiveKey)
fmt.Printf("Total: %d files, %d bytes\n", len(projectCfg.Files), archiveSize)
return nil
}