DEV Community

Boluwatife Adewusi
Boluwatife Adewusi

Posted on

My First Project using Golang (Part 2)

Part 2: Making File Organizer More Flexible 🎯

In Part 1, we explored how to organize files into categories such as images, audio, and documents by manually defining them in a map. But here’s the catch—what if you want to add another category or your friend wants to organize a new type of file? Right now, they would have to change the code and rebuild the tool, which isn't practical for everyone. 🛠️

In this part, we’re going to make File Organizer more flexible, allowing users to add or modify categories without having to write Go code. Exciting, right? Let’s dive in! 🌊

The Problem 🤔

As it stands, the only way to add a new file type is by modifying the ALLOWED_TYPES map in the source code. That’s fine if you're the only one using the tool, but if you want to share this with others, it can get tricky. Expecting everyone to know Go to make a simple change isn’t a good idea.

The Solution 💡

Now, here comes the fun part! Instead of hardcoding everything, we’re going to give File Organizer a makeover by making it much more user-friendly and flexible. 🌟

First off, we’ll find a more permanent storage solution for keeping track of our accepted file types and their corresponding folder names. This way, users won’t need to dig through code just to make changes.

Next up, we’ll implement some neat methods that give the user full control. They’ll be able to manipulate the file types they want to support and even customize the folder names to their liking—no Go knowledge required! 🛠️💻

To achieve all of this, we’re diving into the world of structs, interfaces, and receiver functions. And here’s the best part: we’ll add some CLI interactivity so users can easily interact with our tool without breaking a sweat. 🎮

Alright, enough talk—let’s get into it! ⚡

Defining the Custom Data Type 🛠️

Now, we’re going to level up our code! 💡 Instead of using the map data structure we had in Part 1, let’s define a custom data type to handle our file categories. This gives us more flexibility and makes our code a lot cleaner.

models/models.go

type FileCategory struct {
    FileType   string   `json:"file_type"`
    Extensions []string `json:"extensions"`
    FolderName string   `json:"folder_name"`
}
Enter fullscreen mode Exit fullscreen mode

We’re taking advantage of Go’s ability to define custom types by using a struct. 🏗️ You’ll also notice this little piece of magic: json:"file_type" next to each declaration. Don't worry about that just yet—it’ll come in handy later. 😉

Introducing the DataStore Interface 🔗

Next up, we’re adding an interface that will be the backbone of our tool's flexibility. Why, you ask? Well, this interface enables users to make changes to file categories—like adding extensions or renaming folders.

models/models.go

type DataStore interface {
    AddCategory(fileType, folderName string, extensions []string) error
    RemoveCategory(fileType string) error
    AddExtensionsToCategory(fileType string, extensions []string) error
    RemoveExtensionsFromCategory(fileType string, extensions []string) error
    SetCategoryFolder(fileType, folderName string) error
    GetCategories() ([]*FileCategory, error)
    GetType(fileName string) (FileCategory, error)
}
Enter fullscreen mode Exit fullscreen mode

Now, why do we need this? The simple answer: flexibility! By using an interface, we can easily swap out how we store the file categories—whether it’s a file-based storage solution or a full-blown database. 🗄️🔄 This ensures that any data source we choose conforms to the same methods, so you can change the storage solution without breaking the code. Talk about efficiency! 🔧💡

Final Touches to the models.go file

This all seems good but before we move to the implementation of the DataStore interface, let's add a couple of functions to the models file

models/models.go

func (c FileCategory) IsOfType(fileName string) bool {
    for _, ext := range c.Extensions {
        if strings.HasSuffix(fileName, ext) {
            return true
        }
    }
    return false
}

