loading...
Cover image for Attempting to Learn Go - Sorting and Moving Files by Extension

Attempting to Learn Go - Sorting and Moving Files by Extension

shindakun profile image Steve Layton ・5 min read

Off the Rails

It turns out that we went off the rails last week. Which actually makes for a nice starting point this time around. Our goal last time was to write code that would sort files based on the files extensions. Again thanks to @ladydascalie for the ideas!

  • Write a program to sort files within a folder by their extension

When I read the original ask I assumed that we should sort and order the files in each extension "bucket". As it turns out that was not "required". My assumption caused me to write a good bit of extra code. So while in the end, we did get to what we wanted there was a bunch of extra code for doing a whole bunch of other things. Oh well.

I realized that I also am not leaning on the standard library as much as I should be. I spent a lot of time splitting a filename to get the extension when filepath.Ext() would have returned it for me. Oh well, again.

I am going to try to keep a better eye out for that as we go forward - that's what attempting to learn is all about after all. Though not as close as I should this week since I didn't leave myself enough time to do the coding practice I wanted to do.

Carrying On

This time around we're going to tackle the second part of the ask, the "bonus section" if you will.

  • Later make it sort them in logical folders ex: .txt in Documents, .jpg in Images etc...

Alright, so let's think about it, we've got our map of extensions and filenames, what should we do with it? In this version, we're going to make a simple switch statement based on the extension "bucket"! OK, that's all well and good but are we going to do this to standard out or on the filesystem? I'm going to interpret this as on the filesystem. There I go assuming again! We'll range over the file extensions. Then for each one, we'll create the destination directory, if needed. After that, we'll loop through the actual files in that bucket and move them to their new home. Sounds easy enough let's start with a couple of small snippets which we'll apply in our new code.


Creating A Target Directory

Thanks to os.Mkdir() we can create a directory. MkdirAll() could be an option if we wanted to be able to make a whole set of directories along a path. We don't want to, at least for now, to keep it simple.

  err := os.Mkdir(destination, 0777)
  if err != nil {
    return err
  }

Moving Files

Again the standard library has us covered! os.Rename(src, dest) will "move" files by renaming them with a different path.

  err = os.Rename("./file.md", "./documents/file.go")
  if err != nil {
    return err
  }

So, that's our new features let's get down to modifying our previous code. We're going to remove the entire sections on printing to the screen as it's not needed. Then insert some new functions for creating the directory and then moving the file.


Code Walkthrough

We'll skip over our imports and dive right in!

package main

import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "os"
  "strings"
)

I suppose I didn't need to break this into its own function as it could have fit right in in the main function. Splitting out createDestination() makes the upcoming prepAndMove() a more readable in the end.

func createDestination(s string) error {
  if _, err := os.Stat(s); os.IsNotExist(err) {
    err := os.Mkdir(s, 0777)
    if err != nil {
      return err
    }
  } else {
    // already exists, probably, maybe, hopefully
    return nil
  }
  return nil
}

doMove(), that sounds promising! This does exactly what it says on the tin, it attempts to move the files using os.Rename().

func doMove(file, dir string) error {
  err := os.Rename(file, dir+"/"+file)
  if err != nil {
    return err
  }
  return nil
}

prepAndMove() is the new "core" of our program. It receives our map of extensions and filename strings. Now I suppose we could get creative and have a second map as in map[string][]string. It could hold the directory to sort to along with the extension that goes in that directory. We could then loop through that and have a bunch of nested loops and craziness going on. But, for a small utility that might be overkill, let's stick with the switch statement. We could always add it later, let's stick with trying to solve the "ask" for now.

So, for each extension bucket, we'll range through and match on the extension. Once matched, we will attempt to create a destination directory. If all goes well we'll then range the filenames for that extension and pass them into doMove(). Done with one bucket we'll move on to the next until we've been through them all. For brevity, I'm only including the documents and images section.

func prepAndMove(files map[string][]string) {
  for i, list := range files {
    switch i {
    case "text", "txt", "md":
      dir := "documents"
      err := createDestination(dir)
      if err != nil {
        msg := fmt.Sprintf("An error occured creating destiation.\n%s", err)
        fmt.Println(msg)
        os.Exit(1)
      }
      for j := range list {
        doMove(list[j], dir)
      }
    case "png", "jpg", "gif", "webp":
      dir := "images"
      err := createDestination(dir)
      if err != nil {
        msg := fmt.Sprintf("An error occured creating destiation.\n%s", err)
        fmt.Println(msg)
        os.Exit(1)
      }
      for j := range list {
        doMove(list[j], dir)
      }
    }
  }
}

The first bit of main() hasn't changed much at all. I am still considering pulling our "error then exit" code into a separate function. We are using similar code in so many places it would be worth it. There's one for when refactoring time comes!

func main() {
  wd, err := os.Getwd()
  if err != nil {
    msg := fmt.Sprintf("An error occured getting the current working directory.\n%s", err)
    fmt.Println(msg)
    os.Exit(1)
  }

  dir, err := ioutil.ReadDir(wd)
  if err != nil {
    msg := fmt.Sprintf("An error occured reading the current working directory.\n%s", err)
    fmt.Println(msg)
    os.Exit(1)
  }

I could have swapped in the example done by Benjamin in the comments but that felt a bit like cheating. I do like the use of continue in it and will have to keep that in mind it's much cleaner. We removed the code looking for files with no extension. It is not needed since we can't sort those anyway.

  var m = make(map[string][]string)
  for _, file := range dir {
    if !file.IsDir() {
      fileName := file.Name()
      ext := strings.Split(fileName, ".")
      if len(ext) > 1 {
        m[ext[len(ext)-1]] = append(m[ext[len(ext)-1]], fileName)
      }
    }
  }

  prepAndMove(m)
}

Next Time

It's a tad disjointed but gets the job done. It's not super useful since I didn't go back in and update it to take a source directory or a root destination. I'm happy with how it came out for the short amount of time I ended up giving myself to work on it.

Feel free to let me know what you would change or do differently in the comments below!


You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.



Posted on by:

shindakun profile

Steve Layton

@shindakun

I've been known to write some code from time to time.

Discussion

markdown guide
 

In prepAndMove you have quite some repetition. I would make a map of extension to document type:

jpg: image
png: image
txt: document
...

This way you can remove the duplicate code. Also your config would look cleaner (simple data vs scattered code) and could even be loaded in runtime from an external file.

 

Thanks for the comment! I kind of touched on that in the post. I have given myself an arbitrary time limit to get these posts up. It was looking like I may not hit it so I never went back and added it. The original plan was a map[string][]string with the document folder and then the array of extensions. As it is now, the switch wouldn't be that bad if I got rid of the repetitive error code as well.