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 }