Repository: gphotosuploader/gphotos-uploader-cli
Pull request #149 (2019-11-02 07:51:05)

Add logger package and increase test coverage
**What issue type does this pull request address?** (keep at least one, remove the others)  
feature

**What is this pull request for? Which issues does it resolve?** (use `resolves #<issue_number>` if possible)  
Add a Logger package to be able to configure the amount of information that the CLI would get

**Does this pull request has user-facing changes?** (e.g. config changes, new/modified commands, new/modified flags)  
Two new global flags `--silent` and `--debug` are available.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdce39b..0250fb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
 ## Unreleased
 ### Added
 - [CONTRIBUTING](CONTRIBUTING.md) guide line has been added.
+- New Logger package to improve log readability.
 ### Changed
 - [README](README.md) has been updated fixing some typos.
 ### Deprecated
diff --git a/app/app.go b/app/app.go
index b1b4436..fd13bd1 100644
--- a/app/app.go
+++ b/app/app.go
@@ -1,9 +1,9 @@
 package app
 
 import (
-	"log"
-
 	"golang.org/x/oauth2"
+
+	"github.com/gphotosuploader/gphotos-uploader-cli/log"
 )
 
 type App struct {
@@ -11,7 +11,7 @@ type App struct {
 	TokenManager  TokenManager
 	UploadTracker UploadTracker
 
-	Log *log.Logger
+	Log log.Logger
 }
 
 type FileTracker interface {
diff --git a/cmd/http.go b/app/oauth.go
similarity index 69%
rename from cmd/http.go
rename to app/oauth.go
index 4329d99..6f6bc88 100644
--- a/cmd/http.go
+++ b/app/oauth.go
@@ -1,17 +1,14 @@
-package cmd
+package app
 
 import (
 	"context"
 	"fmt"
-	"log"
 	"net/http"
 
 	"github.com/int128/oauth2cli"
 	"github.com/pkg/browser"
 	"golang.org/x/oauth2"
 	"golang.org/x/sync/errgroup"
-
-	"github.com/gphotosuploader/gphotos-uploader-cli/app"
 )
 
 const successPage = `
@@ -24,25 +21,25 @@ func newHTTPClient() *http.Client {
 	return http.DefaultClient
 }
 
-// newOAuth2Client returns a http client for the supplied Google account.
+// NewOAuth2Client returns a http client for the supplied Google account.
 // It will try to get the credentials from the Token Manager, if they are not valid will try to refresh the token or
 // ask for authenticate again.
-func newOAuth2Client(ctx context.Context, tokenManager app.TokenManager, oauth2Config oauth2.Config, account string) (*http.Client, error) {
-	token, err := tokenManager.RetrieveToken(account)
+func (app *App) NewOAuth2Client(ctx context.Context, oauth2Config oauth2.Config, account string) (*http.Client, error) {
+	token, err := app.TokenManager.RetrieveToken(account)
 	if err != nil {
-		log.Printf("Token has not been retrieved from token store: %s", err)
+		app.Log.Debugf("Token has not been retrieved from token store: %s", err)
 	}
 
 	ctx = context.WithValue(ctx, oauth2.HTTPClient, newHTTPClient())
 	switch {
 	case token == nil:
-		token, err = obtainOAuthTokenFromAuthServer(ctx, oauth2Config)
+		token, err = app.obtainOAuthTokenFromAuthServer(ctx, oauth2Config)
 		if err != nil {
 			return nil, fmt.Errorf("could not get a token: %s", err)
 		}
 
 	case !token.Valid():
-		log.Printf("Token has been expired, refreshing")
+		app.Log.Info("Token has been expired, refreshing")
 		token, err = oauth2Config.TokenSource(ctx, token).Token()
 		if err != nil {
 			return nil, fmt.Errorf("could not refresh the token: %s", err)
@@ -51,11 +48,11 @@ func newOAuth2Client(ctx context.Context, tokenManager app.TokenManager, oauth2C
 
 	// debug
 	if token != nil {
-		log.Printf("Token expiration: %s", token.Expiry.String())
+		app.Log.Debugf("Token expiration: %s", token.Expiry.String())
 	}
 
 	// and store the token into the keyring
-	err = tokenManager.StoreToken(account, token)
+	err = app.TokenManager.StoreToken(account, token)
 	if err != nil {
 		return nil, fmt.Errorf("failed storing token: %s", err)
 	}
@@ -64,7 +61,7 @@ func newOAuth2Client(ctx context.Context, tokenManager app.TokenManager, oauth2C
 	return client, nil
 }
 
-func obtainOAuthTokenFromAuthServer(ctx context.Context, oauth2Config oauth2.Config) (*oauth2.Token, error) {
+func (app *App) obtainOAuthTokenFromAuthServer(ctx context.Context, oauth2Config oauth2.Config) (*oauth2.Token, error) {
 	var token *oauth2.Token
 	var err error
 
@@ -77,10 +74,10 @@ func obtainOAuthTokenFromAuthServer(ctx context.Context, oauth2Config oauth2.Con
 				return nil
 			}
 			// Open a browser to complete OAuth process.
-			log.Printf("Openning browser to complete authorization.")
+			app.Log.Info("Opening browser to complete authorization.")
 			err = browser.OpenURL(url)
 			if err != nil {
-				log.Printf("Browser was not detected. Complete the authorization browsing to: %s", url)
+				app.Log.Warnf("Browser was not detected. Complete the authorization browsing to: %s", url)
 			}
 			return nil
 		case err := <-ctx.Done():
@@ -97,7 +94,7 @@ func obtainOAuthTokenFromAuthServer(ctx context.Context, oauth2Config oauth2.Con
 		return err
 	})
 	if err := eg.Wait(); err != nil {
-		log.Printf("error while authorization: %s", err)
+		app.Log.Errorf("error while authorization: %s", err)
 	}
 
 	return token, err
diff --git a/cmd/flags/flags.go b/cmd/flags/flags.go
new file mode 100644
index 0000000..735ab38
--- /dev/null
+++ b/cmd/flags/flags.go
@@ -0,0 +1,25 @@
+package flags
+
+import flag "github.com/spf13/pflag"
+
+const defaultCfgDir = "~/.config/gphotos-uploader-cli"
+
+// GlobalFlags is the flags that contains the global flags
+type GlobalFlags struct {
+	Silent bool
+	Debug  bool
+	CfgDir string
+}
+
+// SetGlobalFlags applies the global flags
+func SetGlobalFlags(flags *flag.FlagSet) *GlobalFlags {
+	globalFlags := &GlobalFlags{
+		CfgDir: defaultCfgDir,
+	}
+
+	flags.BoolVar(&globalFlags.Debug, "debug", false, "Logs very verbose information. Useful for troubleshooting.")
+	flags.BoolVar(&globalFlags.Silent, "silent", false, "Run in silent mode and prevents any log output except panics & fatals.")
+	flags.StringVar(&globalFlags.CfgDir, "config", defaultCfgDir, "Sets config folder path. All configuration will be keep in this folder.")
+
+	return globalFlags
+}
diff --git a/cmd/init.go b/cmd/init.go
index c49a29c..18f5a4f 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -1,32 +1,55 @@
 package cmd
 
 import (
-	"fmt"
-	"log"
-
+	"github.com/mgutz/ansi"
 	"github.com/spf13/cobra"
 
+	"github.com/gphotosuploader/gphotos-uploader-cli/cmd/flags"
 	"github.com/gphotosuploader/gphotos-uploader-cli/config"
+	"github.com/gphotosuploader/gphotos-uploader-cli/log"
 )
 
-// versionCmd get the application version
-var initCmd = &cobra.Command{
-	Use:   "init",
-	Short: "Init the configuration file",
-	Run: func(cmd *cobra.Command, args []string) {
-		force, err := cmd.Flags().GetBool("force")
-		if err != nil {
-			force = false
-		}
-		err = config.InitConfig(cfgDir, force)
-		if err != nil {
-			log.Fatal(err)
-		}
-		fmt.Printf("Configuration file has been created.\nEdit it by running:\n    nano %s/config.hjson\n", cfgDir)
-	},
+// InitCmd holds the required data for the init cmd
+type InitCmd struct {
+	CfgDir string
+	// Flags
+	Reconfigure bool
 }
 
-func init() {
-	initCmd.Flags().BoolP("force", "f", false, "Remove existing configuration folder (if exists)")
-	rootCmd.AddCommand(initCmd)
+func NewInitCmd(globalFlags *flags.GlobalFlags) *cobra.Command {
+	cmd := &InitCmd{
+		CfgDir: globalFlags.CfgDir,
+	}
+
+	initCmd := &cobra.Command{
+		Use:   "init",
+		Short: "Initializes configuration file",
+		Long:  `Initializes a new configuration file. Creates a config.hjson with a default configuration.`,
+		Args:  cobra.NoArgs,
+		RunE:  cmd.Run,
+	}
+
+	initCmd.Flags().BoolVar(&cmd.Reconfigure, "force", false, "Overwrite existing configuration")
+
+	return initCmd
+}
+
+func (cmd *InitCmd) Run(cobraCmd *cobra.Command, args []string) error {
+	// Check if config already exists
+	configExists := config.ConfigExists(cmd.CfgDir)
+	if configExists && !cmd.Reconfigure {
+		log.Infof("Config already exists. If you want to recreate the config please run `%s`", ansi.Color("gphotos-uploader-cli init --force", "white+b"))
+		log.Infof("\r         \nIf you want to continue with the existing config, run:\n- `%s` to start uploading files.\n", ansi.Color("gphotos-uploader-cli push", "white+b"))
+		return nil
+	}
+
+	err := config.InitConfigFile(cmd.CfgDir)
+	if err != nil {
+		return err
+	}
+
+	log.Done("Configuration file successfully initialized.")
+	log.Infof("\r         \nPlease edit: \n- `%s/config.hjson` to add you configuration.\n", cmd.CfgDir)
+
+	return nil
 }
diff --git a/cmd/push.go b/cmd/push.go
new file mode 100644
index 0000000..af67b99
--- /dev/null
+++ b/cmd/push.go
@@ -0,0 +1,130 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+
+	gphotos "github.com/gphotosuploader/google-photos-api-client-go/lib-gphotos"
+	"github.com/spf13/cobra"
+	"github.com/syndtr/goleveldb/leveldb"
+	"golang.org/x/oauth2"
+
+	"github.com/gphotosuploader/gphotos-uploader-cli/app"
+	"github.com/gphotosuploader/gphotos-uploader-cli/cmd/flags"
+	"github.com/gphotosuploader/gphotos-uploader-cli/config"
+	"github.com/gphotosuploader/gphotos-uploader-cli/datastore/completeduploads"
+	"github.com/gphotosuploader/gphotos-uploader-cli/datastore/leveldbstore"
+	"github.com/gphotosuploader/gphotos-uploader-cli/datastore/tokenstore"
+	"github.com/gphotosuploader/gphotos-uploader-cli/log"
+	"github.com/gphotosuploader/gphotos-uploader-cli/photos"
+	"github.com/gphotosuploader/gphotos-uploader-cli/upload"
+)
+
+// PushCmd holds the required data for the push cmd
+type PushCmd struct {
+	*flags.GlobalFlags
+}
+
+func NewPushCmd(globalFlags *flags.GlobalFlags) *cobra.Command {
+	cmd := &PushCmd{GlobalFlags: globalFlags}
+
+	pushCmd := &cobra.Command{
+		Use:   "push",
+		Short: "Push local files to Google Photos service",
+		Long:  `Scan configured folders in the configuration and push all new object to Google Photos service.`,
+		Args:  cobra.NoArgs,
+		RunE:  cmd.Run,
+	}
+
+	return pushCmd
+}
+
+func (cmd *PushCmd) Run(cobraCmd *cobra.Command, args []string) error {
+		cfg, err := config.LoadConfig(cmd.CfgDir)
+		if err != nil {
+			return fmt.Errorf("could't read configuration. Please review your configuration or run 'gphotos-uploader-cli init': file=%s, err=%s", cmd.CfgDir, err)
+		}
+		err = cfg.Validate()
+		if err != nil {
+			return fmt.Errorf("invalid configuration: file=%s, err=%s", cfg.ConfigFile(), err)
+		}
+
+		// File Tracker service to track uploaded files.
+		ft, err := leveldb.OpenFile(cfg.CompletedUploadsDBDir(), nil)
+		if err != nil {
+			return fmt.Errorf("open completed uploads tracker failed: path=%s, err=%s", cfg.CompletedUploadsDBDir(), err)
+		}
+		defer ft.Close()
+		fileTracker := completeduploads.NewService(completeduploads.NewLevelDBRepository(ft))
+
+		// Token Manager service to be used as secrets backend.
+		kr, err := tokenstore.NewKeyringRepository(cfg.SecretsBackendType, nil, cfg.KeyringDir())
+		if err != nil {
+			return fmt.Errorf("open token manager failed: type=%s, err=%s", cfg.SecretsBackendType, err)
+		}
+		tokenManager := tokenstore.NewService(kr)
+
+		// Upload Session Tracker to keep upload session to resume uploads.
+		uploadTracker, err := leveldbstore.NewStore(cfg.ResumableUploadsDBDir())
+		if err != nil {
+			return fmt.Errorf("open resumable uploads tracker failed: path=%s, err=%s", cfg.ResumableUploadsDBDir(), err)
+		}
+		defer uploadTracker.Close()
+
+		// Initialize the App
+		app := &app.App{
+			FileTracker:   fileTracker,
+			TokenManager:  tokenManager,
+			UploadTracker: uploadTracker,
+
+			Log: log.GetInstance(),
+		}
+
+		// start file upload worker
+		uploadChan, doneUploading := upload.StartFileUploadWorker(app.FileTracker, app.Log)
+
+		deletionQueue := upload.NewDeletionQueue()
+		deletionQueue.StartWorkers(app.Log)
+
+		ctx := context.Background()
+		// get OAuth2 Configuration with our App credentials
+		oauth2Config := oauth2.Config{
+			ClientID:     cfg.APIAppCredentials.ClientID,
+			ClientSecret: cfg.APIAppCredentials.ClientSecret,
+			Endpoint:     photos.Endpoint,
+			Scopes:       photos.Scopes,
+		}
+
+		// launch all folder upload jobs
+		for _, item := range cfg.Jobs {
+			c, err := app.NewOAuth2Client(ctx, oauth2Config, item.Account)
+			if err != nil {
+				return err
+			}
+			gPhotos, err := gphotos.NewClientWithResumableUploads(c, app.UploadTracker)
+			if err != nil {
+				return err
+			}
+
+			opt := upload.NewJobOptions(item.MakeAlbums.Enabled, item.DeleteAfterUpload, item.UploadVideos, item.IncludePatterns, item.ExcludePatterns)
+			job := upload.NewFolderUploadJob(gPhotos, app.FileTracker, item.SourceFolder, opt)
+
+			if err := job.ScanFolder(uploadChan, app.Log); err != nil {
+				log.Fatalf("Failed to upload folder %s: %v", item.SourceFolder, err)
+			}
+		}
+
+		// after we've run all the folder upload jobs we're done adding file upload jobs
+		close(uploadChan)
+		// wait for all the uploads to be completed
+		<-doneUploading
+		log.Done("all uploads done")
+
+		// after the last upload is done we're done queueing files for deletion
+		deletionQueue.Close()
+		// wait for deletions to be completed before exiting
+		deletionQueue.WaitForWorkers()
+		log.Done("all deletions done")
+
+		return nil
+}
diff --git a/cmd/root.go b/cmd/root.go
index 1d4eec7..d4d61bc 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,141 +1,63 @@
 package cmd
 
 import (
-	"context"
-	"fmt"
-	"log"
 	"os"
 
+	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
-	"github.com/syndtr/goleveldb/leveldb"
-	"golang.org/x/oauth2"
 
-	gphotos "github.com/gphotosuploader/google-photos-api-client-go/lib-gphotos"
-
-	"github.com/gphotosuploader/gphotos-uploader-cli/app"
-	"github.com/gphotosuploader/gphotos-uploader-cli/config"
-	"github.com/gphotosuploader/gphotos-uploader-cli/datastore/completeduploads"
-	"github.com/gphotosuploader/gphotos-uploader-cli/datastore/leveldbstore"
-	"github.com/gphotosuploader/gphotos-uploader-cli/datastore/tokenstore"
-	"github.com/gphotosuploader/gphotos-uploader-cli/photos"
-	"github.com/gphotosuploader/gphotos-uploader-cli/upload"
+	"github.com/gphotosuploader/gphotos-uploader-cli/cmd/flags"
+	"github.com/gphotosuploader/gphotos-uploader-cli/log"
 )
 
-const defaultCfgDir = "~/.config/gphotos-uploader-cli"
-
-var cfgDir string
-
 // rootCmd represents the base command when called without any subcommands
 var rootCmd = &cobra.Command{
-	Use:   "gphotos-uploader-cli",
-	Short: "This is an unofficial Google Photos uploader",
-	Long: `This application will allow you to upload your pictures to Google Photos.
+	Use:           "gphotos-uploader-cli",
+	SilenceUsage:  true,
+	SilenceErrors: true,
+	Short:         "Welcome to `gphotos-uploader-cli` a Google Photos uploader!",
+	PersistentPreRun: func(cobraCmd *cobra.Command, args []string) {
+		if globalFlags.Silent {
+			log.GetInstance().SetLevel(logrus.FatalLevel)
+		}
+	},
+	Long: `This application allows you to upload your pictures and videos to Google Photos. You can upload folders to several Google Photos accounts and organize them in albums.
 
-You can upload folders of pictures to several Google Photos accounts and organize them in albums.
+    Get started by running the init command to configure your settings:
+    $ gphotos-uploader-cli init
 
-See https://github.com/gphotosuploader/gphotos-uploader-cli for more information.`,
-	Run: startUploader,
+    once it's configured, start uploading your files:
+    $ gphotos-uploader-cli push`,
 }
 
+var globalFlags *flags.GlobalFlags
+
 // Execute adds all child commands to the root command and sets flags appropriately.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 func Execute() {
-	if err := rootCmd.Execute(); err != nil {
-		fmt.Println(err)
+	// Execute command
+	err := rootCmd.Execute()
+	if err != nil {
+		if globalFlags.Debug {
+			log.Fatalf("%+v", err)
+		} else {
+			log.Fatal(err)
+		}
 		os.Exit(1)
 	}
 }
 
 func init() {
-	rootCmd.PersistentFlags().StringVar(&cfgDir, "config", defaultCfgDir, "set config folder")
-}
-
-func startUploader(cmd *cobra.Command, args []string) {
-	var cfg *config.Config
-
-	cfg, err := config.LoadConfig(cfgDir)
-	if err != nil {
-		log.Fatalf("[ERR] Unable to read configuration: dir=%s.\nReview your configuration or execute 'gphotos-uploader-cli init' to create a new one: err=%s", cfgDir, err)
-	}
-	err = cfg.Validate()
-	if err != nil {
-		log.Fatalf("[ERR] invalid configuration was supplied: file=%s, err=%s", cfg.ConfigFile(), err)
-	}
-
-	// File Tracker service to track uploaded files.
-	ft, err := leveldb.OpenFile(cfg.CompletedUploadsDBDir(), nil)
-	if err != nil {
-		log.Fatalf("[ERR] opening completed uploads tracker: path=%s, err=%s", cfg.CompletedUploadsDBDir(), err)
-	}
-	defer ft.Close()
-	fileTracker := completeduploads.NewService(completeduploads.NewLevelDBRepository(ft))
-
-	// Token Manager service to be used as secrets backend.
-	kr, err := tokenstore.NewKeyringRepository(cfg.SecretsBackendType, nil, cfg.KeyringDir())
-	if err != nil {
-		log.Fatalf("[ERR] opening token manager: type=%s, err=%s", cfg.SecretsBackendType, err)
-	}
-	tokenManager := tokenstore.NewService(kr)
+	persistentFlags := rootCmd.PersistentFlags()
+	globalFlags = flags.SetGlobalFlags(persistentFlags)
 
-	// Upload Session Tracker to keep upload session to resume uploads.
-	uploadTracker, err := leveldbstore.NewStore(cfg.ResumableUploadsDBDir())
-	if err != nil {
-		log.Fatalf("[ERR] opening resumable uploads tracker: path=%s, err=%s", cfg.ResumableUploadsDBDir(), err)
-	}
-	defer uploadTracker.Close()
-
-	// Initialize the App
-	app := &app.App{
-		FileTracker:   fileTracker,
-		TokenManager:  tokenManager,
-		UploadTracker: uploadTracker,
-
-		Log: log.New(os.Stderr, "", 0),
-	}
-
-	// start file upload worker
-	uploadChan, doneUploading := upload.StartFileUploadWorker(app.FileTracker)
-
-	deletionQueue := upload.NewDeletionQueue()
-	deletionQueue.StartWorkers()
-
-	ctx := context.Background()
-	// get OAuth2 Configuration with our App credentials
-	oauth2Config := oauth2.Config{
-		ClientID:     cfg.APIAppCredentials.ClientID,
-		ClientSecret: cfg.APIAppCredentials.ClientSecret,
-		Endpoint:     photos.Endpoint,
-		Scopes:       photos.Scopes,
-	}
-
-	// launch all folder upload jobs
-	for _, item := range cfg.Jobs {
-		c, err := newOAuth2Client(ctx, app.TokenManager, oauth2Config, item.Account)
-		if err != nil {
-			log.Fatal(err)
-		}
-		gPhotos, err := gphotos.NewClientWithResumableUploads(c, app.UploadTracker, gphotos.OptionLog(app.Log))
-		if err != nil {
-			log.Fatal(err)
-		}
-
-		opt := upload.NewJobOptions(item.MakeAlbums.Enabled, item.DeleteAfterUpload, item.UploadVideos, item.IncludePatterns, item.ExcludePatterns)
-		job := upload.NewFolderUploadJob(gPhotos, app.FileTracker, item.SourceFolder, opt)
-
-		if err := job.ScanFolder(uploadChan); err != nil {
-			log.Fatalf("Failed to upload folder %s: %v", item.SourceFolder, err)
-		}
-	}
-
-	// after we've run all the folder upload jobs we're done adding file upload jobs
-	close(uploadChan)
-	// wait for all the uploads to be completed
-	<-doneUploading
-	log.Println("all uploads done")
+	// Add main commands
+	rootCmd.AddCommand(NewVersionCmd())
+	rootCmd.AddCommand(NewInitCmd(globalFlags))
+	rootCmd.AddCommand(NewPushCmd(globalFlags))
+}
 
-	// after the last upload is done we're done queueing files for deletion
-	deletionQueue.Close()
-	// wait for deletions to be completed before exiting
-	deletionQueue.WaitForWorkers()
-	log.Println("all deletions done")
+// GetRoot returns the root command
+func GetRoot() *cobra.Command {
+	return rootCmd
 }
diff --git a/cmd/version.go b/cmd/version.go
index eaf1ea0..816cb48 100644
--- a/cmd/version.go
+++ b/cmd/version.go
@@ -23,16 +23,21 @@ var (
 	version = "v0.0.0" // git tag, output of $(git describe --tags --always --dirty)
 )
 
-// versionCmd returns the overall codebase version. It's for detecting what
-// code a binary was built from.
-var versionCmd = &cobra.Command{
-	Use:   "version",
-	Short: "Print current version",
-	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Printf("gphotos-uploader-cli %s\n", version)
-	},
+type VersionCmd struct {}
+
+func NewVersionCmd() *cobra.Command {
+	cmd := &VersionCmd{}
+
+	versionCmd := &cobra.Command{
+		Use:   "version",
+		Short: "Prints current version",
+		RunE:  cmd.Run,
+	}
+
+	return versionCmd
 }
 
-func init() {
-	rootCmd.AddCommand(versionCmd)
+func (cmd *VersionCmd) Run(command *cobra.Command, args []string) error {
+	_, err := fmt.Printf("gphotos-uploader-cli %s", version)
+	return err
 }
diff --git a/config/config.go b/config/config.go
index 82464d3..7b4e1f4 100644
--- a/config/config.go
+++ b/config/config.go
@@ -6,9 +6,9 @@ import (
 	"log"
 	"os"
 	"path"
+	"path/filepath"
 
 	"github.com/client9/xson/hjson"
-	"github.com/nmrshll/go-cp"
 
 	"github.com/gphotosuploader/gphotos-uploader-cli/utils/filesystem"
 )
@@ -39,7 +39,11 @@ func defaultSettings() *Config {
 // NewConfig returns a *Config with the default settings of the application.
 func NewConfig(dir string) *Config {
 	cfg := defaultSettings()
-	cfg.ConfigPath = filesystem.AbsolutePath(dir)
+	absPath, err := filesystem.AbsolutePath(dir)
+	if err != nil {
+		absPath = dir
+	}
+	cfg.ConfigPath = absPath
 
 	return cfg
 }
@@ -51,7 +55,7 @@ func (c *Config) Validate() error {
 
 	for i := range c.Jobs {
 		item := &c.Jobs[i] // we do that way to modify original object while iterating.
-		path, err := cp.AbsolutePath(item.SourceFolder)
+		path, err := filesystem.AbsolutePath(item.SourceFolder)
 		if err != nil {
 			return fmt.Errorf("invalid source folder. SourceFolder=%s, err=%s", item.SourceFolder, err)
 		}
@@ -138,35 +142,24 @@ func LoadConfig(dir string) (*Config, error) {
 	return cfg, nil
 }
 
-// InitConfig creates an example config file if it doesn't already exist.
-// If 'force' is set then we are going to remove config dir before creating it.
-func InitConfig(dir string, force bool) error {
+// InitConfigFile creates a config file with default settings.
+func InitConfigFile(dir string) error {
 	cfg := NewConfig(dir)
 
-	// if force, we should remove everything to start from the scratch.
-	if force {
-		err := os.RemoveAll(dir)
-		if err != nil {
-			return err
-		}
+	// Delete config & overwrite config
+	err := os.RemoveAll(dir)
+	if err != nil {
+		return err
 	}
 
-	if _, err := os.Stat(cfg.ConfigPath); !os.IsNotExist(err) {
-		// directory already exist and forced was not set
-		return fmt.Errorf("config directory already exists, use '--force' to overwrite: path=%s", cfg.ConfigPath)
-	} else {
-		err := os.MkdirAll(cfg.ConfigPath, 0755)
-		if err != nil {
-			return fmt.Errorf("failed to create config directory: path=%s, err=%v", cfg.ConfigPath, err)
-		}
+	err = os.MkdirAll(cfg.ConfigPath, 0755)
+	if err != nil {
+		return fmt.Errorf("failed to create config directory: path=%s, err=%v", cfg.ConfigPath, err)
 	}
 
-	fh, err := os.Open(cfg.ConfigFile())
+	fh, err := os.Create(cfg.ConfigFile())
 	if err != nil {
-		fh, err = os.Create(cfg.ConfigFile())
-		if err != nil {
-			return fmt.Errorf("failed to create config: file=%s, err=%v", cfg.ConfigFile(), err)
-		}
+		return fmt.Errorf("failed to create config: file=%s, err=%v", cfg.ConfigFile(), err)
 	}
 	defer func() {
 		if err := fh.Close(); err != nil {
@@ -181,3 +174,18 @@ func InitConfig(dir string, force bool) error {
 
 	return fh.Sync()
 }
+
+// ConfigExists checks if a gphotos-uplaoder-cli configuration exists at a certain path
+func ConfigExists(path string) bool {
+	// Check config.hjson
+	cfgFile, err := filesystem.AbsolutePath(filepath.Join(path, DefaultConfigsPath))
+	if err != nil {
+		return false
+	}
+
+	if _, err := os.Stat(cfgFile); err == nil {
+		return true
+	}
+
+	return false // Config file found
+}
diff --git a/config/constants.go b/config/constants.go
new file mode 100644
index 0000000..f9394cd
--- /dev/null
+++ b/config/constants.go
@@ -0,0 +1,7 @@
+package config
+
+// DefaultHomeGPhotosUploaderCLIFolder is the default .gphotos-uploader-cli home f	older where to store specific configurations
+const DefaultHomeGPhotosUploaderCLIFolder = ".gphotos-uploader-cli"
+
+// DefaultConfigsFilename is the default configs file name to use
+const DefaultConfigsPath = "config.hjson"
\ No newline at end of file
diff --git a/config/types.go b/config/schema.go
similarity index 100%
rename from config/types.go
rename to config/schema.go
diff --git a/go.mod b/go.mod
index d59944b..3f2ab37 100644
--- a/go.mod
+++ b/go.mod
@@ -8,10 +8,14 @@ require (
 	github.com/gphotosuploader/google-photos-api-client-go v1.1.2
 	github.com/gphotosuploader/googlemirror v0.3.2
 	github.com/int128/oauth2cli v1.7.0
+	github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
+	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
 	github.com/nmrshll/go-cp v0.0.0-20180115193924-61436d3b7cfa
 	github.com/pierrec/xxHash v0.1.5
 	github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
+	github.com/sirupsen/logrus v1.4.2
 	github.com/spf13/cobra v0.0.5
+	github.com/spf13/pflag v1.0.3
 	github.com/syndtr/goleveldb v1.0.0
 	golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
 	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
diff --git a/go.sum b/go.sum
index 79e4f5f..816a9c2 100644
--- a/go.sum
+++ b/go.sum
@@ -39,8 +39,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/gphotosuploader/google-photos-api-client-go v1.1.1 h1:n5/gCKp76iD094HmHLqAjcSfPh6roZ7PDYtrXcEbTz0=
-github.com/gphotosuploader/google-photos-api-client-go v1.1.1/go.mod h1:drQlpEncMykB/tPjsr0TKUKAfGKCOqg5Zl0XLuEopfA=
 github.com/gphotosuploader/google-photos-api-client-go v1.1.2 h1:11VDD+ZkxyxMphC/C8IBQsscOVwBf5MaHJZYCu2k1so=
 github.com/gphotosuploader/google-photos-api-client-go v1.1.2/go.mod h1:drQlpEncMykB/tPjsr0TKUKAfGKCOqg5Zl0XLuEopfA=
 github.com/gphotosuploader/googlemirror v0.3.2 h1:xaYVJYEDj2rYMYGi5sTGMy6gkD5PVKfz4c3ZW8gQs+M=
@@ -59,17 +57,25 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/int128/oauth2cli v1.7.0 h1:lguQEIJ4IcSFRTqQ6y7avnfvPqVe0U6dlkW8mC1Epts=
 github.com/int128/oauth2cli v1.7.0/go.mod h1:bucNn0/es9IhOf0a2MWPvJ5xO5f6JYrCfitQTyjI5lA=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
+github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
 github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM=
 github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
 github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@@ -91,6 +97,8 @@ github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -101,6 +109,7 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -148,6 +157,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
diff --git a/log/discard_logger.go b/log/discard_logger.go
new file mode 100644
index 0000000..f19aa3a
--- /dev/null
+++ b/log/discard_logger.go
@@ -0,0 +1,97 @@
+package log
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/sirupsen/logrus"
+)
+
+// DiscardLogger just discards every log statement
+type DiscardLogger struct {
+	PanicOnExit bool
+}
+
+// Debug implements logger interface
+func (d *DiscardLogger) Debug(args ...interface{}) {}
+
+// Debugf implements logger interface
+func (d *DiscardLogger) Debugf(format string, args ...interface{}) {}
+
+// Info implements logger interface
+func (d *DiscardLogger) Info(args ...interface{}) {}
+
+// Infof implements logger interface
+func (d *DiscardLogger) Infof(format string, args ...interface{}) {}
+
+// Warn implements logger interface
+func (d *DiscardLogger) Warn(args ...interface{}) {}
+
+// Warnf implements logger interface
+func (d *DiscardLogger) Warnf(format string, args ...interface{}) {}
+
+// Error implements logger interface
+func (d *DiscardLogger) Error(args ...interface{}) {}
+
+// Errorf implements logger interface
+func (d *DiscardLogger) Errorf(format string, args ...interface{}) {}
+
+// Fatal implements logger interface
+func (d *DiscardLogger) Fatal(args ...interface{}) {
+	if d.PanicOnExit {
+		d.Panic(args...)
+	}
+
+	os.Exit(1)
+}
+
+// Fatalf implements logger interface
+func (d *DiscardLogger) Fatalf(format string, args ...interface{}) {
+	if d.PanicOnExit {
+		d.Panicf(format, args...)
+	}
+
+	os.Exit(1)
+}
+
+// Panic implements logger interface
+func (d *DiscardLogger) Panic(args ...interface{}) {
+	panic(fmt.Sprint(args...))
+}
+
+// Panicf implements logger interface
+func (d *DiscardLogger) Panicf(format string, args ...interface{}) {
+	panic(fmt.Sprintf(format, args...))
+}
+
+// Done implements logger interface
+func (d *DiscardLogger) Done(args ...interface{}) {}
+
+// Donef implements logger interface
+func (d *DiscardLogger) Donef(format string, args ...interface{}) {}
+
+// Fail implements logger interface
+func (d *DiscardLogger) Fail(args ...interface{}) {}
+
+// Failf implements logger interface
+func (d *DiscardLogger) Failf(format string, args ...interface{}) {}
+
+// Print implements logger interface
+func (d *DiscardLogger) Print(level logrus.Level, args ...interface{}) {}
+
+// Printf implements logger interface
+func (d *DiscardLogger) Printf(level logrus.Level, format string, args ...interface{}) {}
+
+// SetLevel implements logger interface
+func (d *DiscardLogger) SetLevel(level logrus.Level) {}
+
+// GetLevel implements logger interface
+func (d *DiscardLogger) GetLevel() logrus.Level { return logrus.FatalLevel }
+
+// Write implements logger interface
+func (d *DiscardLogger) Write(message []byte) (int, error) {
+	return len(message), nil
+}
+
+// WriteString implements logger interface
+func (d *DiscardLogger) WriteString(message string) {}
diff --git a/log/doc.go b/log/doc.go
new file mode 100644
index 0000000..292c321
--- /dev/null
+++ b/log/doc.go
@@ -0,0 +1,4 @@
+/*
+Package log implements a simple logger using Logrus. It implements several levels of logging and several targets also.
+*/
+package log
diff --git a/log/file_logger.go b/log/file_logger.go
new file mode 100644
index 0000000..835eaa9
--- /dev/null
+++ b/log/file_logger.go
@@ -0,0 +1,159 @@
+package log
+
+import (
+	"os"
+	"sync"
+
+	"github.com/sirupsen/logrus"
+)
+
+// Logdir specifies the relative path to the gphotos-uploader-cli logs
+var Logdir = "./.gphotos-uploader-cli/logs/"
+
+var logs = map[string]Logger{}
+var logsMutext sync.Mutex
+
+type fileLogger struct {
+	logger *logrus.Logger
+}
+
+// GetFileLogger returns a logger instance for the specified filename
+func GetFileLogger(filename string) Logger {
+	logsMutext.Lock()
+	defer logsMutext.Unlock()
+
+	log := logs[filename]
+	if log == nil {
+		newLogger := &fileLogger{
+			logger: logrus.New(),
+		}
+		newLogger.logger.Formatter = &logrus.JSONFormatter{}
+
+		_ = os.MkdirAll(Logdir, os.ModePerm)
+
+		logFile, err := os.OpenFile(Logdir+filename+".log", os.O_APPEND|os.O_CREATE|os.O_RDWR, os.ModePerm)
+		if err != nil {
+			newLogger.Warnf("Unable to open " + filename + " log file. Will log to stdout.")
+		} else {
+			newLogger.logger.SetOutput(logFile)
+		}
+
+		logs[filename] = newLogger
+	}
+
+	return logs[filename]
+}
+
+func (f *fileLogger) Debug(args ...interface{}) {
+	f.logger.Debug(args...)
+}
+
+func (f *fileLogger) Debugf(format string, args ...interface{}) {
+	f.logger.Debugf(format, args...)
+}
+
+func (f *fileLogger) Info(args ...interface{}) {
+	f.logger.Info(args...)
+}
+
+func (f *fileLogger) Infof(format string, args ...interface{}) {
+	f.logger.Infof(format, args...)
+}
+
+func (f *fileLogger) Warn(args ...interface{}) {
+	f.logger.Warn(args...)
+}
+
+func (f *fileLogger) Warnf(format string, args ...interface{}) {
+	f.logger.Warnf(format, args...)
+}
+
+func (f *fileLogger) Error(args ...interface{}) {
+	f.logger.Error(args...)
+}
+
+func (f *fileLogger) Errorf(format string, args ...interface{}) {
+	f.logger.Errorf(format, args...)
+}
+
+func (f *fileLogger) Fatal(args ...interface{}) {
+	f.logger.Fatal(args...)
+}
+
+func (f *fileLogger) Fatalf(format string, args ...interface{}) {
+	f.logger.Fatalf(format, args...)
+}
+
+func (f *fileLogger) Panic(args ...interface{}) {
+	f.logger.Panic(args...)
+}
+
+func (f *fileLogger) Panicf(format string, args ...interface{}) {
+	f.logger.Panicf(format, args...)
+}
+
+func (f *fileLogger) Done(args ...interface{}) {
+	f.logger.Info(args...)
+}
+
+func (f *fileLogger) Donef(format string, args ...interface{}) {
+	f.logger.Infof(format, args...)
+}
+
+func (f *fileLogger) Fail(args ...interface{}) {
+	f.logger.Error(args...)
+}
+
+func (f *fileLogger) Failf(format string, args ...interface{}) {
+	f.logger.Errorf(format, args...)
+}
+
+func (f *fileLogger) Print(level logrus.Level, args ...interface{}) {
+	switch level {
+	case logrus.InfoLevel:
+		f.Info(args...)
+	case logrus.DebugLevel:
+		f.Debug(args...)
+	case logrus.WarnLevel:
+		f.Warn(args...)
+	case logrus.ErrorLevel:
+		f.Error(args...)
+	case logrus.PanicLevel:
+		f.Panic(args...)
+	case logrus.FatalLevel:
+		f.Fatal(args...)
+	}
+}
+
+func (f *fileLogger) Printf(level logrus.Level, format string, args ...interface{}) {
+	switch level {
+	case logrus.InfoLevel:
+		f.Infof(format, args...)
+	case logrus.DebugLevel:
+		f.Debugf(format, args...)
+	case logrus.WarnLevel:
+		f.Warnf(format, args...)
+	case logrus.ErrorLevel:
+		f.Errorf(format, args...)
+	case logrus.PanicLevel:
+		f.Panicf(format, args...)
+	case logrus.FatalLevel:
+		f.Fatalf(format, args...)
+	}
+}
+
+func (f *fileLogger) SetLevel(level logrus.Level) {
+	f.logger.SetLevel(level)
+}
+
+func (f *fileLogger) GetLevel() logrus.Level {
+	return f.logger.GetLevel()
+}
+
+func (f *fileLogger) Write(message []byte) (int, error) {
+	return f.logger.Out.Write(message)
+}
+
+func (f *fileLogger) WriteString(message string) {
+	_, _ = f.logger.Out.Write([]byte(message))
+}
diff --git a/log/log.go b/log/log.go
new file mode 100644
index 0000000..c898718
--- /dev/null
+++ b/log/log.go
@@ -0,0 +1,141 @@
+package log
+
+import (
+	"github.com/mgutz/ansi"
+	"github.com/sirupsen/logrus"
+)
+
+var defaultLog Logger = &stdoutLogger{
+	level: logrus.DebugLevel,
+}
+
+// Discard is a logger implementation that just discards every log statement
+var Discard = &DiscardLogger{}
+
+// Debug prints debug information
+func Debug(args ...interface{}) {
+	defaultLog.Debug(args...)
+}
+
+// Debugf prints formatted debug information
+func Debugf(format string, args ...interface{}) {
+	defaultLog.Debugf(format, args...)
+}
+
+// Info prints info information
+func Info(args ...interface{}) {
+	defaultLog.Info(args...)
+}
+
+// Infof prints formatted information
+func Infof(format string, args ...interface{}) {
+	defaultLog.Infof(format, args...)
+}
+
+// Warn prints warning information
+func Warn(args ...interface{}) {
+	defaultLog.Warn(args...)
+}
+
+// Warnf prints formatted warning information
+func Warnf(format string, args ...interface{}) {
+	defaultLog.Warnf(format, args...)
+}
+
+// Error prints error information
+func Error(args ...interface{}) {
+	defaultLog.Error(args...)
+}
+
+// Errorf prints formatted error information
+func Errorf(format string, args ...interface{}) {
+	defaultLog.Errorf(format, args...)
+}
+
+// Fatal prints fatal error information
+func Fatal(args ...interface{}) {
+	defaultLog.Fatal(args...)
+}
+
+// Fatalf prints formatted fatal error information
+func Fatalf(format string, args ...interface{}) {
+	defaultLog.Fatalf(format, args...)
+}
+
+// Panic prints panic information
+func Panic(args ...interface{}) {
+	defaultLog.Panic(args...)
+}
+
+// Panicf prints formatted panic information
+func Panicf(format string, args ...interface{}) {
+	defaultLog.Panicf(format, args...)
+}
+
+// Done prints done information
+func Done(args ...interface{}) {
+	defaultLog.Done(args...)
+}
+
+// Donef prints formatted info information
+func Donef(format string, args ...interface{}) {
+	defaultLog.Donef(format, args...)
+}
+
+// Fail prints error information
+func Fail(args ...interface{}) {
+	defaultLog.Fail(args...)
+}
+
+// Failf prints formatted error information
+func Failf(format string, args ...interface{}) {
+	defaultLog.Failf(format, args...)
+}
+
+// Print prints information
+func Print(level logrus.Level, args ...interface{}) {
+	defaultLog.Print(level, args...)
+}
+
+// Printf prints formatted information
+func Printf(level logrus.Level, format string, args ...interface{}) {
+	defaultLog.Printf(level, format, args...)
+}
+
+// SetLevel changes the log level of the global logger
+func SetLevel(level logrus.Level) {
+	defaultLog.SetLevel(level)
+}
+
+// StartFileLogging logs the output of the global logger to the file default.log
+func StartFileLogging() {
+	defaultLogStdout, ok := defaultLog.(*stdoutLogger)
+	if ok {
+		defaultLogStdout.fileLogger = GetFileLogger("default")
+	}
+}
+
+// GetInstance returns the Logger instance
+func GetInstance() Logger {
+	return defaultLog
+}
+
+// SetInstance sets the default logger instance
+func SetInstance(logger Logger) {
+	defaultLog = logger
+}
+
+// WriteColored writes a message in color
+func WriteColored(message string, color string) {
+	_, _ = defaultLog.Write([]byte(ansi.Color(message, color)))
+}
+
+// Write writes to the stdout log without formatting the message, but takes care of locking the log and halting a possible wait message
+func Write(message []byte) {
+	_, _ = defaultLog.Write(message)
+}
+
+// WriteString writes to the stdout log without formatting the message, but takes care of locking the log and halting a possible wait message
+func WriteString(message string) {
+	defaultLog.WriteString(message)
+}
diff --git a/log/logger.go b/log/logger.go
new file mode 100644
index 0000000..e472935
--- /dev/null
+++ b/log/logger.go
@@ -0,0 +1,55 @@
+package log
+
+import (
+	"github.com/sirupsen/logrus"
+)
+
+// Level type
+type logFunctionType uint32
+
+const (
+	panicFn logFunctionType = iota
+	fatalFn
+	errorFn
+	warnFn
+	infoFn
+	debugFn
+	failFn
+	doneFn
+)
+
+// Logger defines the common logging interface
+type Logger interface {
+	Debug(args ...interface{})
+	Debugf(format string, args ...interface{})
+
+	Info(args ...interface{})
+	Infof(format string, args ...interface{})
+
+	Warn(args ...interface{})
+	Warnf(format string, args ...interface{})
+
+	Error(args ...interface{})
+	Errorf(format string, args ...interface{})
+
+	Fatal(args ...interface{})
+	Fatalf(format string, args ...interface{})
+
+	Panic(args ...interface{})
+	Panicf(format string, args ...interface{})
+
+	Done(args ...interface{})
+	Donef(format string, args ...interface{})
+
+	Fail(args ...interface{})
+	Failf(format string, args ...interface{})
+
+	Print(level logrus.Level, args ...interface{})
+	Printf(level logrus.Level, format string, args ...interface{})
+
+	Write(message []byte) (int, error)
+	WriteString(message string)
+
+	SetLevel(level logrus.Level)
+	GetLevel() logrus.Level
+}
diff --git a/log/stdout_logger.go b/log/stdout_logger.go
new file mode 100644
index 0000000..9f7a734
--- /dev/null
+++ b/log/stdout_logger.go
@@ -0,0 +1,357 @@
+package log
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"sync"
+
+	goansi "github.com/k0kubun/go-ansi"
+	"github.com/mgutz/ansi"
+	"github.com/sirupsen/logrus"
+)
+
+// Use of go-ansi library to allow ANSI Colors on Windows
+var stdout = goansi.NewAnsiStdout()
+var stderr = goansi.NewAnsiStderr()
+
+type stdoutLogger struct {
+	logMutex sync.Mutex
+	level    logrus.Level
+
+	fileLogger Logger
+}
+
+type fnTypeInformation struct {
+	tag      string
+	color    string
+	logLevel logrus.Level
+	stream   io.Writer
+}
+
+var fnTypeInformationMap = map[logFunctionType]*fnTypeInformation{
+	debugFn: {
+		tag:      "[debug]  ",
+		color:    "green+b",
+		logLevel: logrus.DebugLevel,
+		stream:   stdout,
+	},
+	infoFn: {
+		tag:      "[info]   ",
+		color:    "cyan+b",
+		logLevel: logrus.InfoLevel,
+		stream:   stdout,
+	},
+	warnFn: {
+		tag:      "[warn]   ",
+		color:    "red+b",
+		logLevel: logrus.WarnLevel,
+		stream:   stdout,
+	},
+	errorFn: {
+		tag:      "[error]  ",
+		color:    "red+b",
+		logLevel: logrus.ErrorLevel,
+		stream:   stdout,
+	},
+	fatalFn: {
+		tag:      "[fatal]  ",
+		color:    "red+b",
+		logLevel: logrus.FatalLevel,
+		stream:   stdout,
+	},
+	panicFn: {
+		tag:      "[panic]  ",
+		color:    "red+b",
+		logLevel: logrus.PanicLevel,
+		stream:   stderr,
+	},
+	doneFn: {
+		tag:      "[done] √ ",
+		color:    "green+b",
+		logLevel: logrus.InfoLevel,
+		stream:   stdout,
+	},
+	failFn: {
+		tag:      "[fail] X ",
+		color:    "red+b",
+		logLevel: logrus.ErrorLevel,
+		stream:   stdout,
+	},
+}
+
+func (s *stdoutLogger) writeMessage(fnType logFunctionType, message string) {
+	fnInformation := fnTypeInformationMap[fnType]
+	if s.level >= fnInformation.logLevel {
+		_, _ = fnInformation.stream.Write([]byte(ansi.Color(fnInformation.tag, fnInformation.color)))
+		_, _ = fnInformation.stream.Write([]byte(message))
+	}
+}
+
+func (s *stdoutLogger) writeMessageToFileLogger(fnType logFunctionType, args ...interface{}) {
+	fnInformation := fnTypeInformationMap[fnType]
+
+	if s.level >= fnInformation.logLevel && s.fileLogger != nil {
+		switch fnType {
+		case doneFn:
+			s.fileLogger.Done(args...)
+		case infoFn:
+			s.fileLogger.Info(args...)
+		case debugFn:
+			s.fileLogger.Debug(args...)
+		case warnFn:
+			s.fileLogger.Warn(args...)
+		case failFn:
+			s.fileLogger.Fail(args...)
+		case errorFn:
+			s.fileLogger.Error(args...)
+		case panicFn:
+			s.fileLogger.Panic(args...)
+		case fatalFn:
+			s.fileLogger.Fatal(args...)
+		}
+	}
+}
+
+func (s *stdoutLogger) writeMessageToFileLoggerf(fnType logFunctionType, format string, args ...interface{}) {
+	fnInformation := fnTypeInformationMap[fnType]
+
+	if s.level >= fnInformation.logLevel && s.fileLogger != nil {
+		switch fnType {
+		case doneFn:
+			s.fileLogger.Donef(format, args...)
+		case infoFn:
+			s.fileLogger.Infof(format, args...)
+		case debugFn:
+			s.fileLogger.Debugf(format, args...)
+		case warnFn:
+			s.fileLogger.Warnf(format, args...)
+		case failFn:
+			s.fileLogger.Failf(format, args...)
+		case errorFn:
+			s.fileLogger.Errorf(format, args...)
+		case panicFn:
+			s.fileLogger.Panicf(format, args...)
+		case fatalFn:
+			s.fileLogger.Fatalf(format, args...)
+		}
+	}
+}
+
+func (s *stdoutLogger) Debug(args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(debugFn, fmt.Sprintln(args...))
+	s.writeMessageToFileLogger(debugFn, args...)
+}
+
+func (s *stdoutLogger) Debugf(format string, args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(debugFn, fmt.Sprintf(format, args...)+"\n")
+	s.writeMessageToFileLoggerf(debugFn, format, args...)
+}
+
+func (s *stdoutLogger) Info(args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(infoFn, fmt.Sprintln(args...))
+	s.writeMessageToFileLogger(infoFn, args...)
+}
+
+func (s *stdoutLogger) Infof(format string, args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(infoFn, fmt.Sprintf(format, args...)+"\n")
+	s.writeMessageToFileLoggerf(infoFn, format, args...)
+}
+
+func (s *stdoutLogger) Warn(args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(warnFn, fmt.Sprintln(args...))
+	s.writeMessageToFileLogger(warnFn, args...)
+}
+
+func (s *stdoutLogger) Warnf(format string, args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(warnFn, fmt.Sprintf(format, args...)+"\n")
+	s.writeMessageToFileLoggerf(warnFn, format, args...)
+}
+
+func (s *stdoutLogger) Error(args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(errorFn, fmt.Sprintln(args...))
+	s.writeMessageToFileLogger(errorFn, args...)
+}
+
+func (s *stdoutLogger) Errorf(format string, args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(errorFn, fmt.Sprintf(format, args...)+"\n")
+	s.writeMessageToFileLoggerf(errorFn, format, args...)
+}
+
+func (s *stdoutLogger) Fatal(args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	msg := fmt.Sprintln(args...)
+
+	s.writeMessage(fatalFn, msg)
+	s.writeMessageToFileLogger(fatalFn, args...)
+
+	if s.fileLogger == nil {
+		os.Exit(1)
+	}
+}
+
+func (s *stdoutLogger) Fatalf(format string, args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	msg := fmt.Sprintf(format, args...)
+
+	s.writeMessage(fatalFn, msg+"\n")
+	s.writeMessageToFileLoggerf(fatalFn, format, args...)
+
+	if s.fileLogger == nil {
+		os.Exit(1)
+	}
+}
+
+func (s *stdoutLogger) Panic(args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(panicFn, fmt.Sprintln(args...))
+	s.writeMessageToFileLogger(panicFn, args...)
+
+	if s.fileLogger == nil {
+		panic(fmt.Sprintln(args...))
+	}
+}
+
+func (s *stdoutLogger) Panicf(format string, args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(panicFn, fmt.Sprintf(format, args...)+"\n")
+	s.writeMessageToFileLoggerf(panicFn, format, args...)
+
+	if s.fileLogger == nil {
+		panic(fmt.Sprintf(format, args...))
+	}
+}
+
+func (s *stdoutLogger) Done(args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(doneFn, fmt.Sprintln(args...))
+	s.writeMessageToFileLogger(doneFn, args...)
+
+}
+
+func (s *stdoutLogger) Donef(format string, args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(doneFn, fmt.Sprintf(format, args...)+"\n")
+	s.writeMessageToFileLoggerf(doneFn, format, args...)
+}
+
+func (s *stdoutLogger) Fail(args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(failFn, fmt.Sprintln(args...))
+	s.writeMessageToFileLogger(failFn, args...)
+}
+
+func (s *stdoutLogger) Failf(format string, args ...interface{}) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.writeMessage(failFn, fmt.Sprintf(format, args...)+"\n")
+	s.writeMessageToFileLoggerf(failFn, format, args...)
+}
+
+func (s *stdoutLogger) Print(level logrus.Level, args ...interface{}) {
+	switch level {
+	case logrus.InfoLevel:
+		s.Info(args...)
+	case logrus.DebugLevel:
+		s.Debug(args...)
+	case logrus.WarnLevel:
+		s.Warn(args...)
+	case logrus.ErrorLevel:
+		s.Error(args...)
+	case logrus.PanicLevel:
+		s.Panic(args...)
+	case logrus.FatalLevel:
+		s.Fatal(args...)
+	}
+}
+
+func (s *stdoutLogger) Printf(level logrus.Level, format string, args ...interface{}) {
+	switch level {
+	case logrus.InfoLevel:
+		s.Infof(format, args...)
+	case logrus.DebugLevel:
+		s.Debugf(format, args...)
+	case logrus.WarnLevel:
+		s.Warnf(format, args...)
+	case logrus.ErrorLevel:
+		s.Errorf(format, args...)
+	case logrus.PanicLevel:
+		s.Panicf(format, args...)
+	case logrus.FatalLevel:
+		s.Fatalf(format, args...)
+	}
+}
+
+func (s *stdoutLogger) SetLevel(level logrus.Level) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	s.level = level
+}
+
+func (s *stdoutLogger) GetLevel() logrus.Level {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	return s.level
+}
+
+func (s *stdoutLogger) Write(message []byte) (int, error) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	if s.level >= logrus.InfoLevel {
+		n, err := fnTypeInformationMap[infoFn].stream.Write(message)
+		return n, err
+	}
+
+	return len(message), nil
+}
+
+func (s *stdoutLogger) WriteString(message string) {
+	s.logMutex.Lock()
+	defer s.logMutex.Unlock()
+
+	if s.level >= logrus.InfoLevel {
+		_, _ = fnTypeInformationMap[infoFn].stream.Write([]byte(message))
+	}
+}
diff --git a/main.go b/main.go
index dc71e08..2736fe0 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,12 @@
 package main
 
 import (
+	"os"
+
 	"github.com/gphotosuploader/gphotos-uploader-cli/cmd"
 )
 
 func main() {
 	cmd.Execute()
+	os.Exit(0)
 }
diff --git a/upload/deletion.go b/upload/deletion.go
index 1e4673a..35899ea 100644
--- a/upload/deletion.go
+++ b/upload/deletion.go
@@ -2,8 +2,9 @@ package upload
 
 import (
 	"errors"
-	"log"
 	"os"
+
+	"github.com/gphotosuploader/gphotos-uploader-cli/log"
 )
 
 var (
@@ -50,13 +51,13 @@ func NewDeletionQueue() *DeletionQueue {
 }
 
 // StartWorkers start concurrent deletion workers.
-func (q *DeletionQueue) StartWorkers() {
+func (q *DeletionQueue) StartWorkers(log log.Logger) {
 	go func() {
 		for job := range q.Requests {
-			log.Printf("Processing deletion request: file=%s", job.ObjectPath)
+			log.Debugf("Processing deletion request: file=%s", job.ObjectPath)
 			err := job.deleteIfCorrectlyUploaded()
 			if err != nil {
-				log.Printf("Deletion request failed: file=%s, err=%v\n", job.ObjectPath, err)
+				log.Errorf("Deletion request failed: file=%s, err=%v\n", job.ObjectPath, err)
 			}
 		}
 		q.DoneChannel <- true
diff --git a/upload/fileUpload.go b/upload/fileUpload.go
index 2cc302c..07b16c4 100644
--- a/upload/fileUpload.go
+++ b/upload/fileUpload.go
@@ -2,11 +2,11 @@ package upload
 
 import (
 	"context"
-	"log"
 
 	gphotos "github.com/gphotosuploader/google-photos-api-client-go/lib-gphotos"
 
 	"github.com/gphotosuploader/gphotos-uploader-cli/app"
+	"github.com/gphotosuploader/gphotos-uploader-cli/log"
 )
 
 // number of concurrent workers uploading items
@@ -27,17 +27,17 @@ type Item struct {
 //  eg: https://gobyexample.com/worker-pools
 //  eg: https://gobyexample.com/waitgroups
 //  eg: https://github.schibsted.io/spt-infrastructure/yams-delivery-images/blob/master/images/image_gif.go
-func concurrentUpload(fileUploadsChan <-chan *Item, doneUploading chan<- bool, fileTracker app.FileTracker) {
+func concurrentUpload(fileUploadsChan <-chan *Item, doneUploading chan<- bool, fileTracker app.FileTracker, log log.Logger) {
 	semaphore := make(chan bool, maxNumberOfWorkers)
 	for item := range fileUploadsChan {
 		semaphore <- true
 		go func(item *Item) {
 			defer func() { <-semaphore }()
 			ctx := context.TODO()
-			log.Printf("Uploading object: file=%s", item.path)
+			log.Debugf("Uploading object: file=%s", item.path)
 			err := item.process(ctx, fileTracker)
 			if err != nil {
-				log.Printf("[ERR] Failed to process media item: file=%s, err=%s", item.path, err)
+				log.Errorf("Failed to process media item: file=%s, err=%s", item.path, err)
 				return
 			}
 		}(item)
@@ -52,10 +52,10 @@ func concurrentUpload(fileUploadsChan <-chan *Item, doneUploading chan<- bool, f
 // StartFileUploadWorker set up channels and start concurrentUpload
 // fileUploadsChan will receive Item structs and upload them
 // will signal doneUploading when fileUploadsChan is done
-func StartFileUploadWorker(fileTracker app.FileTracker) (fileUploadsChan chan *Item, doneUploading chan bool) {
+func StartFileUploadWorker(fileTracker app.FileTracker, log log.Logger) (fileUploadsChan chan *Item, doneUploading chan bool) {
 	doneUploading = make(chan bool)
 	fileUploadsChan = make(chan *Item)
-	go concurrentUpload(fileUploadsChan, doneUploading, fileTracker)
+	go concurrentUpload(fileUploadsChan, doneUploading, fileTracker, log)
 	return fileUploadsChan, doneUploading
 }
 
@@ -67,7 +67,7 @@ func (f *Item) process(ctx context.Context, ft app.FileTracker) error {
 
 	err = ft.CacheAsAlreadyUploaded(f.path)
 	if err != nil {
-		log.Printf("Tracking file as uploaded failed: file=%s, error=%v", f.path, err)
+		log.Warnf("Tracking file as uploaded failed: file=%s, error=%v", f.path, err)
 	}
 
 	if f.deleteOnSuccess {
diff --git a/upload/folderUploadJob.go b/upload/folderUploadJob.go
index e4fb361..9eba2a3 100644
--- a/upload/folderUploadJob.go
+++ b/upload/folderUploadJob.go
@@ -2,13 +2,13 @@ package upload
 
 import (
 	"fmt"
-	"log"
 	"os"
 	"path/filepath"
 
 	gphotos "github.com/gphotosuploader/google-photos-api-client-go/lib-gphotos"
 
 	"github.com/gphotosuploader/gphotos-uploader-cli/app"
+	"github.com/gphotosuploader/gphotos-uploader-cli/log"
 	"github.com/gphotosuploader/gphotos-uploader-cli/utils/filesystem"
 )
 
@@ -53,7 +53,7 @@ func NewFolderUploadJob(gPhotos *gphotos.Client, fileTracker app.FileTracker, fp
 }
 
 // ScanFolder uploads folder
-func (job *Job) ScanFolder(uploadChan chan<- *Item) error {
+func (job *Job) ScanFolder(uploadChan chan<- *Item, log log.Logger) error {
 	var err error
 
 	if !filesystem.IsDir(job.sourceFolder) {
@@ -90,9 +90,9 @@ func (job *Job) ScanFolder(uploadChan chan<- *Item) error {
 		// check completed uploads db for previous uploads
 		isAlreadyUploaded, err := job.fileTracker.IsAlreadyUploaded(fp)
 		if err != nil {
-			log.Println(err)
+			log.Error(err)
 		} else if isAlreadyUploaded {
-			log.Printf("already uploaded: %s: skipping file...\n", fp)
+			log.Debugf("Already uploaded: %s: skipping file...", fp)
 			return nil
 		}
 
@@ -100,7 +100,7 @@ func (job *Job) ScanFolder(uploadChan chan<- *Item) error {
 		var albumID string
 		if job.options.createAlbum {
 			name := filepath.Base(filepath.Dir(fp))
-			albumID = getGooglePhotosAlbumID(name, job.gPhotos)
+			albumID = getGooglePhotosAlbumID(name, job.gPhotos, log)
 		}
 
 		// set file upload options depending on folder upload options
@@ -117,7 +117,7 @@ func (job *Job) ScanFolder(uploadChan chan<- *Item) error {
 		return nil
 	})
 	if errW != nil {
-		log.Printf("walk error [%v]", errW)
+		log.Errorf("walk error err=%s", errW)
 	}
 
 	return nil
@@ -125,14 +125,14 @@ func (job *Job) ScanFolder(uploadChan chan<- *Item) error {
 
 // getGooglePhotosAlbumID returns the ID of an album with the specified name.
 // If the album didn't exist, it's created thanks to GetOrCreateAlbumByName().
-func getGooglePhotosAlbumID(name string, c *gphotos.Client) string {
+func getGooglePhotosAlbumID(name string, c *gphotos.Client, log log.Logger) string {
 	if name == "" {
 		return ""
 	}
 
 	album, err := c.GetOrCreateAlbumByName(name)
 	if err != nil {
-		log.Printf("Album creation failed: name=%s, error=%v", name, err)
+		log.Warnf("Album creation failed: name=%s, error=%v", name, err)
 		return ""
 	}
 	return album.Id
diff --git a/utils/filesystem/filesystem.go b/utils/filesystem/filesystem.go
index db38394..5851cc3 100644
--- a/utils/filesystem/filesystem.go
+++ b/utils/filesystem/filesystem.go
@@ -1,7 +1,6 @@
 package filesystem
 
 import (
-	"log"
 	"os"
 	"os/user"
 	"path/filepath"
@@ -11,28 +10,22 @@ import (
 
 // AbsolutePath converts a path (relative or absolute) into an absolute one.
 // Supports '~' notation for $HOME directory of the current user.
-func AbsolutePath(path string) string {
+func AbsolutePath(path string) (string, error) {
 	usr, err := user.Current()
 	if err != nil {
-		// It's very strange to have an error here, but in any case,
-		// return the same path was given.
-		return path
+		return "", err
 	}
 	dir := usr.HomeDir
 
 	if path == "~" {
 		// In case of "~", which won't be caught by the "else if"
-		return dir
+		return dir, nil
 	} else if strings.HasPrefix(path, "~/") {
 		// Use strings.HasPrefix so we don't match paths like
 		// "/something/~/something/"
-		return filepath.Join(dir, path[2:])
+		return filepath.Join(dir, path[2:]), nil
 	}
-	abspath, err := filepath.Abs(path)
-	if err != nil {
-		log.Println(err)
-	}
-	return abspath
+	return filepath.Abs(path)
 }
 
 // IsFile asserts there is a file at path


Test patch:
diff --git a/cmd/version_test.go b/cmd/version_test.go
new file mode 100644
index 0000000..cf27548
--- /dev/null
+++ b/cmd/version_test.go
@@ -0,0 +1,7 @@
+package cmd
+
+func ExampleVersionCmd_Run() {
+	cmd := &VersionCmd{}
+	_ = cmd.Run(nil, nil)
+	// Output: gphotos-uploader-cli v0.0.0
+}
diff --git a/config/config_test.go b/config/config_test.go
index b4acd37..726f509 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -15,25 +15,17 @@ func TestInitConfig(t *testing.T) {
 	dir := filepath.Join(os.TempDir(), fmt.Sprintf("gphotos-config.%d", time.Now().UnixNano()))
 
 	t.Run("TestInitConfigFile", func(t *testing.T) {
-		err := config.InitConfig(dir, true)
+		err := config.InitConfigFile(dir)
 		if err != nil {
 			t.Errorf("could not create init config file: %v", err)
 		}
 	})
-
 	defer func() {
 		err := os.RemoveAll(dir)
 		if err != nil {
 			t.Errorf("could not remove test config file (dir: %s): %v", dir, err)
 		}
 	}()
-
-	t.Run("TestInitConfigFileWithExistentFile", func(t *testing.T) {
-		err := config.InitConfig(dir, false)
-		if err == nil {
-			t.Error("an error creating an existent file was expected")
-		}
-	})
 }
 
 func TestInitAndLoadConfig(t *testing.T) {
@@ -41,12 +33,11 @@ func TestInitAndLoadConfig(t *testing.T) {
 	dir := filepath.Join(os.TempDir(), fmt.Sprintf("gphotos-config.%d", time.Now().UnixNano()))
 
 	t.Run("TestInitConfigFile", func(t *testing.T) {
-		err := config.InitConfig(dir, true)
+		err := config.InitConfigFile(dir)
 		if err != nil {
 			t.Errorf("could not create init config file: %v", err)
 		}
 	})
-
 	defer func() {
 		err := os.RemoveAll(dir)
 		if err != nil {
@@ -189,3 +180,46 @@ func createTestConfiguration() *config.Config {
 	c.Jobs = append(c.Jobs, job)
 	return c
 }
+
+func TestConfigExists(t *testing.T) {
+	dir := filepath.Join(os.TempDir(), fmt.Sprintf("gphotos-test.%d", time.Now().UnixNano()))
+	if err := os.RemoveAll(dir); err != nil {
+		t.Fatalf("no error was expected at this point: err=%s", err)
+	}
+
+	t.Run("TestNonExistingConfiguration", func(t *testing.T) {
+		if config.ConfigExists(dir) {
+			t.Errorf("config file exists. That was not expected: dir=%s", dir)
+		}
+	})
+
+	if err := os.MkdirAll(dir, os.ModePerm); err != nil {
+		t.Fatalf("no error was expected at this point: err=%s", err)
+	}
+	defer func() {
+		if err := os.RemoveAll(dir); err != nil {
+			t.Fatalf("could not remove test config folder: path=%s err=%s", dir, err)
+		}
+	}()
+
+	cfgFile := filepath.Join(dir, "config.hjson")
+	fh, err := os.Create(cfgFile)
+	if err != nil {
+		t.Fatalf("failed to create config: file=%s, err=%v", cfgFile, err)
+	}
+	defer func() {
+		if err := fh.Close(); err != nil {
+			t.Fatal(err)
+		}
+	}()
+
+	if _, err = fh.WriteString("testTest"); err != nil {
+		t.Fatalf("failed to write configuration: file=%s, err=%v", cfgFile, err)
+	}
+
+	t.Run("TestExistingConfiguration", func(t *testing.T) {
+		if !config.ConfigExists(dir) {
+			t.Errorf("config file doesn't exist. That was not expected: dir=%s", dir)
+		}
+	})
+}
diff --git a/utils/filesystem/filesystem_test.go b/utils/filesystem/filesystem_test.go
index 770f527..c0ad535 100644
--- a/utils/filesystem/filesystem_test.go
+++ b/utils/filesystem/filesystem_test.go
@@ -27,7 +27,7 @@ func TestAbsolutePath(t *testing.T) {
 		}
 
 		for _, test := range absolutePathInputs {
-			got := filesystem.AbsolutePath(test.in)
+			got, _ := filesystem.AbsolutePath(test.in)
 			if got != test.out {
 				t.Errorf("failed for '%s': expected '%v', got '%v'", test.in, test.out, got)
 			}
@@ -55,7 +55,7 @@ func TestAbsolutePath(t *testing.T) {
 		}
 
 		for _, test := range relativePathInputs {
-			got := filesystem.AbsolutePath(test.in)
+			got, _ := filesystem.AbsolutePath(test.in)
 			expected := path.Join(cwd, test.out)
 
 			if got != expected {
@@ -87,7 +87,7 @@ func TestAbsolutePath(t *testing.T) {
 		dir := usr.HomeDir
 
 		for _, test := range tildePathInputs {
-			got := filesystem.AbsolutePath(test.in)
+			got, _ := filesystem.AbsolutePath(test.in)
 			expected := path.Join(dir, test.out)
 
 			if got != expected {
@@ -104,6 +104,7 @@ func TestIsFile(t *testing.T) {
 	}{
 		{"testdata/file.txt", true},
 		{"testdata/folder", false},
+		{"testdata/non-existent-file", false},
 	}
 
 	for _, test := range objectsTest {
@@ -121,6 +122,7 @@ func TestIsDir(t *testing.T) {
 	}{
 		{"testdata/file.txt", false},
 		{"testdata/folder", true},
+		{"testdata/non-existent-dir", false},
 	}
 
 	for _, test := range objectsTest {