diff --git a/README.md b/README.md index f38b8e0..cda77d0 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Folder Structure CLI is a command-line tool written in Go that creates a folder - Create folders and files based on a JSON structure - Simple command-line interface - Recursive creation of nested structures +- Optional protection against overwriting existing files and folders - Error handling and reporting ## Installation @@ -40,17 +41,38 @@ Folder Structure CLI is a command-line tool written in Go that creates a folder ### Basic Command ```bash -./folder-structure-cli create [json_file_path] [output_path] +./folder-structure-cli create [json_file_path] [output_path] [flags] ``` - `[json_file_path]`: Path to the JSON file describing the folder structure - `[output_path]`: Path where the folder structure will be created -### Example +### Available Flags +- `--no-overwrite` or `-n`: Do not overwrite existing files and folders. When this flag is used, any existing files or folders will be skipped instead of being overwritten. + +### Examples + +#### Basic usage (overwrites existing files by default): ```bash ./folder-structure-cli create structure.json ./output ``` +#### With no-overwrite protection: +```bash +./folder-structure-cli create structure.json ./output --no-overwrite +``` + +or using the short flag: +```bash +./folder-structure-cli create structure.json ./output -n +``` + +When using the `--no-overwrite` flag: +- Existing files and folders will be skipped +- A message will be displayed for each skipped item +- New files and folders will still be created normally +- This helps prevent accidental data loss + ### Version Information To display the version of the CLI: @@ -62,6 +84,7 @@ or ```bash ./folder-structure-cli --version ``` + ## JSON Structure The JSON file should describe the folder structure. Use `null` for files and nested objects for folders. @@ -94,12 +117,29 @@ output/ │ └── file3.txt └── file4.txt ``` + +## Safety Features + +### No-Overwrite Protection + +When using the `--no-overwrite` flag, the tool will: +- Check if each file or folder already exists before creating it +- Skip creation if the item already exists +- Display a message indicating which items were skipped +- Continue processing the rest of the structure + +This is particularly useful when: +- Adding new files to an existing project structure +- Running the command multiple times +- Protecting important existing files from being accidentally overwritten + ## Error Handling The CLI will print error messages for: - Invalid JSON files - File read/write errors - Invalid folder structures +- Permission issues ## Development @@ -108,13 +148,17 @@ The CLI will print error messages for: folder-structure-cli/ ├── cmd/ │ ├── root.go -│ └── create.go +│ ├── create.go +│ ├── create_test.go +│ └── version.go ├── main.go └── go.mod ``` - `main.go`: Entry point of the application - `cmd/root.go`: Defines the root command and version flag -- `cmd/create.go`: Implements the `create` command +- `cmd/create.go`: Implements the `create` command with no-overwrite functionality +- `cmd/create_test.go`: Tests for the create command including no-overwrite scenarios +- `cmd/version.go`: Version command implementation ### Adding New Commands @@ -124,10 +168,22 @@ To add a new command: 2. Define the command structure and functionality 3. Add the command to the root command in the `init()` function +### Running Tests + +```bash +go test ./... +``` + +or with coverage: + +```bash +make test +``` + ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License -This project is licensed under the MIT License - see the LICENSE file for details. +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/TODO b/TODO deleted file mode 100644 index fa0dee3..0000000 --- a/TODO +++ /dev/null @@ -1,16 +0,0 @@ -project: - goreleaser.yaml: - project_name: - ✔ replace PLACEHOLDER with actual name @done(24-09-09 11:28) - env: - GO_MAIN_PATH: - ✔ replace PLACEHOLDER with actual path @done(24-09-09 11:28) - VERSION_PATH: - ✔ replace PLACEHOLDER with acutual path @done(24-09-09 11:28) - readme.md: - ✔ update based on relevant project info @done(24-09-09 12:19) - testing: - ✔ write tests @done(24-09-09 12:27) - .github: - workflows: - ✔ verify everythign is setup @done(24-09-10 12:05) \ No newline at end of file diff --git a/cmd/create.go b/cmd/create.go index e084152..764bfe7 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -10,6 +10,10 @@ import ( "github.com/spf13/cobra" ) +var ( + noOverwrite bool +) + var createCmd = &cobra.Command{ Use: "create [json_file_path] [output_path]", Short: "Create folder structure from JSON", @@ -19,6 +23,7 @@ var createCmd = &cobra.Command{ } func init() { + createCmd.Flags().BoolVarP(&noOverwrite, "no-overwrite", "n", false, "Do not overwrite existing files and folders") RootCmd.AddCommand(createCmd) } @@ -42,7 +47,7 @@ func runCreate(cmd *cobra.Command, args []string) { } // Create folder structure - err = createStructure(outputPath, structure) + err = createStructure(outputPath, structure, noOverwrite) if err != nil { fmt.Printf("Error creating folder structure: %v\n", err) return @@ -51,23 +56,49 @@ func runCreate(cmd *cobra.Command, args []string) { fmt.Println("Folder structure created successfully.") } -func createStructure(basePath string, structure map[string]interface{}) error { +func createStructure(basePath string, structure map[string]interface{}, noOverwrite bool) error { for key, value := range structure { itemPath := filepath.Join(basePath, key) + if value == nil { - // Create file + // Handle file creation + if noOverwrite { + if _, err := os.Stat(itemPath); err == nil { + fmt.Printf("Skipping existing file: %s\n", itemPath) + continue + } + } + + // Ensure the parent directory exists + parentDir := filepath.Dir(itemPath) + if err := os.MkdirAll(parentDir, os.ModePerm); err != nil { + return fmt.Errorf("error creating parent directory %s: %v", parentDir, err) + } + _, err := os.Create(itemPath) if err != nil { return fmt.Errorf("error creating file %s: %v", itemPath, err) } } else if subStructure, ok := value.(map[string]interface{}); ok { - // Create directory - err := os.MkdirAll(itemPath, os.ModePerm) - if err != nil { - return fmt.Errorf("error creating directory %s: %v", itemPath, err) + // Handle directory creation + dirExists := false + if _, err := os.Stat(itemPath); err == nil { + dirExists = true + if noOverwrite { + fmt.Printf("Directory already exists: %s\n", itemPath) + } } - // Recursively create its structure - err = createStructure(itemPath, subStructure) + + // Create directory if it doesn't exist + if !dirExists { + err := os.MkdirAll(itemPath, os.ModePerm) + if err != nil { + return fmt.Errorf("error creating directory %s: %v", itemPath, err) + } + } + + // Always process the directory contents, regardless of whether the directory existed + err := createStructure(itemPath, subStructure, noOverwrite) if err != nil { return err } diff --git a/cmd/create_test.go b/cmd/create_test.go index 87d110a..030c5b8 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -27,7 +27,7 @@ func TestCreateStructure(t *testing.T) { "file2.txt": nil, } - err = createStructure(tempDir, structure) + err = createStructure(tempDir, structure, false) if err != nil { t.Fatalf("createStructure failed: %v", err) } @@ -54,7 +54,7 @@ func TestCreateStructureWithNestedFolders(t *testing.T) { "file2.txt": nil, } - err = createStructure(tempDir, structure) + err = createStructure(tempDir, structure, false) if err != nil { t.Fatalf("createStructure failed: %v", err) } @@ -75,12 +75,135 @@ func TestCreateStructureWithInvalidInput(t *testing.T) { "folder1": "invalid", } - err = createStructure(tempDir, structure) + err = createStructure(tempDir, structure, false) if err == nil { t.Fatalf("Expected an error for invalid structure, but got nil") } } +func TestCreateStructureWithNoOverwrite(t *testing.T) { + tempDir, err := os.MkdirTemp("", "test-no-overwrite") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create initial structure + initialStructure := map[string]interface{}{ + "existing_folder": map[string]interface{}{ + "existing_file.txt": nil, + }, + "existing_file.txt": nil, + } + + err = createStructure(tempDir, initialStructure, false) + if err != nil { + t.Fatalf("Failed to create initial structure: %v", err) + } + + // Write some content to the existing file to verify it's not overwritten + existingFilePath := filepath.Join(tempDir, "existing_file.txt") + originalContent := "original content" + err = os.WriteFile(existingFilePath, []byte(originalContent), 0644) + if err != nil { + t.Fatalf("Failed to write to existing file: %v", err) + } + + // Also write content to the file inside the existing folder + existingFileInFolderPath := filepath.Join(tempDir, "existing_folder", "existing_file.txt") + existingFileContent := "existing file in folder" + err = os.WriteFile(existingFileInFolderPath, []byte(existingFileContent), 0644) + if err != nil { + t.Fatalf("Failed to write to existing file in folder: %v", err) + } + + // Try to create structure again with no-overwrite flag + newStructure := map[string]interface{}{ + "existing_folder": map[string]interface{}{ + "existing_file.txt": nil, + "new_file.txt": nil, + }, + "existing_file.txt": nil, + "new_folder": map[string]interface{}{ + "new_file.txt": nil, + }, + } + + err = createStructure(tempDir, newStructure, true) + if err != nil { + t.Fatalf("createStructure with no-overwrite failed: %v", err) + } + + // Verify existing file content is preserved + content, err := os.ReadFile(existingFilePath) + if err != nil { + t.Fatalf("Failed to read existing file: %v", err) + } + if string(content) != originalContent { + t.Errorf("Expected existing file content to be preserved, got %s", string(content)) + } + + // Verify existing file in folder content is preserved + content, err = os.ReadFile(existingFileInFolderPath) + if err != nil { + t.Fatalf("Failed to read existing file in folder: %v", err) + } + if string(content) != existingFileContent { + t.Errorf("Expected existing file in folder content to be preserved, got %s", string(content)) + } + + // Verify new files and folders are created + newFilePath := filepath.Join(tempDir, "existing_folder", "new_file.txt") + if _, err := os.Stat(newFilePath); os.IsNotExist(err) { + t.Errorf("Expected new file to be created: %s", newFilePath) + } + + newFolderPath := filepath.Join(tempDir, "new_folder") + if _, err := os.Stat(newFolderPath); os.IsNotExist(err) { + t.Errorf("Expected new folder to be created: %s", newFolderPath) + } + + newFileInNewFolderPath := filepath.Join(tempDir, "new_folder", "new_file.txt") + if _, err := os.Stat(newFileInNewFolderPath); os.IsNotExist(err) { + t.Errorf("Expected new file in new folder to be created: %s", newFileInNewFolderPath) + } +} + +func TestCreateStructureOverwriteDefault(t *testing.T) { + tempDir, err := os.MkdirTemp("", "test-overwrite-default") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create initial file + existingFilePath := filepath.Join(tempDir, "test_file.txt") + originalContent := "original content" + err = os.WriteFile(existingFilePath, []byte(originalContent), 0644) + if err != nil { + t.Fatalf("Failed to create initial file: %v", err) + } + + // Create structure without no-overwrite (should overwrite by default) + structure := map[string]interface{}{ + "test_file.txt": nil, + } + + err = createStructure(tempDir, structure, false) + if err != nil { + t.Fatalf("createStructure failed: %v", err) + } + + // Verify file was overwritten (should be empty now) + content, err := os.ReadFile(existingFilePath) + if err != nil { + t.Fatalf("Failed to read file: %v", err) + } + if len(content) != 0 { + t.Errorf("Expected file to be overwritten (empty), but got content: %s", string(content)) + } +} + func verifyStructure(t *testing.T, basePath string, structure map[string]interface{}) { for key, value := range structure { path := filepath.Join(basePath, key) @@ -134,6 +257,9 @@ func TestRunCreate(t *testing.T) { t.Fatalf("Failed to create output directory: %v", err) } + // Reset the noOverwrite flag to false for this test + noOverwrite = false + // Run the create command cmd := &cobra.Command{} args := []string{jsonFile, outputDir}