├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── backup.go ├── root.go ├── search.go ├── unzip.go ├── version.go └── zip.go ├── go.mod ├── go.sum ├── install.sh ├── main.go └── pkg ├── auth.go ├── backup.go ├── search.go ├── version ├── service.go ├── storage.go ├── types.go └── utils.go └── zipper.go /.gitignore: -------------------------------------------------------------------------------- 1 | go.sum 2 | godex 3 | credentials.json 4 | token.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Aditya Singh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # godex 2 | 3 | godex is a powerful command-line file manager that simplifies file operations with features for searching, compression, cloud backup integration, and file versioning. 4 | 5 | ## Features 6 | 7 | - **File Search**: Fast and flexible file search functionality 8 | - **Compression Tools**: Zip and unzip files with ease 9 | - **Google Drive Backup**: Seamless cloud backup integration 10 | - **File Versioning**: Create, list, compare, restore and remove file versions 11 | - **Shell Completion**: Built-in shell completion script generation 12 | 13 | ## Quick Install 14 | 15 | For a quick installation on Linux or macOS systems, you can use our install script: 16 | 17 | ```sh 18 | # Download the install script 19 | curl -O https://raw.githubusercontent.com/inodinwetrust10/godex/main/install.sh 20 | 21 | # Make it executable 22 | chmod +x install.sh 23 | 24 | # Run the installer 25 | ./install.sh 26 | ``` 27 | 28 | The installer will: 29 | 30 | - Automatically detect your system architecture and OS 31 | - Download the latest release from GitHub 32 | - Install the binary to /usr/local/bin 33 | - Set up shell completion for bash, zsh, or fish 34 | - Create necessary config directories 35 | 36 | The installation script features a user-friendly interface with progress tracking and colored output. If an existing installation is detected, the script will ask for confirmation before replacing it. 37 | 38 | After installation, you can run `godex --help` to verify the installation was successful. 39 | 40 | ## Build from Source 41 | 42 | ### **1️⃣ Install Go (1.21 or later)** 43 | 44 | #### **Linux (Debian/Ubuntu)** 45 | 46 | ```sh 47 | sudo apt update 48 | sudo apt install -y golang 49 | ``` 50 | 51 | #### **Linux (Arch Linux)** 52 | 53 | ```sh 54 | sudo pacman -S go 55 | ``` 56 | 57 | #### **macOS** 58 | 59 | ```sh 60 | brew install go 61 | ``` 62 | 63 | Verify the installation: 64 | 65 | ```sh 66 | go version 67 | ``` 68 | 69 | ### **2️⃣ Clone the Repository** 70 | 71 | ```sh 72 | git clone https://github.com/inodinwetrust10/godex 73 | cd godex 74 | ``` 75 | 76 | ### **3️⃣ Build the Binary** 77 | 78 | ```sh 79 | go build -o godex 80 | ``` 81 | 82 | This will generate an executable named `godex` in the same directory. 83 | 84 | To install it system-wide, move it to `/usr/local/bin`: 85 | 86 | ```sh 87 | sudo mv godex /usr/local/bin/ 88 | ``` 89 | 90 | Now you can run: 91 | 92 | ```sh 93 | godex --help 94 | ``` 95 | 96 | ### **4️⃣ Cross-Compile for Different Systems** 97 | 98 | If you need to build for multiple platforms: 99 | 100 | ```sh 101 | # Linux (x86_64) 102 | GOOS=linux GOARCH=amd64 go build -o godex-linux 103 | 104 | # macOS (x86_64) 105 | GOOS=darwin GOARCH=amd64 go build -o godex-macos 106 | 107 | # macOS (Apple Silicon - M1/M2) 108 | GOOS=darwin GOARCH=arm64 go build -o godex-macos-arm 109 | ``` 110 | 111 | ### **5️⃣ Install Dependencies (If Any)** 112 | 113 | If your project has missing dependencies, run: 114 | 115 | ```sh 116 | go mod tidy 117 | ``` 118 | 119 | To fetch dependencies: 120 | 121 | ```sh 122 | go get -u ./... 123 | ``` 124 | 125 | ### **6️⃣ Running godex** 126 | 127 | Once built, run: 128 | 129 | ```sh 130 | ./godex 131 | ``` 132 | 133 | Or if installed system-wide: 134 | 135 | ```sh 136 | godex 137 | ``` 138 | 139 | ## Usage 140 | 141 | ### Command Structure 142 | 143 | ```bash 144 | godex [flags] 145 | godex [command] 146 | ``` 147 | 148 | ### Available Commands 149 | 150 | - `search`: Search files with various criteria 151 | - `zip`: Zip one or more files into a .zip archive 152 | - `unzip`: Unzip a .zip archive to a destination directory 153 | - `backup`: Backup file to Google Drive 154 | - `version`: File versioning operations 155 | - `completion`: Generate the autocompletion script for the specified shell 156 | - `help`: Help about any command 157 | 158 | ### Global Flags 159 | 160 | ```bash 161 | -h, --help Help for godex 162 | -t, --toggle Help message for toggle 163 | -v, --verbose Enable verbose output 164 | ``` 165 | 166 | ### Search Command 167 | 168 | Search for files in the specified root directory using various criteria including exact name match, file size range, and modification date range. 169 | 170 | ```bash 171 | godex search [flags] 172 | ``` 173 | 174 | #### Search Flags 175 | 176 | ```bash 177 | -h, --help Help for search 178 | -M, --max-size int Maximum file size in bytes 179 | -m, --min-size int Minimum file size in bytes 180 | -a, --modified-after string Find files modified after this date (YYYY-MM-DD) 181 | -b, --modified-before string Find files modified before this date (YYYY-MM-DD) 182 | -n, --name string Search by exact file name 183 | -p, --path string Root path for the search (default is current directory) 184 | ``` 185 | 186 | #### Search Examples 187 | 188 | Search by exact filename: 189 | 190 | ```bash 191 | godex search --name "document.pdf" 192 | ``` 193 | 194 | Search by file size range: 195 | 196 | ```bash 197 | godex search --min-size 1000000 --max-size 5000000 198 | ``` 199 | 200 | Search by modification date: 201 | 202 | ```bash 203 | godex search --modified-after "2024-01-01" --modified-before "2024-01-31" 204 | ``` 205 | 206 | Combined search: 207 | 208 | ```bash 209 | godex search --path "/documents" --name "report.pdf" --modified-after "2024-01-01" 210 | ``` 211 | 212 | ### Zip Command 213 | 214 | Zip one or more files into a .zip archive. The command accepts an output zip filename followed by one or more input files. 215 | 216 | ```bash 217 | godex zip [output.zip] [files...] 218 | ``` 219 | 220 | #### Zip Flags 221 | 222 | ```bash 223 | -d, --dir Zipping directory 224 | -h, --help Help for zip 225 | ``` 226 | 227 | #### Zip Examples 228 | 229 | Zip a single file: 230 | 231 | ```bash 232 | godex zip archive.zip document.pdf 233 | ``` 234 | 235 | Zip multiple files: 236 | 237 | ```bash 238 | godex zip documents.zip file1.txt file2.pdf file3.docx 239 | ``` 240 | 241 | Zip a directory: 242 | 243 | ```bash 244 | godex zip project-backup.zip -d ./myproject/ 245 | ``` 246 | 247 | ### Unzip Command 248 | 249 | Unzip a .zip archive to a destination directory. The command requires an input zip file and a destination directory path. 250 | 251 | ```bash 252 | godex unzip [input.zip] [destination] 253 | ``` 254 | 255 | #### Unzip Flags 256 | 257 | ```bash 258 | -h, --help Help for unzip 259 | ``` 260 | 261 | #### Unzip Examples 262 | 263 | Unzip to current directory: 264 | 265 | ```bash 266 | godex unzip archive.zip . 267 | ``` 268 | 269 | Unzip to specific directory: 270 | 271 | ```bash 272 | godex unzip documents.zip ./extracted-files 273 | ``` 274 | 275 | Unzip to new directory: 276 | 277 | ```bash 278 | godex unzip project-backup.zip ./project-restored 279 | ``` 280 | 281 | ### Version Command 282 | 283 | The version command provides file versioning capabilities, allowing you to create, list, compare, restore, and remove versions of your files. 284 | 285 | ```bash 286 | godex version [command] 287 | ``` 288 | 289 | #### Available Version Commands 290 | 291 | - `create`: Create a new version of a file 292 | - `list`: List all versions of a file 293 | - `restore`: Restore your file to a specific version 294 | - `diff`: Check differences between two files or between a file and its version 295 | - `remove`: Remove a specific version or all versions of a file 296 | 297 | #### Create Command 298 | 299 | Create a new version of a file with an optional commit message. 300 | 301 | ```bash 302 | godex version create [filepath] 303 | ``` 304 | 305 | ##### Create Flags 306 | 307 | ```bash 308 | -m, --message string Add a commit message (default "commit") 309 | -h, --help Help for create 310 | ``` 311 | 312 | ##### Create Examples 313 | 314 | Create a version with default commit message: 315 | 316 | ```bash 317 | godex version create document.txt 318 | ``` 319 | 320 | Create a version with custom commit message: 321 | 322 | ```bash 323 | godex version create document.txt -m "Added section 3" 324 | ``` 325 | 326 | #### List Command 327 | 328 | List all versions of a file with their version IDs and commit messages. 329 | 330 | ```bash 331 | godex version list [filepath] 332 | ``` 333 | 334 | ##### List Flags 335 | 336 | ```bash 337 | -h, --help Help for list 338 | ``` 339 | 340 | ##### List Examples 341 | 342 | ```bash 343 | godex version list document.txt 344 | ``` 345 | 346 | #### Restore Command 347 | 348 | Restore a file to a specific version using the version ID. 349 | 350 | ```bash 351 | godex version restore [filepath] [versionID] 352 | ``` 353 | 354 | ##### Restore Flags 355 | 356 | ```bash 357 | -h, --help Help for restore 358 | ``` 359 | 360 | ##### Restore Examples 361 | 362 | ```bash 363 | godex version restore document.txt v2 364 | ``` 365 | 366 | #### Diff Command 367 | 368 | Check differences between two files or between a file and its last version. 369 | 370 | ```bash 371 | godex version diff [filepath1] [filepath2] 372 | ``` 373 | 374 | ##### Diff Flags 375 | 376 | ```bash 377 | -d, --default Compare with the last version 378 | -h, --help Help for diff 379 | ``` 380 | 381 | ##### Diff Examples 382 | 383 | Compare two specific files: 384 | 385 | ```bash 386 | godex version diff document-v1.txt document-v2.txt 387 | ``` 388 | 389 | Compare a file with its last version: 390 | 391 | ```bash 392 | godex version diff document.txt -d 393 | ``` 394 | 395 | #### Remove Command 396 | 397 | Remove a specific version or all versions of a file. 398 | 399 | ```bash 400 | godex version remove [filepath] 401 | ``` 402 | 403 | ##### Remove Flags 404 | 405 | ```bash 406 | -v, --version string Remove a specific version 407 | -h, --help Help for remove 408 | ``` 409 | 410 | ##### Remove Examples 411 | 412 | Remove a specific version: 413 | 414 | ```bash 415 | godex version remove document.txt -v v2 416 | ``` 417 | 418 | Remove all versions: 419 | 420 | ```bash 421 | godex version remove document.txt 422 | ``` 423 | 424 | ### Backup Command 425 | 426 | Backup a file to Google Drive. The command requires a file path to backup. 427 | 428 | ```bash 429 | godex backup [file] 430 | ``` 431 | 432 | #### Backup Flags 433 | 434 | ```bash 435 | -h, --help Help for backup 436 | ``` 437 | 438 | #### Backup Examples 439 | 440 | Backup a single file: 441 | 442 | ```bash 443 | godex backup important-document.pdf 444 | ``` 445 | 446 | #### Google Drive Setup 447 | 448 | Before using the backup command: 449 | 450 | 1. Set up Google Cloud Project: 451 | 452 | - Create a project in Google Cloud Console 453 | - Enable Google Drive API 454 | - Create credentials (OAuth 2.0 Client ID (Desktop app)) 455 | - Download the client configuration file and rename it credentials.json and place it in ~/.config/godex 456 | 457 | 2. First-time configuration: 458 | - Run any backup command 459 | - Follow the authentication flow in your browser 460 | - Grant necessary permissions to godex 461 | - It will show cannot connect to the browser 462 | - Copy the the url -- http://localhost/?state=state-token&code=4/0IudJceGNktoKZlk-0K-\_X_aCsib7868786pJzH71tR-mjyYEJy\_\_MFw&scope=https://www.googleapis.com/auth/drive.file 463 | - Copy the code in between &code=xxxxxxxxxxxx&scope the xxxx will be your code 464 | - Paste it in the terminal 465 | 466 | ## Configuration 467 | 468 | 1. For Google Drive integration: 469 | - Create a Google Cloud project 470 | - Enable Google Drive API 471 | - Download credentials file 472 | - Configure your Google Drive settings 473 | 474 | ## Dependencies 475 | 476 | - Go 1.16+ (for building from source) 477 | - Git (for building from source) 478 | 479 | ## Autocompletion 480 | 481 | The installation script automatically sets up shell completion for bash, zsh, and fish. If you need to manually set it up, you can use the following instructions. 482 | 483 | ### Usage 484 | 485 | ```sh 486 | godex completion [command] 487 | ``` 488 | 489 | ### Available Commands 490 | 491 | - **bash** Generate the autocompletion script for Bash 492 | - **fish** Generate the autocompletion script for Fish 493 | - **powershell** Generate the autocompletion script for PowerShell 494 | - **zsh** Generate the autocompletion script for Zsh 495 | 496 | ### Flags 497 | 498 | ``` 499 | -h, --help help for completion 500 | ``` 501 | 502 | ### Manual Installation 503 | 504 | To enable autocompletion for your shell, run the appropriate command below: 505 | 506 | #### Bash 507 | 508 | ```sh 509 | echo 'source <(godex completion bash)' >> ~/.bashrc 510 | source ~/.bashrc 511 | ``` 512 | 513 | #### Zsh 514 | 515 | ```sh 516 | echo 'source <(godex completion zsh)' >> ~/.zshrc 517 | source ~/.zshrc 518 | ``` 519 | 520 | #### Fish 521 | 522 | ```sh 523 | godex completion fish | source 524 | ``` 525 | 526 | To make it persistent: 527 | 528 | ```sh 529 | godex completion fish > ~/.config/fish/completions/godex.fish 530 | ``` 531 | 532 | ## Contributing 533 | 534 | 1. Fork the repository 535 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 536 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 537 | 4. Push to the branch (`git push origin feature/amazing-feature`) 538 | 5. Open a Pull Request 539 | 540 | ## Support 541 | 542 | If you encounter any issues or have questions: 543 | 544 | - Open an issue in the GitHub repository 545 | - Contact: [adi4gbsingh@gmail.com] 546 | 547 | ## Acknowledgments 548 | 549 | - Google Drive API team 550 | - Go community 551 | - All contributors 552 | 553 | --- 554 | -------------------------------------------------------------------------------- /cmd/backup.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.inodinwetrust10/godex/pkg" 10 | ) 11 | 12 | var backupCmd = &cobra.Command{ 13 | Use: "backup [file]", 14 | Short: "Backup file to Google Drive", 15 | Args: cobra.ExactArgs(1), 16 | Run: func(cmd *cobra.Command, args []string) { 17 | err := pkg.UploadFile(args[0]) 18 | if err != nil { 19 | log.Fatalf("Backup failed: %v", err) 20 | } 21 | fmt.Println("Backup successful!") 22 | }, 23 | } 24 | 25 | func init() { 26 | rootCmd.AddCommand(backupCmd) 27 | } 28 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 NAME HERE 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | verbose *bool 15 | // rootCmd represents the base command when called without any subcommands 16 | rootCmd = &cobra.Command{ 17 | Use: "godex", 18 | Short: "A powerful file management CLI tool", 19 | Long: `godex is a file management CLI built in Go, designed for advanced file operations.It supports zipping, file backup ,versioning and much more with a clean and extensible interface.`, 20 | // Uncomment the following line if your bare application 21 | // has an action associated with it: 22 | Run: func(cmd *cobra.Command, args []string) { 23 | if *verbose { 24 | fmt.Println("Verbose mode enabled") 25 | } 26 | fmt.Println("Welcome to godex CLI! Use --help to see available commands.") 27 | }, 28 | } 29 | ) 30 | 31 | // Execute adds all child commands to the root command and sets flags appropriately. 32 | // This is called by main.main(). It only needs to happen once to the rootCmd. 33 | func Execute() { 34 | err := rootCmd.Execute() 35 | if err != nil { 36 | os.Exit(1) 37 | } 38 | } 39 | 40 | func init() { 41 | // Here you will define your flags and configuration settings. 42 | // Cobra supports persistent flags, which, if defined here, 43 | // will be global for your application. 44 | 45 | // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.godex.yaml)") 46 | 47 | // Cobra also supports local flags, which will only run 48 | // when this action is called directly. 49 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 50 | verbose = rootCmd.Flags().BoolP("verbose", "v", false, "Enable verbose output") 51 | } 52 | -------------------------------------------------------------------------------- /cmd/search.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.inodinwetrust10/godex/pkg" 11 | ) 12 | 13 | var ( 14 | rootDir string 15 | name string 16 | minSize int64 17 | maxSize int64 18 | modifiedAfter string 19 | modifiedBefore string 20 | ) 21 | 22 | var searchCmd = &cobra.Command{ 23 | Use: "search", 24 | Short: "Search files with various criteria", 25 | Long: `Search for files in the specified root directory using various criteria: 26 | - exact name match 27 | - file size range 28 | - modification date range`, 29 | Run: func(cmd *cobra.Command, args []string) { 30 | if rootDir == "" { 31 | rootDir = "." 32 | } 33 | 34 | var afterTime, beforeTime time.Time 35 | var err error 36 | 37 | if modifiedAfter != "" { 38 | afterTime, err = time.Parse("2006-01-02", modifiedAfter) 39 | if err != nil { 40 | log.Fatalf( 41 | "Invalid date format for --modified-after: %v. Use YYYY-MM-DD format.", 42 | err, 43 | ) 44 | } 45 | } 46 | 47 | if modifiedBefore != "" { 48 | beforeTime, err = time.Parse("2006-01-02", modifiedBefore) 49 | if err != nil { 50 | log.Fatalf( 51 | "Invalid date format for --modified-before: %v. Use YYYY-MM-DD format.", 52 | err, 53 | ) 54 | } 55 | } 56 | 57 | criteria := pkg.SearchCriteria{ 58 | Name: name, 59 | MinSize: minSize, 60 | MaxSize: maxSize, 61 | After: afterTime, 62 | Before: beforeTime, 63 | } 64 | 65 | // Perform search using the provided filters 66 | results, err := pkg.SearchFiles(rootDir, criteria) 67 | if err != nil { 68 | log.Fatalf("Error searching files: %v", err) 69 | } 70 | 71 | if len(results) == 0 { 72 | fmt.Println("No files found matching the criteria") 73 | return 74 | } 75 | 76 | fmt.Println("Found files:") 77 | for _, result := range results { 78 | fmt.Println(result) 79 | } 80 | }, 81 | } 82 | 83 | func init() { 84 | rootCmd.AddCommand(searchCmd) 85 | 86 | searchCmd.Flags().StringVarP(&rootDir, "path", "p", "", 87 | "Root path for the search (default is current directory)") 88 | 89 | searchCmd.Flags().StringVarP(&name, "name", "n", "", 90 | "Search by exact file name") 91 | 92 | searchCmd.Flags().Int64VarP(&minSize, "min-size", "m", 0, 93 | "Minimum file size in bytes") 94 | searchCmd.Flags().Int64VarP(&maxSize, "max-size", "M", 0, 95 | "Maximum file size in bytes") 96 | 97 | searchCmd.Flags().StringVarP(&modifiedAfter, "modified-after", "a", "", 98 | "Find files modified after this date (YYYY-MM-DD)") 99 | searchCmd.Flags().StringVarP(&modifiedBefore, "modified-before", "b", "", 100 | "Find files modified before this date (YYYY-MM-DD)") 101 | } 102 | -------------------------------------------------------------------------------- /cmd/unzip.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.inodinwetrust10/godex/pkg" 10 | ) 11 | 12 | var unzipCmd = &cobra.Command{ 13 | Use: "unzip [input.zip] [destination]", 14 | Short: "Unzip a .zip archive to a destination directory", 15 | Args: cobra.ExactArgs(2), 16 | Run: func(cmd *cobra.Command, args []string) { 17 | inputFile := args[0] 18 | destination := args[1] 19 | 20 | if err := pkg.UnzipFile(inputFile, destination); err != nil { 21 | fmt.Println("Error:", err) 22 | os.Exit(1) 23 | } 24 | fmt.Println("Unzipped successfully to:", destination) 25 | }, 26 | } 27 | 28 | func init() { 29 | rootCmd.AddCommand(unzipCmd) 30 | } 31 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.inodinwetrust10/godex/pkg/version" 10 | ) 11 | 12 | var ( 13 | message string 14 | versionCmd = &cobra.Command{ 15 | Use: "version", 16 | Short: "File versioning operations", 17 | Long: `Create,list,check diffs,restore and remove versions of files`, 18 | } 19 | ) 20 | 21 | var createCmd = &cobra.Command{ 22 | Use: "create [filepath]", 23 | Short: "Create a new version of a file", 24 | Args: cobra.ExactArgs(1), 25 | RunE: createVersion, 26 | } 27 | 28 | var listCmd = &cobra.Command{ 29 | Use: "list [filepath]", 30 | Short: "List all versions of a file", 31 | Args: cobra.ExactArgs(1), 32 | RunE: listVersion, 33 | } 34 | 35 | var restoreCmd = &cobra.Command{ 36 | Use: "restore [filepath] [versionID]", 37 | Short: "Restore your file to a specific versionID", 38 | Args: cobra.ExactArgs(2), 39 | RunE: restoreVersion, 40 | } 41 | 42 | var ( 43 | useLastVersion bool 44 | seeDiffCmd = &cobra.Command{ 45 | Use: "diff [filepath1] [filepath2]", 46 | Short: "Check diffs between two files", 47 | Args: cobra.MinimumNArgs(1), 48 | RunE: seeDiff, 49 | } 50 | ) 51 | 52 | var ( 53 | versionToRemove string 54 | removeCmd = &cobra.Command{ 55 | Use: "remove [filepath] [verionFlag]", 56 | Short: "Remove a specific version or all versions of a file", 57 | Args: cobra.ExactArgs(1), 58 | RunE: removeVersion, 59 | } 60 | ) 61 | 62 | func init() { 63 | createCmd.Flags().StringVarP(&message, "message", "m", "commit", "Add a commit message") 64 | seeDiffCmd.Flags(). 65 | BoolVarP(&useLastVersion, "default", "d", false, "Compare with the last version") 66 | removeCmd.Flags().StringVarP(&versionToRemove, "version", "v", "", "Remove a specific version") 67 | versionCmd.AddCommand(removeCmd) 68 | versionCmd.AddCommand(createCmd) 69 | versionCmd.AddCommand(listCmd) 70 | versionCmd.AddCommand(restoreCmd) 71 | versionCmd.AddCommand(seeDiffCmd) 72 | rootCmd.AddCommand(versionCmd) 73 | } 74 | 75 | func createVersion(cmd *cobra.Command, args []string) error { 76 | filePath, err := filepath.Abs(args[0]) 77 | if err != nil { 78 | return err 79 | } 80 | id, err := version.GenerateVersionID(filePath) 81 | if err != nil { 82 | return err 83 | } 84 | meta, err := version.CreateFile(filePath, id, message) 85 | if err != nil { 86 | return err 87 | } 88 | fmt.Printf("A version was successfully created with version ID: %s\n", meta.ID) 89 | return nil 90 | } 91 | 92 | // /////////////////////////////////////////////////////////////////// 93 | // /////////////////////////////////////////////////////////////////// 94 | func listVersion(cmd *cobra.Command, args []string) error { 95 | filePath, err := filepath.Abs((args[0])) 96 | if err != nil { 97 | return err 98 | } 99 | filePath, err = version.GetVersionPath(filePath) 100 | if err != nil { 101 | return err 102 | } 103 | list, err := version.ListAllVersions(filePath) 104 | if err != nil { 105 | return err 106 | } 107 | for _, data := range *list { 108 | fmt.Printf("ID: %s\n", data.ID) 109 | fmt.Printf("Message: %s\n", data.Message) 110 | fmt.Printf("Created At: %s\n", data.CreatedAt) 111 | fmt.Printf("Size(in Bytes): %d\n", data.Size) 112 | } 113 | return nil 114 | } 115 | 116 | // ///////////////////////////////////////////////////////////////////// 117 | // ///////////////////////////////////////////////////////////////////// 118 | func restoreVersion(cmd *cobra.Command, args []string) error { 119 | filePath, err := filepath.Abs(args[0]) 120 | if err != nil { 121 | return err 122 | } 123 | versionDir, err := version.GetVersionPath(filePath) 124 | if err != nil { 125 | return err 126 | } 127 | err = version.RestoreFile(versionDir, args[1], filePath) 128 | if err != nil { 129 | return err 130 | } 131 | fmt.Printf("File restored to version %s successfully", args[1]) 132 | return nil 133 | } 134 | 135 | //////////////////////////////////////////////////////////////////////// 136 | //////////////////////////////////////////////////////////////////////// 137 | 138 | func seeDiff(cmd *cobra.Command, args []string) error { 139 | var diffRes version.DiffResult 140 | if useLastVersion && len(args) == 1 { 141 | filePath, err := filepath.Abs(args[0]) 142 | if err != nil { 143 | return err 144 | } 145 | fileDir, err := version.GetVersionPath(filePath) 146 | lastVersionPath := version.ReturnLastSecondFilePath(fileDir) 147 | if lastVersionPath == "No file found" { 148 | return fmt.Errorf("No last version found. Make a version first to check") 149 | } else if lastVersionPath == "No previous version found to check" { 150 | return fmt.Errorf("No previous version found to check") 151 | } 152 | 153 | diffRes, err = version.FileDiff(filePath, lastVersionPath) 154 | if err != nil { 155 | return err 156 | } 157 | } else if len(args) == 2 { 158 | filePath1, err := filepath.Abs(args[0]) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | filePath2, err := filepath.Abs(args[1]) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | diffRes, err = version.FileDiff(filePath1, filePath2) 169 | if err != nil { 170 | return err 171 | } 172 | } else { 173 | return fmt.Errorf("incorrect number of arguments: provide either one file with -d flag or two files to compare") 174 | } 175 | version.PrintDiffResults(&diffRes) 176 | return nil 177 | } 178 | 179 | // ////////////////////////////////////////////////////////////////////////////////////////// 180 | // ////////////////////////////////////////////////////////////////////////////////////////// 181 | func removeVersion(cmd *cobra.Command, args []string) error { 182 | filePath, err := filepath.Abs(args[0]) 183 | if err != nil { 184 | return err 185 | } 186 | fileDir, err := version.GetVersionPath(filePath) 187 | if err != nil { 188 | return err 189 | } 190 | if cmd.Flags().Changed("version") { 191 | err := version.ClearVersion(fileDir, versionToRemove) 192 | if err != nil { 193 | return err 194 | } 195 | 196 | fmt.Printf("Version with verisonID %s is cleared", versionToRemove) 197 | } else { 198 | err := version.ClearAllVersion(fileDir) 199 | if err != nil { 200 | return err 201 | } 202 | fmt.Println("All versions of this file are now cleared") 203 | } 204 | return nil 205 | } 206 | -------------------------------------------------------------------------------- /cmd/zip.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.inodinwetrust10/godex/pkg" 10 | ) 11 | 12 | var zipCmd = &cobra.Command{ 13 | Use: "zip [output.zip] [files...]", 14 | Short: "Zip files or directories into a .zip archive", 15 | Args: cobra.MinimumNArgs(2), 16 | Run: func(cmd *cobra.Command, args []string) { 17 | dirFlag, err := cmd.Flags().GetBool("dir") 18 | if err != nil { 19 | fmt.Println("Error:", err) 20 | os.Exit(1) 21 | } 22 | 23 | if dirFlag { 24 | if len(args) != 2 { 25 | fmt.Println("Error: when using -d, specify output file and directory") 26 | os.Exit(1) 27 | } 28 | outputFile := args[0] 29 | directory := args[1] 30 | 31 | fileInfo, err := os.Stat(directory) 32 | if err != nil { 33 | fmt.Println("Error:", err) 34 | os.Exit(1) 35 | } 36 | if !fileInfo.IsDir() { 37 | fmt.Println("Error:", directory, "is not a directory") 38 | os.Exit(1) 39 | } 40 | 41 | if err := pkg.ZipDirectory(outputFile, directory); err != nil { 42 | fmt.Println("Error:", err) 43 | os.Exit(1) 44 | } 45 | } else { 46 | if len(args) < 2 { 47 | fmt.Println("Error: need output file and at least one file to zip") 48 | os.Exit(1) 49 | } 50 | outputFile := args[0] 51 | files := args[1:] 52 | 53 | for _, file := range files { 54 | fileInfo, err := os.Stat(file) 55 | if err != nil { 56 | fmt.Println("Error:", err) 57 | os.Exit(1) 58 | } 59 | if fileInfo.IsDir() { 60 | fmt.Println("Error:", file, "is a directory (use -d flag for directories)") 61 | os.Exit(1) 62 | } 63 | } 64 | 65 | if err := pkg.ZipFiles(outputFile, files); err != nil { 66 | fmt.Println("Error:", err) 67 | os.Exit(1) 68 | } 69 | } 70 | fmt.Println("Zipped successfully:", args[0]) 71 | }, 72 | } 73 | 74 | func init() { 75 | zipCmd.Flags().BoolP("dir", "d", false, "Zip a directory instead of individual files") 76 | rootCmd.AddCommand(zipCmd) 77 | } 78 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.inodinwetrust10/godex 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/spf13/cobra v1.8.1 7 | golang.org/x/oauth2 v0.25.0 8 | google.golang.org/api v0.218.0 9 | ) 10 | 11 | require ( 12 | cloud.google.com/go/auth v0.14.0 // indirect 13 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect 14 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 15 | github.com/felixge/httpsnoop v1.0.4 // indirect 16 | github.com/go-logr/logr v1.4.2 // indirect 17 | github.com/go-logr/stdr v1.2.2 // indirect 18 | github.com/google/s2a-go v0.1.9 // indirect 19 | github.com/google/uuid v1.6.0 // indirect 20 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 21 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 22 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 23 | github.com/sergi/go-diff v1.3.1 // indirect 24 | github.com/spf13/pflag v1.0.5 // indirect 25 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 26 | go.opentelemetry.io/otel v1.31.0 // indirect 27 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 28 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 29 | golang.org/x/crypto v0.32.0 // indirect 30 | golang.org/x/net v0.34.0 // indirect 31 | golang.org/x/sys v0.29.0 // indirect 32 | golang.org/x/text v0.21.0 // indirect 33 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 34 | google.golang.org/grpc v1.69.4 // indirect 35 | google.golang.org/protobuf v1.36.3 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM= 2 | cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A= 3 | cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= 4 | cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= 5 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 6 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 12 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 13 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 14 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 15 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 16 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 17 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 18 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 19 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 20 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 21 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 23 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 24 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 25 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 26 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= 27 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 28 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 29 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 30 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 31 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 32 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 33 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 34 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 38 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 39 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 40 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 41 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 42 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 43 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 46 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 47 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 48 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 49 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 50 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= 51 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= 52 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= 53 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= 54 | go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= 55 | go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= 56 | go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= 57 | go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= 58 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= 59 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 60 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 61 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 62 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 63 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 64 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 65 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 66 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 67 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 68 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 69 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 70 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 71 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 72 | google.golang.org/api v0.218.0 h1:x6JCjEWeZ9PFCRe9z0FBrNwj7pB7DOAqT35N+IPnAUA= 73 | google.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M= 74 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= 75 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= 76 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= 77 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 78 | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= 79 | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 80 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= 81 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 82 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 85 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 86 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 87 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | GREEN='\033[0;32m' 5 | BLUE='\033[0;34m' 6 | YELLOW='\033[1;33m' 7 | RED='\033[0;31m' 8 | NC='\033[0m' # No Color 9 | 10 | print_header() { 11 | echo -e "${BLUE}┌────────────────────────────────────────┐${NC}" 12 | echo -e "${BLUE}│ ${GREEN}godex Installer ${VERSION}${BLUE} │${NC}" 13 | echo -e "${BLUE}└────────────────────────────────────────┘${NC}" 14 | echo 15 | } 16 | 17 | print_step() { 18 | echo -e "${YELLOW}[${1}/${2}]${NC} ${3}" 19 | } 20 | 21 | print_success() { 22 | echo -e "${GREEN}✓${NC} $1" 23 | } 24 | 25 | print_error() { 26 | echo -e "${RED}✗${NC} $1" 27 | exit 1 28 | } 29 | 30 | print_step 1 6 "Detecting system..." 31 | OS=$(uname -s | tr '[:upper:]' '[:lower:]') 32 | ARCH=$(uname -m) 33 | if [[ "$ARCH" == "x86_64" ]]; then 34 | ARCH="amd64" 35 | elif [[ "$ARCH" == "arm64" || "$ARCH" == "aarch64" ]]; then 36 | ARCH="arm64" 37 | else 38 | print_error "Unsupported architecture: $ARCH" 39 | fi 40 | 41 | print_success "Detected $OS/$ARCH" 42 | 43 | # Get latest version from GitHub releases 44 | print_step 2 6 "Checking for latest version..." 45 | VERSION=$(curl -s https://api.github.com/repos/inodinwetrust10/godex/releases/latest \ 46 | | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 47 | VERSION=${VERSION#v} # Remove 'v' prefix if present 48 | 49 | if [ -z "$VERSION" ]; then 50 | print_error "Failed to retrieve version information" 51 | fi 52 | 53 | print_success "Latest version: v${VERSION}" 54 | 55 | # Download the appropriate binary archive 56 | print_step 3 6 "Downloading godex..." 57 | BINARY="godex_${OS}_${ARCH}" 58 | URL="https://github.com/inodinwetrust10/godex/releases/download/v${VERSION}/${BINARY}.tar.gz" 59 | echo -e "From: ${BLUE}$URL${NC}" 60 | 61 | TEMP_FILE=$(mktemp) 62 | curl -L "$URL" -o "$TEMP_FILE" --progress-bar 63 | 64 | if ! file "$TEMP_FILE" | grep -q 'gzip compressed data'; then 65 | print_error "Downloaded file is not a valid gzip archive. Please check the URL and asset name." 66 | fi 67 | 68 | print_success "Download complete" 69 | 70 | # Extract the binary 71 | print_step 4 6 "Extracting files..." 72 | tar xzf "$TEMP_FILE" 73 | rm "$TEMP_FILE" 74 | 75 | print_success "Extraction complete" 76 | 77 | print_step 5 6 "Installing godex..." 78 | if [ -f /usr/local/bin/godex ]; then 79 | echo -e "${YELLOW}A previous installation of godex was found.${NC}" 80 | read -p "Do you want to update it to v${VERSION}? [y/N] " answer 81 | if [[ "$answer" =~ ^[Yy]$ ]]; then 82 | echo "Removing previous installation..." 83 | sudo rm -f /usr/local/bin/godex 84 | else 85 | echo "Installation aborted." 86 | exit 0 87 | fi 88 | fi 89 | 90 | # Install the new version 91 | sudo mv "${BINARY}" /usr/local/bin/godex 92 | sudo chmod +x /usr/local/bin/godex 93 | 94 | print_success "godex v${VERSION} has been installed to /usr/local/bin/godex" 95 | 96 | # Setup shell completion 97 | print_step 6 6 "Setting up shell completion..." 98 | 99 | # Create config directory if it doesn't exist 100 | mkdir -p ~/.config/godex 101 | 102 | # Detect shell 103 | SHELL_TYPE=$(basename "$SHELL") 104 | case "$SHELL_TYPE" in 105 | bash) 106 | /usr/local/bin/godex completion bash > ~/.config/godex/godex.bash 107 | 108 | # Check if completion is already in .bashrc 109 | if ! grep -q "godex completion" ~/.bashrc; then 110 | echo "source ~/.config/godex/godex.bash" >> ~/.bashrc 111 | print_success "Bash completion installed. It will be activated in new shell sessions." 112 | echo -e "${YELLOW}Run 'source ~/.bashrc' to enable completion in the current session.${NC}" 113 | else 114 | print_success "Bash completion already configured in ~/.bashrc" 115 | fi 116 | ;; 117 | zsh) 118 | /usr/local/bin/godex completion zsh > ~/.config/godex/godex.zsh 119 | 120 | # Check if completion is already in .zshrc 121 | if ! grep -q "godex completion" ~/.zshrc; then 122 | echo "source ~/.config/godex/godex.zsh" >> ~/.zshrc 123 | print_success "Zsh completion installed. It will be activated in new shell sessions." 124 | echo -e "${YELLOW}Run 'source ~/.zshrc' to enable completion in the current session.${NC}" 125 | else 126 | print_success "Zsh completion already configured in ~/.zshrc" 127 | fi 128 | ;; 129 | fish) 130 | # Create fish completions directory if it doesn't exist 131 | mkdir -p ~/.config/fish/completions 132 | /usr/local/bin/godex completion fish > ~/.config/fish/completions/godex.fish 133 | print_success "Fish completion installed." 134 | ;; 135 | *) 136 | echo -e "${YELLOW}Shell completion for $SHELL_TYPE not set up automatically.${NC}" 137 | echo -e "You can manually set it up with: ${BLUE}godex completion --help${NC}" 138 | ;; 139 | esac 140 | 141 | # Installation complete 142 | echo 143 | echo -e "${GREEN}┌────────────────────────────────────────┐${NC}" 144 | echo -e "${GREEN}│ Installation Complete! 🎉 │${NC}" 145 | echo -e "${GREEN}└────────────────────────────────────────┘${NC}" 146 | echo 147 | echo -e "Run ${BLUE}godex --help${NC} to get started." 148 | echo 149 | echo -e "${YELLOW}Google Drive Integration:${NC}" 150 | echo -e "For Google Drive backup functionality, you need to:" 151 | echo -e "1. Create a Google Cloud project and enable Google Drive API" 152 | echo -e "2. Create OAuth 2.0 credentials (Desktop app)" 153 | echo -e "3. Download credentials.json and place it in ${BLUE}~/.config/godex/${NC}" 154 | echo 155 | echo -e "Thank you for installing godex! 🚀" 156 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 NAME HERE 3 | 4 | */ 5 | package main 6 | 7 | import "github.inodinwetrust10/godex/cmd" 8 | 9 | func main() { 10 | cmd.Execute() 11 | } 12 | -------------------------------------------------------------------------------- /pkg/auth.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | const ( 16 | configDir = ".config/godex" 17 | tokenFileName = "token.json" 18 | ) 19 | 20 | func GetConfigDir() string { 21 | home, err := os.UserHomeDir() 22 | if err != nil { 23 | log.Fatalf("Unable to find home directory: %v", err) 24 | } 25 | return filepath.Join(home, configDir) 26 | } 27 | 28 | func getTokenFilePath() string { 29 | return filepath.Join(GetConfigDir(), tokenFileName) 30 | } 31 | 32 | func getClient(config *oauth2.Config) *http.Client { 33 | tokenPath := getTokenFilePath() 34 | 35 | tok, err := tokenFromFile(tokenPath) 36 | if err != nil { 37 | tok = getTokenFromWeb(config) 38 | saveToken(tokenPath, tok) 39 | } 40 | return config.Client(context.Background(), tok) 41 | } 42 | 43 | func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { 44 | authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) 45 | 46 | fmt.Println("Go to the following link in your browser:") 47 | fmt.Println(authURL) 48 | fmt.Println("\nAfter approving the permissions, you'll get an authorization code.") 49 | fmt.Println("Paste that code here:") 50 | 51 | var authCode string 52 | if _, err := fmt.Scan(&authCode); err != nil { 53 | log.Fatalf("Unable to read authorization code: %v", err) 54 | } 55 | 56 | tok, err := config.Exchange(context.TODO(), authCode) 57 | if err != nil { 58 | log.Fatalf("Unable to retrieve token: %v", err) 59 | } 60 | return tok 61 | } 62 | 63 | func tokenFromFile(file string) (*oauth2.Token, error) { 64 | f, err := os.Open(file) 65 | if err != nil { 66 | return nil, err 67 | } 68 | defer f.Close() 69 | tok := &oauth2.Token{} 70 | err = json.NewDecoder(f).Decode(tok) 71 | return tok, err 72 | } 73 | 74 | func saveToken(path string, token *oauth2.Token) { 75 | dir := filepath.Dir(path) 76 | if err := os.MkdirAll(dir, 0700); err != nil { 77 | log.Fatalf("Unable to create config directory: %v", err) 78 | } 79 | 80 | f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 81 | if err != nil { 82 | log.Fatalf("Unable to cache token: %v", err) 83 | } 84 | defer f.Close() 85 | json.NewEncoder(f).Encode(token) 86 | } 87 | -------------------------------------------------------------------------------- /pkg/backup.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "golang.org/x/oauth2/google" 12 | "google.golang.org/api/drive/v3" 13 | "google.golang.org/api/option" 14 | ) 15 | 16 | type ProgressReader struct { 17 | io.Reader 18 | Total int64 19 | ReadBytes int64 20 | } 21 | 22 | func (pr *ProgressReader) Read(p []byte) (int, error) { 23 | n, err := pr.Reader.Read(p) 24 | pr.ReadBytes += int64(n) 25 | return n, err 26 | } 27 | 28 | func formatBytes(b int64) string { 29 | const unit = 1024 30 | if b < unit { 31 | return fmt.Sprintf("%d B", b) 32 | } 33 | div, exp := int64(unit), 0 34 | for n := b / unit; n >= unit; n /= unit { 35 | div *= unit 36 | exp++ 37 | } 38 | return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) 39 | } 40 | 41 | func UploadFile(filePath string) error { 42 | ctx := context.Background() 43 | configPath := filepath.Join(GetConfigDir(), "credentials.json") 44 | 45 | b, err := os.ReadFile(configPath) 46 | if err != nil { 47 | return fmt.Errorf("error reading credentials.json: %v", err) 48 | } 49 | 50 | config, err := google.ConfigFromJSON(b, drive.DriveFileScope) 51 | if err != nil { 52 | return fmt.Errorf("error parsing credentials.json: %v", err) 53 | } 54 | 55 | client := getClient(config) 56 | srv, err := drive.NewService(ctx, option.WithHTTPClient(client)) 57 | if err != nil { 58 | return fmt.Errorf("error creating Drive service: %v", err) 59 | } 60 | 61 | file, err := os.Open(filePath) 62 | if err != nil { 63 | return fmt.Errorf("error opening file: %v", err) 64 | } 65 | defer file.Close() 66 | 67 | fileInfo, err := file.Stat() 68 | if err != nil { 69 | return fmt.Errorf("error getting file info: %v", err) 70 | } 71 | 72 | pr := &ProgressReader{ 73 | Reader: file, 74 | Total: fileInfo.Size(), 75 | } 76 | 77 | done := make(chan bool) 78 | go func() { 79 | ticker := time.NewTicker(500 * time.Millisecond) 80 | defer ticker.Stop() 81 | 82 | for { 83 | select { 84 | case <-ticker.C: 85 | percent := float64(pr.ReadBytes) / float64(pr.Total) * 100 86 | fmt.Printf("\rUploading... %s/%s (%.2f%%)", 87 | formatBytes(pr.ReadBytes), 88 | formatBytes(pr.Total), 89 | percent) 90 | case <-done: 91 | fmt.Printf("\rUpload complete! %s uploaded\n", formatBytes(pr.Total)) 92 | return 93 | } 94 | } 95 | }() 96 | 97 | driveFile := &drive.File{Name: fileInfo.Name()} 98 | 99 | _, err = srv.Files.Create(driveFile).Media(pr).Context(ctx).Do() 100 | 101 | done <- true 102 | if err != nil { 103 | return fmt.Errorf("error uploading file: %v", err) 104 | } 105 | 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /pkg/search.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type SearchCriteria struct { 11 | Name string 12 | MinSize int64 13 | MaxSize int64 14 | After time.Time 15 | Before time.Time 16 | } 17 | 18 | func SearchFiles(root string, criteria SearchCriteria) ([]string, error) { 19 | resultChan := make(chan string) 20 | errChan := make(chan error, 1) 21 | var wg sync.WaitGroup 22 | var results []string 23 | 24 | wg.Add(1) 25 | go func() { 26 | defer wg.Done() 27 | err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 28 | if err != nil { 29 | return err 30 | } 31 | 32 | if info.IsDir() { 33 | files, err := os.ReadDir(path) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | for _, file := range files { 39 | if file.IsDir() { 40 | continue 41 | } 42 | 43 | // Get detailed file info for size and modification time 44 | fileInfo, err := file.Info() 45 | if err != nil { 46 | continue 47 | } 48 | 49 | // Check if file matches all specified criteria 50 | matches := true 51 | 52 | // Name check 53 | if criteria.Name != "" && file.Name() != criteria.Name { 54 | matches = false 55 | } 56 | 57 | // Size check 58 | if criteria.MinSize > 0 && fileInfo.Size() < criteria.MinSize { 59 | matches = false 60 | } 61 | if criteria.MaxSize > 0 && fileInfo.Size() > criteria.MaxSize { 62 | matches = false 63 | } 64 | 65 | // Modification time check 66 | modTime := fileInfo.ModTime() 67 | if !criteria.After.IsZero() && modTime.Before(criteria.After) { 68 | matches = false 69 | } 70 | if !criteria.Before.IsZero() && modTime.After(criteria.Before) { 71 | matches = false 72 | } 73 | 74 | if matches { 75 | select { 76 | case resultChan <- filepath.Join(path, file.Name()): 77 | case <-errChan: 78 | return filepath.SkipAll 79 | } 80 | } 81 | } 82 | } 83 | return nil 84 | }) 85 | if err != nil { 86 | select { 87 | case errChan <- err: 88 | default: 89 | } 90 | } 91 | }() 92 | 93 | go func() { 94 | wg.Wait() 95 | close(resultChan) 96 | close(errChan) 97 | }() 98 | 99 | var err error 100 | for { 101 | select { 102 | case path, ok := <-resultChan: 103 | if !ok { 104 | return results, err 105 | } 106 | results = append(results, path) 107 | case e, ok := <-errChan: 108 | if ok { 109 | err = e 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/version/service.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "bufio" 5 | "crypto/md5" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | func CreateFile(filePath, versionID, message string) (VersionMetaData, error) { 16 | fileDir, err := GetVersionPath(filePath) 17 | if err != nil { 18 | return VersionMetaData{}, err 19 | } 20 | lastFilePath := ReturnLastFilePath(fileDir) 21 | if lastFilePath == "" { 22 | return VersionMetaData{}, err 23 | } 24 | isRequired, err := checkDiffs(filePath, lastFilePath) 25 | if err != nil { 26 | return VersionMetaData{}, err 27 | } 28 | // if a version without change already exists it return an error 29 | if isRequired == false { 30 | return VersionMetaData{}, errors.New("A version already exists") 31 | } 32 | meta, err := saveFile(filePath, versionID, message, fileDir) 33 | return meta, err 34 | } 35 | 36 | // ///////////////////////////////////////////////////////////////////////////////// 37 | // ///////////////////////////////////////////////////////////////////////////////// 38 | func ListAllVersions(fileDir string) (*[]VersionMetaData, error) { 39 | metaDataFilePath := filepath.Join(fileDir, "version.json") 40 | 41 | var listAllVersions []VersionMetaData 42 | 43 | data, err := os.ReadFile(metaDataFilePath) 44 | if err != nil { 45 | if os.IsNotExist(err) { 46 | return nil, fmt.Errorf( 47 | "No version currently exists of this file", 48 | ) 49 | } 50 | return nil, err 51 | } 52 | err = json.Unmarshal(data, &listAllVersions) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return &listAllVersions, nil 57 | } 58 | 59 | // ///////////////////////////////////////////////////////////////////////////////// 60 | // ///////////////////////////////////////////////////////////////////////////////// 61 | 62 | func FileDiff(path1, path2 string) (DiffResult, error) { 63 | result := DiffResult{ 64 | Identical: true, 65 | DiffLines: []LineDiff{}, 66 | } 67 | 68 | info1, err := os.Stat(path1) 69 | if err != nil { 70 | return result, fmt.Errorf("error accessing first file: %w", err) 71 | } 72 | 73 | _, err = os.Stat(path2) 74 | if err != nil { 75 | return result, fmt.Errorf("error accessing second file: %w", err) 76 | } 77 | 78 | if info1.Size() < 10*1024*1024 { 79 | return compareFilesDetailed(path1, path2) 80 | } 81 | 82 | checksum1, err := calculateMD5(path1) 83 | if err != nil { 84 | return result, err 85 | } 86 | 87 | checksum2, err := calculateMD5(path2) 88 | if err != nil { 89 | return result, err 90 | } 91 | 92 | if checksum1 != checksum2 { 93 | result.Identical = false 94 | result.DiffType = "content" 95 | result.Message = "Files have different content (detected by checksum)" 96 | 97 | detailedResult, err := compareFilesDetailed(path1, path2) 98 | if err != nil { 99 | return result, err 100 | } 101 | result.DiffLines = detailedResult.DiffLines 102 | return result, nil 103 | } 104 | 105 | result.Message = "Files are identical" 106 | return result, nil 107 | } 108 | 109 | // calculateMD5 calculates the MD5 checksum of a file 110 | func calculateMD5(filePath string) (string, error) { 111 | file, err := os.Open(filePath) 112 | if err != nil { 113 | return "", err 114 | } 115 | defer file.Close() 116 | 117 | hash := md5.New() 118 | if _, err := io.Copy(hash, file); err != nil { 119 | return "", err 120 | } 121 | 122 | return fmt.Sprintf("%x", hash.Sum(nil)), nil 123 | } 124 | 125 | func compareFilesDetailed(path1, path2 string) (DiffResult, error) { 126 | result := DiffResult{ 127 | Identical: true, 128 | DiffLines: []LineDiff{}, 129 | } 130 | 131 | file1, err := os.Open(path1) 132 | if err != nil { 133 | return result, err 134 | } 135 | defer file1.Close() 136 | 137 | file2, err := os.Open(path2) 138 | if err != nil { 139 | return result, err 140 | } 141 | defer file2.Close() 142 | 143 | scanner1 := bufio.NewScanner(file1) 144 | scanner2 := bufio.NewScanner(file2) 145 | 146 | lineNum := 0 147 | 148 | for { 149 | hasLine1 := scanner1.Scan() 150 | hasLine2 := scanner2.Scan() 151 | lineNum++ 152 | 153 | if !hasLine1 && !hasLine2 { 154 | break 155 | } 156 | 157 | if hasLine1 != hasLine2 { 158 | result.Identical = false 159 | result.DiffType = "line" 160 | 161 | line1 := "" 162 | line2 := "" 163 | 164 | if hasLine1 { 165 | line1 = scanner1.Text() 166 | } 167 | 168 | if hasLine2 { 169 | line2 = scanner2.Text() 170 | } 171 | 172 | result.DiffLines = append(result.DiffLines, LineDiff{ 173 | LineNumber: lineNum, 174 | Line1: line1, 175 | Line2: line2, 176 | }) 177 | 178 | continue 179 | } 180 | 181 | // Both files have lines at this point, so compare them 182 | line1 := scanner1.Text() 183 | line2 := scanner2.Text() 184 | 185 | if line1 != line2 { 186 | result.Identical = false 187 | result.DiffType = "line" 188 | 189 | result.DiffLines = append(result.DiffLines, LineDiff{ 190 | LineNumber: lineNum, 191 | Line1: line1, 192 | Line2: line2, 193 | }) 194 | } 195 | } 196 | 197 | // Check for scanner errors 198 | if err := scanner1.Err(); err != nil { 199 | return result, err 200 | } 201 | if err := scanner2.Err(); err != nil { 202 | return result, err 203 | } 204 | 205 | if !result.Identical { 206 | result.Message = fmt.Sprintf("Found %d different lines", len(result.DiffLines)) 207 | } else { 208 | result.Message = "Files are identical" 209 | } 210 | 211 | return result, nil 212 | } 213 | 214 | func FormatDiffResult(result DiffResult) string { 215 | var sb strings.Builder 216 | 217 | if result.Identical { 218 | sb.WriteString("Files are identical\n") 219 | return sb.String() 220 | } 221 | 222 | sb.WriteString(result.Message + "\n\n") 223 | 224 | if len(result.DiffLines) > 0 { 225 | sb.WriteString("Differences found:\n") 226 | 227 | for _, diff := range result.DiffLines { 228 | sb.WriteString(fmt.Sprintf("Line %d:\n", diff.LineNumber)) 229 | sb.WriteString(fmt.Sprintf(" File 1: %s\n", diff.Line1)) 230 | sb.WriteString(fmt.Sprintf(" File 2: %s\n\n", diff.Line2)) 231 | } 232 | } 233 | 234 | return sb.String() 235 | } 236 | 237 | // //////////////////////////////////////////////////////////////////////////////////////////////// 238 | // //////////////////////////////////////////////////////////////////////////////////////////////// 239 | func ClearAllVersion(dirPath string) error { 240 | entries, err := os.ReadDir(dirPath) 241 | if err != nil { 242 | return fmt.Errorf("error reading directory: %w", err) 243 | } 244 | deletedCount := 0 245 | errorCount := 0 246 | 247 | for _, entry := range entries { 248 | if entry.IsDir() { 249 | fmt.Printf("Skipping subdirectory: %s\n", entry.Name()) 250 | continue 251 | } 252 | filePath := filepath.Join(dirPath, entry.Name()) 253 | 254 | if err := os.Remove(filePath); err != nil { 255 | fmt.Printf("Error deleting %s: %v\n", filePath, err) 256 | errorCount++ 257 | } else { 258 | fmt.Printf("Deleted: %s\n", filePath) 259 | deletedCount++ 260 | } 261 | } 262 | fmt.Printf("\nSummary: Deleted %d files with %d errors\n", deletedCount, errorCount) 263 | return nil 264 | } 265 | 266 | // /////////////////////////////////////////////////////////////////////////////////////////////// 267 | // /////////////////////////////////////////////////////////////////////////////////////////////// 268 | func ClearVersion(dirPath, versionID string) error { 269 | versionPath := filepath.Join(dirPath, versionID) 270 | 271 | if _, err := os.Stat(versionPath); err != nil { 272 | if os.IsNotExist(err) { 273 | return fmt.Errorf("no file exists with versionID %s", versionID) 274 | } 275 | return fmt.Errorf("failed to access file %s: %v", versionID, err) 276 | } 277 | 278 | if err := os.Remove(versionPath); err != nil { 279 | return fmt.Errorf("failed to remove file %s: %v", versionID, err) 280 | } 281 | 282 | allVersions, err := ListAllVersions(dirPath) 283 | if err != nil { 284 | return fmt.Errorf("failed to list versions: %v", err) 285 | } 286 | 287 | if allVersions == nil { 288 | return fmt.Errorf("version list is nil") 289 | } 290 | 291 | i := 0 292 | for _, version := range *allVersions { 293 | if version.ID != versionID { 294 | (*allVersions)[i] = version 295 | i++ 296 | } 297 | } 298 | *allVersions = (*allVersions)[:i] 299 | 300 | jsonPath := filepath.Join(dirPath, "version.json") 301 | jsonData, err := json.MarshalIndent(*allVersions, "", " ") 302 | if err != nil { 303 | return fmt.Errorf("failed to marshal metadata to JSON: %v", err) 304 | } 305 | 306 | if err := os.WriteFile(jsonPath, jsonData, 0644); err != nil { 307 | return fmt.Errorf("failed to write metadata to file %s: %v", jsonPath, err) 308 | } 309 | 310 | return nil 311 | } 312 | -------------------------------------------------------------------------------- /pkg/version/storage.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "os" 11 | "path/filepath" 12 | "time" 13 | ) 14 | 15 | // ////////////////////////////////////////////////////////////////////////////////////////////// 16 | // ////////////////////////////////////////////////////////////////////////////////////////////// 17 | func saveFile( 18 | filePath, 19 | versionID, 20 | message, 21 | versionPathDir string, 22 | ) (VersionMetaData, error) { 23 | versionFilePath := filepath.Join(versionPathDir, versionID) 24 | 25 | sourceFile, err := os.Open(filePath) 26 | if err != nil { 27 | return VersionMetaData{}, fmt.Errorf("failed to open source file") 28 | } 29 | defer sourceFile.Close() 30 | 31 | destinationFile, err := os.Create(versionFilePath) 32 | if err != nil { 33 | return VersionMetaData{}, fmt.Errorf("failed to create version file") 34 | } 35 | defer destinationFile.Close() 36 | 37 | hasher := sha256.New() 38 | 39 | tee := io.TeeReader(sourceFile, hasher) 40 | 41 | size, err := io.Copy(destinationFile, tee) 42 | if err != nil { 43 | return VersionMetaData{}, fmt.Errorf("failed to copy file") 44 | } 45 | 46 | checksum := hex.EncodeToString(hasher.Sum(nil)) 47 | 48 | metadata := VersionMetaData{ 49 | ID: versionID, 50 | CreatedAt: time.Now(), 51 | Message: message, 52 | Size: int64(size), 53 | Checksum: checksum, 54 | } 55 | 56 | if err = saveMetaData(versionPathDir, metadata); err != nil { 57 | return VersionMetaData{}, fmt.Errorf("failed to update meta data") 58 | } 59 | 60 | if err = updateGlobalIndex(versionID, filePath); err != nil { 61 | return VersionMetaData{}, fmt.Errorf("unable to update version index") 62 | } 63 | 64 | return metadata, nil 65 | } 66 | 67 | // ////////////////////////////////////////////////////////////////////////////////////// 68 | // ////////////////////////////////////////////////////////////////////////////////////// 69 | 70 | func saveMetaData(filePath string, metadata VersionMetaData) error { 71 | fullPath := filepath.Join(filePath, "version.json") 72 | var metadataEntries []VersionMetaData 73 | 74 | if _, err := os.Stat(fullPath); err == nil { 75 | fileData, err := os.ReadFile(fullPath) 76 | if err != nil { 77 | return fmt.Errorf("failed to read existing metadata file") 78 | } 79 | 80 | // Unmarshal existing data 81 | if err := json.Unmarshal(fileData, &metadataEntries); err != nil { 82 | var singleMetadata VersionMetaData 83 | if err := json.Unmarshal(fileData, &singleMetadata); err != nil { 84 | return fmt.Errorf("failed to parse existing metadata") 85 | } 86 | metadataEntries = append(metadataEntries, singleMetadata) 87 | } 88 | metadataEntries = append(metadataEntries, metadata) 89 | } else { 90 | // File doesnt exist start with just the new metadata 91 | metadataEntries = []VersionMetaData{metadata} 92 | } 93 | 94 | jsonData, err := json.MarshalIndent(metadataEntries, "", " ") 95 | if err != nil { 96 | return fmt.Errorf("failed to marshal metadata to JSON") 97 | } 98 | 99 | if err := os.WriteFile(fullPath, jsonData, 0644); err != nil { 100 | return fmt.Errorf("failed to write metadata to file %s", fullPath) 101 | } 102 | 103 | return nil 104 | } 105 | 106 | /////////////////////////////////////////////////////////////////////////////////// 107 | /////////////////////////////////////////////////////////////////////////////////// 108 | 109 | func updateGlobalIndex(versionID, filePath string) error { 110 | homeDir, err := os.UserHomeDir() 111 | if err != nil { 112 | return fmt.Errorf("failed to get home directory") 113 | } 114 | 115 | dirPath := filepath.Join(homeDir, ".config", "godex") 116 | if err := os.MkdirAll(dirPath, 0755); err != nil { 117 | return fmt.Errorf("failed to create directory %s", dirPath) 118 | } 119 | 120 | globalIndexPath := filepath.Join(dirPath, "global.json") 121 | indexMap := make(map[string]*GlobalIndex) 122 | 123 | if _, err := os.Stat(globalIndexPath); err == nil { 124 | fileData, err := os.ReadFile(globalIndexPath) 125 | if err != nil { 126 | return fmt.Errorf("failed to read global index file") 127 | } 128 | 129 | var indices []GlobalIndex 130 | if err := json.Unmarshal(fileData, &indices); err != nil { 131 | var singleIndex GlobalIndex 132 | if err := json.Unmarshal(fileData, &singleIndex); err != nil { 133 | return fmt.Errorf("failed to parse global index") 134 | } 135 | indices = []GlobalIndex{singleIndex} 136 | } 137 | 138 | for i := range indices { 139 | indexMap[indices[i].OriginalFilePath] = &indices[i] 140 | } 141 | } 142 | 143 | now := time.Now() 144 | if idx, exists := indexMap[filePath]; exists { 145 | versionExists := false 146 | for _, v := range idx.Versions { 147 | if v == versionID { 148 | versionExists = true 149 | break 150 | } 151 | } 152 | 153 | if !versionExists { 154 | idx.Versions = append(idx.Versions, versionID) 155 | } 156 | idx.LastUpdatedAt = now 157 | } else { 158 | indexMap[filePath] = &GlobalIndex{ 159 | OriginalFilePath: filePath, 160 | Versions: []string{versionID}, 161 | LastUpdatedAt: now, 162 | } 163 | } 164 | 165 | var updatedIndices []GlobalIndex 166 | for _, idx := range indexMap { 167 | updatedIndices = append(updatedIndices, *idx) 168 | } 169 | 170 | jsonData, err := json.MarshalIndent(updatedIndices, "", " ") 171 | if err != nil { 172 | return fmt.Errorf("failed to marshal global index to JSON") 173 | } 174 | 175 | if err := os.WriteFile(globalIndexPath, jsonData, 0644); err != nil { 176 | return fmt.Errorf("failed to write global index to file") 177 | } 178 | 179 | return nil 180 | } 181 | 182 | ///////////////////////////////////////////////////////////////////////////////// 183 | ///////////////////////////////////////////////////////////////////////////////// 184 | 185 | func RestoreFile(filePath, versionID, originalFilePath string) error { 186 | versionFilePath := filepath.Join(filePath, versionID) 187 | 188 | if _, err := os.Stat(versionFilePath); os.IsNotExist(err) { 189 | return fmt.Errorf("version %s does not exist", versionID) 190 | } 191 | allVersionMetaData, err := ListAllVersions(filePath) 192 | if err != nil { 193 | return err 194 | } 195 | var metadata VersionMetaData 196 | for _, fileMetaData := range *allVersionMetaData { 197 | if fileMetaData.ID == versionID { 198 | metadata = fileMetaData 199 | break 200 | } 201 | } 202 | sourceFile, err := os.Open(versionFilePath) 203 | if err != nil { 204 | return fmt.Errorf("failed to open version file: %w", err) 205 | } 206 | defer sourceFile.Close() 207 | 208 | hasher := sha256.New() 209 | sourceReader := io.TeeReader(sourceFile, hasher) 210 | 211 | var buffer bytes.Buffer 212 | _, err = io.Copy(&buffer, sourceReader) 213 | if err != nil { 214 | return fmt.Errorf("failed to read version file: %w", err) 215 | } 216 | 217 | actualChecksum := hex.EncodeToString(hasher.Sum(nil)) 218 | if actualChecksum != metadata.Checksum { 219 | return fmt.Errorf("checksum verification failed: file may be corrupted") 220 | } 221 | 222 | if _, err = sourceFile.Seek(0, 0); err != nil { 223 | return fmt.Errorf("failed to reset file pointer: %w", err) 224 | } 225 | 226 | destinationFile, err := os.Create(originalFilePath) 227 | if err != nil { 228 | return fmt.Errorf("failed to create destination file: %w", err) 229 | } 230 | defer destinationFile.Close() 231 | 232 | _, err = io.Copy(destinationFile, sourceFile) 233 | if err != nil { 234 | return fmt.Errorf("failed to copy file contents: %w", err) 235 | } 236 | 237 | return nil 238 | } 239 | -------------------------------------------------------------------------------- /pkg/version/types.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "time" 4 | 5 | type VersionMetaData struct { 6 | ID string 7 | Message string 8 | Size int64 9 | Checksum string 10 | CreatedAt time.Time 11 | } 12 | 13 | type GlobalIndex struct { 14 | OriginalFilePath string 15 | Versions []string 16 | LastUpdatedAt time.Time 17 | } 18 | 19 | type DiffResult struct { 20 | Identical bool 21 | DiffType string 22 | Message string 23 | DiffLines []LineDiff 24 | } 25 | 26 | type LineDiff struct { 27 | LineNumber int 28 | Line1 string 29 | Line2 string 30 | } 31 | -------------------------------------------------------------------------------- /pkg/version/utils.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | 12 | "github.com/sergi/go-diff/diffmatchpatch" 13 | ) 14 | 15 | // /////////////////////////////////////////////////////// 16 | // /////////////////////////////////////////////////////// 17 | func GetVersionPath(filePath string) (string, error) { 18 | homeDir, err := os.UserHomeDir() 19 | if err != nil { 20 | return "", fmt.Errorf("could not get home directory: %w", err) 21 | } 22 | hash := sha256.Sum256([]byte(filePath)) 23 | hashedName := hex.EncodeToString(hash[:]) 24 | 25 | versionDir := filepath.Join(homeDir, ".config", "godex", "versions") 26 | versionFilePath := filepath.Join(versionDir, hashedName) 27 | 28 | err = os.MkdirAll(versionFilePath, 0755) 29 | if err != nil { 30 | return "", fmt.Errorf("could not create directory: %w", err) 31 | } 32 | 33 | return versionFilePath, nil 34 | } 35 | 36 | ///////////////////////////////////////////////////////////////////// 37 | ///////////////////////////////////////////////////////////////////// 38 | 39 | func GenerateVersionID(filePath string) (string, error) { 40 | dirPath, err := GetVersionPath(filePath) 41 | if err != nil { 42 | return "", err 43 | } 44 | 45 | versionFilePath := filepath.Join(dirPath, "version.json") 46 | 47 | nextVersion := 1 48 | 49 | if _, err := os.Stat(versionFilePath); err == nil { 50 | fileData, err := os.ReadFile(versionFilePath) 51 | if err != nil { 52 | return "", fmt.Errorf("failed to read version file: %w", err) 53 | } 54 | 55 | var versions []VersionMetaData 56 | if err := json.Unmarshal(fileData, &versions); err != nil { 57 | var singleVersion VersionMetaData 58 | if err := json.Unmarshal(fileData, &singleVersion); err != nil { 59 | return "", fmt.Errorf("failed to parse version data: %w", err) 60 | } 61 | versions = []VersionMetaData{singleVersion} 62 | } 63 | 64 | if len(versions) > 0 { 65 | highestVersion := 0 66 | for _, version := range versions { 67 | if len(version.ID) > 1 && version.ID[0] == 'v' { 68 | vNum, err := strconv.Atoi(version.ID[1:]) 69 | if err == nil && vNum > highestVersion { 70 | highestVersion = vNum 71 | } 72 | } 73 | } 74 | nextVersion = highestVersion + 1 75 | } 76 | } 77 | 78 | versionID := fmt.Sprintf("v%d", nextVersion) 79 | return versionID, nil 80 | } 81 | 82 | ///////////////////////////////////////////////////////////////////////////// 83 | ///////////////////////////////////////////////////////////////////////////// 84 | 85 | func checkDiffs(filePath1, filePath2 string) (bool, error) { 86 | if filePath2 == "No file found" { 87 | return true, nil 88 | } 89 | content1, err := os.ReadFile(filepath.Clean(filePath1)) 90 | if err != nil { 91 | return false, fmt.Errorf("Error reading the file1 %w", err) 92 | } 93 | 94 | content2, err := os.ReadFile(filepath.Clean(filePath2)) 95 | if err != nil { 96 | return false, fmt.Errorf("Error reading the file2 %w", err) 97 | } 98 | 99 | dmp := diffmatchpatch.New() 100 | 101 | diffs := dmp.DiffMain(string(content1), string(content2), false) 102 | 103 | return !(len(diffs) == 1 && diffs[0].Type == diffmatchpatch.DiffEqual), nil 104 | } 105 | 106 | /////////////////////////////////////////////////////////////////////////////// 107 | /////////////////////////////////////////////////////////////////////////////// 108 | 109 | func ReturnLastFilePath(jsonDirPath string) string { 110 | filePath := filepath.Join(jsonDirPath, "version.json") 111 | var elements []VersionMetaData 112 | 113 | if _, err := os.Stat(filePath); err != nil { 114 | if os.IsNotExist(err) { 115 | return "No file found" 116 | } 117 | return "" 118 | } 119 | file, err := os.ReadFile(filePath) 120 | if err != nil { 121 | return "" 122 | } 123 | err = json.Unmarshal(file, &elements) 124 | if err != nil { 125 | return "" 126 | } 127 | num := len(elements) 128 | filename := fmt.Sprintf("v%d", num) 129 | returnPath := filepath.Join(jsonDirPath, filename) 130 | return returnPath 131 | } 132 | 133 | // ////////////////////////////////////////////////////////////////////////////// 134 | // ////////////////////////////////////////////////////////////////////////////// 135 | func PrintDiffResults(diffRes *DiffResult) { 136 | if diffRes.Identical { 137 | fmt.Println("Files are identical") 138 | return 139 | } 140 | 141 | fmt.Printf("Diff Type: %s\n", diffRes.DiffType) 142 | 143 | if diffRes.Message != "" { 144 | fmt.Printf("Message: %s\n", diffRes.Message) 145 | } 146 | 147 | if len(diffRes.DiffLines) == 0 { 148 | fmt.Println("No line differences found") 149 | return 150 | } 151 | 152 | fmt.Println("\nDifferences:") 153 | fmt.Println("-------------------------------------------") 154 | 155 | for _, diff := range diffRes.DiffLines { 156 | fmt.Printf("Line %d:\n", diff.LineNumber) 157 | if diff.Line1 == "" { 158 | fmt.Printf("+ %s\n", diff.Line2) 159 | } else if diff.Line2 == "" { 160 | fmt.Printf("- %s\n", diff.Line1) 161 | } else { 162 | fmt.Printf("- %s\n+ %s\n", diff.Line1, diff.Line2) 163 | } 164 | fmt.Println("-------------------------------------------") 165 | } 166 | 167 | fmt.Printf("\nTotal differences: %d\n", len(diffRes.DiffLines)) 168 | } 169 | 170 | //////////////////////////////////////////////////////////// 171 | //////////////////////////////////////////////////////////// 172 | 173 | func ReturnLastSecondFilePath(jsonDirPath string) string { 174 | filePath := filepath.Join(jsonDirPath, "version.json") 175 | var elements []VersionMetaData 176 | if _, err := os.Stat(filePath); err != nil { 177 | if os.IsNotExist(err) { 178 | return "No file found" 179 | } 180 | return "" 181 | } 182 | file, err := os.ReadFile(filePath) 183 | if err != nil { 184 | return "" 185 | } 186 | err = json.Unmarshal(file, &elements) 187 | if err != nil { 188 | return "" 189 | } 190 | if len(elements) < 2 { 191 | return "No previous version found to check" 192 | } 193 | secondLastVersionData := elements[len(elements)-2] 194 | filename := secondLastVersionData.ID 195 | returnPath := filepath.Join(jsonDirPath, filename) 196 | return returnPath 197 | } 198 | -------------------------------------------------------------------------------- /pkg/zipper.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func ZipFiles(outputFile string, files []string) error { 12 | zipFile, err := os.Create(outputFile) 13 | if err != nil { 14 | return fmt.Errorf("failed to create zip file: %w", err) 15 | } 16 | defer zipFile.Close() 17 | 18 | zipWriter := zip.NewWriter(zipFile) 19 | defer zipWriter.Close() 20 | 21 | for _, file := range files { 22 | if err := addFileToZip(zipWriter, file); err != nil { 23 | return fmt.Errorf("failed to add %s to zip: %w", file, err) 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func addFileToZip(zipWriter *zip.Writer, filename string) error { 31 | fmt.Printf("Adding file: %s\n", filename) 32 | file, err := os.Open(filename) 33 | if err != nil { 34 | return fmt.Errorf("failed to open file %s: %w", filename, err) 35 | } 36 | defer file.Close() 37 | 38 | info, err := file.Stat() 39 | if err != nil { 40 | return fmt.Errorf("failed to get file info for %s: %w", filename, err) 41 | } 42 | 43 | header, err := zip.FileInfoHeader(info) 44 | if err != nil { 45 | return fmt.Errorf("failed to create header for %s: %w", filename, err) 46 | } 47 | 48 | header.Name = filepath.Base(filename) 49 | header.Method = zip.Deflate 50 | 51 | writer, err := zipWriter.CreateHeader(header) 52 | if err != nil { 53 | return fmt.Errorf("failed to create header in zip for %s: %w", filename, err) 54 | } 55 | 56 | _, err = io.Copy(writer, file) 57 | if err != nil { 58 | return fmt.Errorf("failed to copy file %s to zip: %w", filename, err) 59 | } 60 | 61 | fmt.Printf("Successfully added file: %s\n", filename) 62 | return nil 63 | } 64 | 65 | func UnzipFile(inputFile, destination string) error { 66 | reader, err := zip.OpenReader(inputFile) 67 | if err != nil { 68 | return fmt.Errorf("failed to open zip file: %w", err) 69 | } 70 | defer reader.Close() 71 | 72 | for _, file := range reader.File { 73 | if err := extractFile(file, destination); err != nil { 74 | return fmt.Errorf("failed to extract %s: %w", file.Name, err) 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func extractFile(file *zip.File, destination string) error { 82 | path := filepath.Join(destination, file.Name) 83 | 84 | if file.FileInfo().IsDir() { 85 | return os.MkdirAll(path, os.ModePerm) 86 | } 87 | 88 | if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { 89 | return err 90 | } 91 | 92 | outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) 93 | if err != nil { 94 | return err 95 | } 96 | defer outFile.Close() 97 | 98 | zipFile, err := file.Open() 99 | if err != nil { 100 | return err 101 | } 102 | defer zipFile.Close() 103 | 104 | _, err = io.Copy(outFile, zipFile) 105 | return err 106 | } 107 | 108 | func ZipDirectory(outputFile string, directory string) error { 109 | zipFile, err := os.Create(outputFile) 110 | if err != nil { 111 | return fmt.Errorf("failed to create zip file: %w", err) 112 | } 113 | defer zipFile.Close() 114 | 115 | zipWriter := zip.NewWriter(zipFile) 116 | defer zipWriter.Close() 117 | 118 | absDir, err := filepath.Abs(directory) 119 | if err != nil { 120 | return fmt.Errorf("failed to get absolute path: %w", err) 121 | } 122 | 123 | // Ensure the input is a directory 124 | fileInfo, err := os.Stat(absDir) 125 | if err != nil { 126 | return fmt.Errorf("failed to stat directory: %w", err) 127 | } 128 | if !fileInfo.IsDir() { 129 | return fmt.Errorf("%s is not a directory", absDir) 130 | } 131 | 132 | baseDir := filepath.Dir(absDir) // Parent directory of the target directory 133 | 134 | err = filepath.Walk(absDir, func(filePath string, info os.FileInfo, err error) error { 135 | if err != nil { 136 | return err 137 | } 138 | 139 | // Get relative path from base directory to current file/directory 140 | relPath, err := filepath.Rel(baseDir, filePath) 141 | if err != nil { 142 | return fmt.Errorf("failed to get relative path: %w", err) 143 | } 144 | 145 | header, err := zip.FileInfoHeader(info) 146 | if err != nil { 147 | return fmt.Errorf("failed to create header: %w", err) 148 | } 149 | 150 | // Set header name to preserve directory structure 151 | header.Name = relPath 152 | 153 | // Handle directories by adding trailing slash and using Store method 154 | if info.IsDir() { 155 | header.Name += "/" 156 | header.Method = zip.Store 157 | } else { 158 | header.Method = zip.Deflate 159 | } 160 | 161 | writer, err := zipWriter.CreateHeader(header) 162 | if err != nil { 163 | return fmt.Errorf("failed to create writer: %w", err) 164 | } 165 | 166 | if info.IsDir() { 167 | return nil // No content to write for directories 168 | } 169 | 170 | file, err := os.Open(filePath) 171 | if err != nil { 172 | return fmt.Errorf("failed to open file: %w", err) 173 | } 174 | defer file.Close() 175 | 176 | _, err = io.Copy(writer, file) 177 | if err != nil { 178 | return fmt.Errorf("failed to copy file: %w", err) 179 | } 180 | 181 | fmt.Printf("Added: %s\n", relPath) 182 | return nil 183 | }) 184 | if err != nil { 185 | return fmt.Errorf("error walking directory: %w", err) 186 | } 187 | 188 | return nil 189 | } 190 | --------------------------------------------------------------------------------