|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "flag" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "path/filepath" |
| 8 | + "strings" |
| 9 | + "time" |
| 10 | +) |
| 11 | + |
| 12 | +// stringSliceFlag implements flag.Value for handling multiple file arguments |
| 13 | +type stringSliceFlag []string |
| 14 | + |
| 15 | +func (s *stringSliceFlag) String() string { |
| 16 | + return strings.Join(*s, ",") |
| 17 | +} |
| 18 | + |
| 19 | +func (s *stringSliceFlag) Set(value string) error { |
| 20 | + *s = append(*s, value) |
| 21 | + return nil |
| 22 | +} |
| 23 | + |
| 24 | +// CLIArgs holds parsed command line arguments |
| 25 | +type CLIArgs struct { |
| 26 | + // Operations |
| 27 | + Encrypt bool |
| 28 | + Decrypt bool |
| 29 | + GenerateKeys bool |
| 30 | + |
| 31 | + // Files and keys |
| 32 | + Files []string |
| 33 | + Key string |
| 34 | + Password string |
| 35 | + KeyBaseName string |
| 36 | + |
| 37 | + // Configuration |
| 38 | + ConfigFile string |
| 39 | + Timeout time.Duration |
| 40 | + |
| 41 | + // Output options |
| 42 | + Verbose bool |
| 43 | + Quiet bool |
| 44 | + |
| 45 | + // Web UI options |
| 46 | + WebUI bool |
| 47 | + WebPort int |
| 48 | + WebHost string |
| 49 | + WebTLS bool |
| 50 | + CertFile string |
| 51 | + KeyFile string |
| 52 | + |
| 53 | + // Additional options |
| 54 | + ShowConfig bool |
| 55 | + SaveConfig string |
| 56 | + ShowVersion bool |
| 57 | + ShowHelp bool |
| 58 | +} |
| 59 | + |
| 60 | +// ParseCLI parses command line arguments and returns CLIArgs |
| 61 | +func ParseCLI() (*CLIArgs, error) { |
| 62 | + args := &CLIArgs{} |
| 63 | + var files stringSliceFlag |
| 64 | + |
| 65 | + // Define flags |
| 66 | + flag.BoolVar(&args.Encrypt, "e", false, "Encrypt the file(s)") |
| 67 | + flag.BoolVar(&args.Encrypt, "encrypt", false, "Encrypt the file(s)") |
| 68 | + |
| 69 | + flag.BoolVar(&args.Decrypt, "d", false, "Decrypt the file(s)") |
| 70 | + flag.BoolVar(&args.Decrypt, "decrypt", false, "Decrypt the file(s)") |
| 71 | + |
| 72 | + flag.Var(&files, "file", "Files to encrypt or decrypt (can be specified multiple times)") |
| 73 | + flag.Var(&files, "f", "Files to encrypt or decrypt (shorthand)") |
| 74 | + |
| 75 | + flag.StringVar(&args.Key, "key", "", "Path to the key file") |
| 76 | + flag.StringVar(&args.Key, "k", "", "Path to the key file (shorthand)") |
| 77 | + |
| 78 | + flag.StringVar(&args.Password, "password", "", "Password for encryption/decryption") |
| 79 | + flag.StringVar(&args.Password, "p", "", "Password for encryption/decryption (shorthand)") |
| 80 | + |
| 81 | + flag.BoolVar(&args.GenerateKeys, "generate-keys", false, "Generate a new RSA key pair") |
| 82 | + flag.StringVar(&args.KeyBaseName, "key-name", "key", "Base name for the generated key files") |
| 83 | + |
| 84 | + flag.StringVar(&args.ConfigFile, "config", "", "Path to configuration file") |
| 85 | + flag.StringVar(&args.ConfigFile, "c", "", "Path to configuration file (shorthand)") |
| 86 | + |
| 87 | + flag.DurationVar(&args.Timeout, "timeout", 0, "Timeout for the entire operation") |
| 88 | + flag.DurationVar(&args.Timeout, "t", 0, "Timeout for the entire operation (shorthand)") |
| 89 | + |
| 90 | + flag.BoolVar(&args.Verbose, "verbose", false, "Enable verbose output") |
| 91 | + flag.BoolVar(&args.Verbose, "v", false, "Enable verbose output (shorthand)") |
| 92 | + |
| 93 | + flag.BoolVar(&args.Quiet, "quiet", false, "Suppress non-error output") |
| 94 | + flag.BoolVar(&args.Quiet, "q", false, "Suppress non-error output (shorthand)") |
| 95 | + |
| 96 | + flag.BoolVar(&args.ShowConfig, "show-config", false, "Show current configuration and exit") |
| 97 | + flag.StringVar(&args.SaveConfig, "save-config", "", "Save current configuration to file") |
| 98 | + |
| 99 | + flag.BoolVar(&args.WebUI, "web", false, "Start web UI server") |
| 100 | + flag.IntVar(&args.WebPort, "web-port", 8080, "Port for web UI server") |
| 101 | + flag.StringVar(&args.WebHost, "web-host", "localhost", "Host for web UI server") |
| 102 | + flag.BoolVar(&args.WebTLS, "web-tls", false, "Enable TLS for web UI") |
| 103 | + flag.StringVar(&args.CertFile, "cert-file", "", "TLS certificate file") |
| 104 | + flag.StringVar(&args.KeyFile, "key-file", "", "TLS private key file") |
| 105 | + |
| 106 | + flag.BoolVar(&args.ShowVersion, "version", false, "Show version information") |
| 107 | + flag.BoolVar(&args.ShowHelp, "help", false, "Show help information") |
| 108 | + flag.BoolVar(&args.ShowHelp, "h", false, "Show help information (shorthand)") |
| 109 | + |
| 110 | + // Custom usage function |
| 111 | + flag.Usage = func() { |
| 112 | + fmt.Fprintf(os.Stderr, "File Encryptor - Secure file encryption tool\n\n") |
| 113 | + fmt.Fprintf(os.Stderr, "Usage: %s [options] [files...]\n\n", os.Args[0]) |
| 114 | + fmt.Fprintf(os.Stderr, "Operations:\n") |
| 115 | + fmt.Fprintf(os.Stderr, " -e, --encrypt Encrypt the specified files\n") |
| 116 | + fmt.Fprintf(os.Stderr, " -d, --decrypt Decrypt the specified files\n") |
| 117 | + fmt.Fprintf(os.Stderr, " --generate-keys Generate a new RSA key pair\n") |
| 118 | + fmt.Fprintf(os.Stderr, "\nFiles and Keys:\n") |
| 119 | + fmt.Fprintf(os.Stderr, " -f, --file FILE Files to process (can be used multiple times)\n") |
| 120 | + fmt.Fprintf(os.Stderr, " -k, --key FILE Path to key file (public for encrypt, private for decrypt)\n") |
| 121 | + fmt.Fprintf(os.Stderr, " -p, --password PASS Password for encryption/decryption\n") |
| 122 | + fmt.Fprintf(os.Stderr, " --key-name NAME Base name for generated key files (default: key)\n") |
| 123 | + fmt.Fprintf(os.Stderr, "\nConfiguration:\n") |
| 124 | + fmt.Fprintf(os.Stderr, " -c, --config FILE Path to configuration file\n") |
| 125 | + fmt.Fprintf(os.Stderr, " -t, --timeout DURATION Timeout for operation (e.g., 30m, 1h)\n") |
| 126 | + fmt.Fprintf(os.Stderr, " --show-config Show current configuration\n") |
| 127 | + fmt.Fprintf(os.Stderr, " --save-config FILE Save current configuration to file\n") |
| 128 | + fmt.Fprintf(os.Stderr, "\nOutput:\n") |
| 129 | + fmt.Fprintf(os.Stderr, " -v, --verbose Enable verbose output\n") |
| 130 | + fmt.Fprintf(os.Stderr, " -q, --quiet Suppress non-error output\n") |
| 131 | + fmt.Fprintf(os.Stderr, " --version Show version information\n") |
| 132 | + fmt.Fprintf(os.Stderr, " -h, --help Show this help message\n") |
| 133 | + fmt.Fprintf(os.Stderr, "\nExamples:\n") |
| 134 | + fmt.Fprintf(os.Stderr, " # Encrypt files with password\n") |
| 135 | + fmt.Fprintf(os.Stderr, " %s -e -p mypassword file1.txt file2.pdf\n", os.Args[0]) |
| 136 | + fmt.Fprintf(os.Stderr, "\n # Encrypt with RSA public key\n") |
| 137 | + fmt.Fprintf(os.Stderr, " %s -e -k public.key -f document.docx\n", os.Args[0]) |
| 138 | + fmt.Fprintf(os.Stderr, "\n # Decrypt with private key\n") |
| 139 | + fmt.Fprintf(os.Stderr, " %s -d -k private.key document.docx.enc\n", os.Args[0]) |
| 140 | + fmt.Fprintf(os.Stderr, "\n # Generate keys and encrypt\n") |
| 141 | + fmt.Fprintf(os.Stderr, " %s --generate-keys -e -f secret.txt\n", os.Args[0]) |
| 142 | + fmt.Fprintf(os.Stderr, "\nEnvironment Variables:\n") |
| 143 | + fmt.Fprintf(os.Stderr, " FILE_ENCRYPTOR_LOG_LEVEL Set log level (debug, info, warn, error)\n") |
| 144 | + fmt.Fprintf(os.Stderr, " FILE_ENCRYPTOR_MAX_WORKERS Set maximum worker threads\n") |
| 145 | + fmt.Fprintf(os.Stderr, " FILE_ENCRYPTOR_TIMEOUT Set default timeout\n") |
| 146 | + fmt.Fprintf(os.Stderr, " FILE_ENCRYPTOR_DEBUG Enable debug mode (true/false)\n") |
| 147 | + } |
| 148 | + |
| 149 | + // Parse flags |
| 150 | + flag.Parse() |
| 151 | + |
| 152 | + // Add remaining arguments as files (if they don't start with -) |
| 153 | + remainingArgs := flag.Args() |
| 154 | + for _, arg := range remainingArgs { |
| 155 | + if !strings.HasPrefix(arg, "-") { |
| 156 | + files = append(files, arg) |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + args.Files = []string(files) |
| 161 | + |
| 162 | + return args, nil |
| 163 | +} |
| 164 | + |
| 165 | +// ValidateArgs validates the parsed CLI arguments |
| 166 | +func ValidateArgs(args *CLIArgs, config *Config) error { |
| 167 | + // Handle special cases first |
| 168 | + if args.ShowHelp { |
| 169 | + flag.Usage() |
| 170 | + os.Exit(0) |
| 171 | + } |
| 172 | + |
| 173 | + if args.ShowVersion { |
| 174 | + if Version != "dev" { |
| 175 | + fmt.Printf("File Encryptor %s\n", Version) |
| 176 | + fmt.Printf("Git Commit: %s\n", GitCommit) |
| 177 | + fmt.Printf("Build Time: %s\n", BuildTime) |
| 178 | + } else { |
| 179 | + fmt.Printf("File Encryptor %s (development build)\n", AppVersion) |
| 180 | + } |
| 181 | + fmt.Printf("Built with Go %s\n", "1.23+") |
| 182 | + os.Exit(0) |
| 183 | + } |
| 184 | + |
| 185 | + if args.ShowConfig { |
| 186 | + return nil // Will be handled in main |
| 187 | + } |
| 188 | + |
| 189 | + if args.SaveConfig != "" { |
| 190 | + return nil // Will be handled in main |
| 191 | + } |
| 192 | + |
| 193 | + // Validate conflicting options |
| 194 | + if args.Verbose && args.Quiet { |
| 195 | + return fmt.Errorf("cannot specify both --verbose and --quiet") |
| 196 | + } |
| 197 | + |
| 198 | + // Handle key generation special case |
| 199 | + if args.GenerateKeys && !args.Encrypt && len(args.Files) == 0 { |
| 200 | + return nil // Just generating keys |
| 201 | + } |
| 202 | + |
| 203 | + if args.GenerateKeys { |
| 204 | + if args.Decrypt || args.Key != "" || args.Password != "" { |
| 205 | + return fmt.Errorf("--generate-keys cannot be combined with decrypt, key, or password options") |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + // Validate operation selection |
| 210 | + if (args.Encrypt && args.Decrypt) || (!args.Encrypt && !args.Decrypt && !args.GenerateKeys) { |
| 211 | + return fmt.Errorf("please specify either -e/--encrypt or -d/--decrypt") |
| 212 | + } |
| 213 | + |
| 214 | + // Validate files |
| 215 | + if len(args.Files) == 0 && !args.GenerateKeys { |
| 216 | + return fmt.Errorf("please provide at least one file using --file/-f or as arguments") |
| 217 | + } |
| 218 | + |
| 219 | + // Validate authentication method |
| 220 | + if args.Key == "" && args.Password == "" && !args.GenerateKeys { |
| 221 | + return fmt.Errorf("please provide either --key/-k or --password/-p") |
| 222 | + } |
| 223 | + |
| 224 | + if args.Key != "" && args.Password != "" { |
| 225 | + return fmt.Errorf("please provide either --key/-k or --password/-p, not both") |
| 226 | + } |
| 227 | + |
| 228 | + // Validate file existence and permissions |
| 229 | + if err := validateFiles(args.Files, args.Encrypt); err != nil { |
| 230 | + return err |
| 231 | + } |
| 232 | + |
| 233 | + // Validate key file if specified |
| 234 | + if args.Key != "" { |
| 235 | + if err := validateKeyFile(args.Key); err != nil { |
| 236 | + return fmt.Errorf("key file validation failed: %w", err) |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + return nil |
| 241 | +} |
| 242 | + |
| 243 | +// validateFiles checks if the specified files exist and are accessible |
| 244 | +func validateFiles(files []string, isEncryption bool) error { |
| 245 | + for _, file := range files { |
| 246 | + if err := validateSingleFile(file, isEncryption); err != nil { |
| 247 | + return fmt.Errorf("file '%s': %w", file, err) |
| 248 | + } |
| 249 | + } |
| 250 | + return nil |
| 251 | +} |
| 252 | + |
| 253 | +// validateSingleFile validates a single file |
| 254 | +func validateSingleFile(file string, isEncryption bool) error { |
| 255 | + info, err := os.Stat(file) |
| 256 | + if err != nil { |
| 257 | + if os.IsNotExist(err) { |
| 258 | + return fmt.Errorf("file does not exist") |
| 259 | + } |
| 260 | + return fmt.Errorf("cannot access file: %w", err) |
| 261 | + } |
| 262 | + |
| 263 | + if info.IsDir() { |
| 264 | + return fmt.Errorf("is a directory, not a file") |
| 265 | + } |
| 266 | + |
| 267 | + // Check file permissions |
| 268 | + if isEncryption { |
| 269 | + // For encryption, we need read access |
| 270 | + if err := checkReadPermission(file); err != nil { |
| 271 | + return fmt.Errorf("cannot read file: %w", err) |
| 272 | + } |
| 273 | + } else { |
| 274 | + // For decryption, check if it looks like an encrypted file |
| 275 | + if !strings.HasSuffix(file, ".enc") { |
| 276 | + return fmt.Errorf("file does not appear to be encrypted (missing .enc extension)") |
| 277 | + } |
| 278 | + if err := checkReadPermission(file); err != nil { |
| 279 | + return fmt.Errorf("cannot read encrypted file: %w", err) |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + // Check if file is too large (optional warning) |
| 284 | + if info.Size() > 10<<30 { // 10GB |
| 285 | + fmt.Fprintf(os.Stderr, "Warning: File '%s' is very large (%s). This may take a long time.\n", |
| 286 | + file, formatBytes(info.Size())) |
| 287 | + } |
| 288 | + |
| 289 | + return nil |
| 290 | +} |
| 291 | + |
| 292 | +// validateKeyFile checks if the key file exists and is accessible |
| 293 | +func validateKeyFile(keyFile string) error { |
| 294 | + info, err := os.Stat(keyFile) |
| 295 | + if err != nil { |
| 296 | + if os.IsNotExist(err) { |
| 297 | + return fmt.Errorf("key file does not exist") |
| 298 | + } |
| 299 | + return fmt.Errorf("cannot access key file: %w", err) |
| 300 | + } |
| 301 | + |
| 302 | + if info.IsDir() { |
| 303 | + return fmt.Errorf("key path is a directory, not a file") |
| 304 | + } |
| 305 | + |
| 306 | + if err := checkReadPermission(keyFile); err != nil { |
| 307 | + return fmt.Errorf("cannot read key file: %w", err) |
| 308 | + } |
| 309 | + |
| 310 | + return nil |
| 311 | +} |
| 312 | + |
| 313 | +// checkReadPermission checks if we can read the file |
| 314 | +func checkReadPermission(file string) error { |
| 315 | + f, err := os.Open(file) |
| 316 | + if err != nil { |
| 317 | + return err |
| 318 | + } |
| 319 | + f.Close() |
| 320 | + return nil |
| 321 | +} |
| 322 | + |
| 323 | +// formatBytes formats byte count as human readable string |
| 324 | +func formatBytes(bytes int64) string { |
| 325 | + const unit = 1024 |
| 326 | + if bytes < unit { |
| 327 | + return fmt.Sprintf("%d B", bytes) |
| 328 | + } |
| 329 | + div, exp := int64(unit), 0 |
| 330 | + for n := bytes / unit; n >= unit; n /= unit { |
| 331 | + div *= unit |
| 332 | + exp++ |
| 333 | + } |
| 334 | + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) |
| 335 | +} |
| 336 | + |
| 337 | +// GetConfigPath returns the configuration file path, checking multiple locations |
| 338 | +func GetConfigPath(specified string) string { |
| 339 | + if specified != "" { |
| 340 | + return specified |
| 341 | + } |
| 342 | + |
| 343 | + // Check common configuration locations |
| 344 | + locations := []string{ |
| 345 | + "./file-encryptor.yaml", |
| 346 | + "./file-encryptor.yml", |
| 347 | + "~/.config/file-encryptor/config.yaml", |
| 348 | + "~/.file-encryptor.yaml", |
| 349 | + } |
| 350 | + |
| 351 | + for _, location := range locations { |
| 352 | + // Expand home directory |
| 353 | + if strings.HasPrefix(location, "~/") { |
| 354 | + home, err := os.UserHomeDir() |
| 355 | + if err == nil { |
| 356 | + location = filepath.Join(home, location[2:]) |
| 357 | + } |
| 358 | + } |
| 359 | + |
| 360 | + if _, err := os.Stat(location); err == nil { |
| 361 | + return location |
| 362 | + } |
| 363 | + } |
| 364 | + |
| 365 | + return "" // No config file found |
| 366 | +} |
0 commit comments