func (c FileCategory) MoveToFolder(src string) error {
    dest := filepath.Join(filepath.Dir(src), c.FolderName)

    if _, err := os.Stat(dest); os.IsNotExist(err) {
        err := os.Mkdir(dest, 0755)
        if err != nil {
            return err
        }
    }

    fileName := filepath.Base(src)

    err := os.Rename(src, filepath.Join(dest, fileName))

    if err != nil {
        return err
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

The IsOfType function checks whether a file matches any of the extensions in its category, while MoveToFolder does exactly what the name implies—moves files to the appropriate folder.

You might’ve noticed the syntax between func and the function name—this is where receiver functions come into play. In Go, receiver functions allow you to associate methods with a specific type (like our FileCategory struct). This means we can define behaviors directly on our structs, making them more functional and keeping our code clean and organized.

Implementing our DataStore

First, let's move our ALLOWED_TYPE map to a constants package

// constants/constants.go

package constants

var ALLOWED_TYPES = map[string][]string{
    "images":   {".jpg", ".jpeg", ".png", ".gif", ".svg", ".bmp", ".webp", ".psd", ".ico", ".heic", ".raw", ".ai"},
    "audio":    {".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a", ".wma", ".alac", ".aiff", ".pcm"},
    "video":    {".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v", ".mpeg", ".3gp", ".ogg"},
    "document": {".doc", ".docx", ".pdf", ".txt", ".rtf", ".odt", ".ppt", ".pptx", ".xls", ".xlsx", ".csv", ".html", ".xml", ".md", ".epub", ".pages"},
}
Enter fullscreen mode Exit fullscreen mode

Next, We are going to use the FileStorage as our datastore. Here's the implementation

// models/store_file.go

package models

import (
    "bufio"
    "encoding/json"
    "errors"
    "fmt"
    "os"
    "path/filepath"

    "your_project/constants"
)

// Define the store file name
const storeFileName = "store.json"

// FileDataStore struct to store the path
type FileDataStore struct {
    storagePath string
}

// New function creates a new instance of DataStore (FileDataStore)
func New(storagePath string) DataStore {
    storage := FileDataStore{storagePath: storagePath}
    storage.Init()
    return &storage
}

// Init initializes the file categories if they don't exist
func (f FileDataStore) Init() {
    _, err := f.GetCategories()
    if err != nil {
        dataStore := make([]*FileCategory, len(constants.ALLOWED_TYPES))
        index := 0
        for key, value := range constants.ALLOWED_TYPES {
            dataStore[index] = &FileCategory{
                FileType:   key,
                Extensions: value,
                FolderName: key,
            }
            index++
        }
        err = saveToFile(dataStore, filepath.Join(f.storagePath, storeFileName))

        if err != nil {
            panic(err)
        }
    }
}

// GetCategories retrieves stored categories from the JSON file
func (f FileDataStore) GetCategories() ([]*FileCategory, error) {
    file, err := os.Open(filepath.Join(f.storagePath, storeFileName))

    if err != nil {
        return nil, err
    }

    defer file.Close()

    scanner := bufio.NewScanner(file)
    jsonString := ""

    for scanner.Scan() {
        jsonString += scanner.Text()
    }

    var categories []*FileCategory
    err = json.Unmarshal([]byte(jsonString), &categories)

    if err != nil {
        return nil, err
    }

    return categories, nil
}

// GetType determines the category of a given file based on its extension
func (f FileDataStore) GetType(fileName string) (FileCategory, error) {
    categories, err := f.GetCategories()

    if err != nil {
        return FileCategory{}, err
    }

    for _, category := range categories {
        if category.IsOfType(fileName) {
            return *category, nil
        }
    }
    return FileCategory{}, errors.New(fmt.Sprintf("Category for File: %s not found", fileName))
}

// AddCategory allows adding new file categories
func (f FileDataStore) AddCategory(fileType string, folderName string, extensions []string) error {
    categories, err := f.GetCategories()

    if err != nil {
        return err
    }

    var fileCategory *FileCategory

    for _, category := range categories {
        if category.FileType == fileType {
            fileCategory = category
            break
        }
    }

    if fileCategory == nil {
        fileCategory = &FileCategory{
            FileType:   fileType,
            Extensions: extensions,
            FolderName: fileType,
        }

        if folderName != "" {
            fileCategory.FolderName = folderName
        }

        categories = append(categories, fileCategory)
    } else {
        fileCategory.Extensions = append(fileCategory.Extensions, extensions...)
        if folderName != "" {
            fileCategory.FolderName = folderName
        }
    }

    err = saveToFile(categories, filepath.Join(f.storagePath, storeFileName))

    if err != nil {
        return err
    }

    return nil
}

// RemoveCategory removes a file category by file type
func (f FileDataStore) RemoveCategory(fileType string) error {
    categories, err := f.GetCategories()

    if err != nil {
        return err
    }

    var updatedCategories []*FileCategory

    for _, category := range categories {
        if category.FileType != fileType {
            updatedCategories = append(updatedCategories, category)
        }
    }

    err = saveToFile(updatedCategories, filepath.Join(f.storagePath, storeFileName))

    if err != nil {
        return err
    }

    return nil
}

// AddExtensionsToCategory adds new extensions to a given file category
func (f FileDataStore) AddExtensionsToCategory(fileType string, extensions []string) error {
    categories, err := f.GetCategories()

    if err != nil {
        return err
    }

    for _, category := range categories {
        if category.FileType == fileType {
            category.Extensions = append(category.Extensions, extensions...)
            break
        }
    }

    err = saveToFile(categories, filepath.Join(f.storagePath, storeFileName))

    if err != nil {
        return err
    }

    return nil
}

// RemoveExtensionsFromCategory removes extensions from a given file category
func (f FileDataStore) RemoveExtensionsFromCategory(fileType string, extensions []string) error {
    categories, err := f.GetCategories()

    if err != nil {
        return err
    }

    for _, category := range categories {
        if category.FileType == fileType {
            for _, ext := range extensions {
                for i, existingExt := range category.Extensions {
                    if existingExt == ext {
                        category.Extensions = append(category.Extensions[:i], category.Extensions[i+1:]...)
                    }
                }
            }
        }
    }

    err = saveToFile(categories, filepath.Join(f.storagePath, storeFileName))

    if err != nil {
        return err
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

The New function returns a DataStore, but notice how we're actually returning a FileDataStore instance. How does that work without Go throwing an error? Well, this is thanks to Go's awesome feature: implicit interfaces! 🎉

In Go, as long as a type (like FileDataStore) implements all the methods defined in an interface (like DataStore), it automatically satisfies that interface. No need to explicitly declare it. So, since FileDataStore implements all the required methods, it’s treated as a DataStore by Go.

It's like Go saying, "You do the work? Great, you get the title!" 😎

Let's take it for a spin

go
main.go

package main

import (
    "your_project/models"
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    //cmd.Execute()

    dataStore := models.NewFileDataStore("")

    displayCategories(dataStore)

    err := dataStore.AddCategory("private", "none_of_your_business", []string{".xyz", ".abc"})

    if err != nil {
        fmt.Printf("Add Category: %v", err)
        return
    }

    displayCategories(dataStore)

    organizeFiles("", dataStore)
}

func displayCategories(store models.DataStore) {
    categories, err := store.GetCategories()

    if err != nil {
        fmt.Printf("Get Categories: %v", err)
        return
    }

    fmt.Println("Category\t\t\t\t Folder Name\t\t\t\t")
    for _, category := range categories {
        fmt.Println(fmt.Sprintf("%s\t\t\t\t %s\t\t\t\t", category.FileType, category.FolderName))
    }

}

func organizeFiles(dir string, store models.DataStore) {
    files, err := os.ReadDir(dir)

    if err != nil {
        fmt.Println(err)
    }

    for _, file := range files {
        if file.IsDir() {
            continue
        }

        fileName := file.Name()

        category, err := store.GetType(fileName)

        if err != nil {
            fmt.Println(err)
            continue
        }

        err = category.MoveToFolder(filepath.Join(dir, fileName))

        if err != nil {
            fmt.Println(err)
            continue
        }

        fmt.Printf("%s moved to %s\n", fileName, filepath.Join(dir, category.FolderName, fileName))
    }
}

Enter fullscreen mode Exit fullscreen mode

So far, everything’s shaping up nicely! But guess what? We’re just getting started. 🎉 Our application works, but it’s still missing that spark of interactivity. Imagine giving users the ability to tweak and manage categories right from the command line! 👨‍💻👩‍💻

In Part 3, we’re diving deeper and adding that layer of interaction to really bring this tool to life. Stay tuned—it's about to get way more fun and dynamic! ⚡

Top comments (0)