From c511fa1fd6afcdf41f1500f8a0fb1689f9746982 Mon Sep 17 00:00:00 2001 From: Arctic Ice Studio Date: Mon, 15 Jul 2019 11:00:42 +0200 Subject: [PATCH] `Clean` task runner API implementation Implemented the `snowblock.TaskRunner` API interface to handle `clean` tasks from the original Python implementation (1). References: (1) https://github.com/arcticicestudio/snowsaw/blob/3e3840824bf6f3d5cc09573b9505737473c7ed95/README.md#clean Epic: GH-33 Resolves GH-75 --- pkg/config/constants.go | 2 + pkg/snowblock/task/clean/clean.go | 213 ++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 pkg/snowblock/task/clean/clean.go diff --git a/pkg/config/constants.go b/pkg/config/constants.go index dbfd3c4..d6c66dc 100644 --- a/pkg/config/constants.go +++ b/pkg/config/constants.go @@ -15,6 +15,7 @@ import ( "github.com/arcticicestudio/snowsaw/pkg/api/snowblock" "github.com/arcticicestudio/snowsaw/pkg/config/source/file" "github.com/arcticicestudio/snowsaw/pkg/snowblock/task" + "github.com/arcticicestudio/snowsaw/pkg/snowblock/task/clean" "github.com/arcticicestudio/snowsaw/pkg/snowblock/task/link" ) @@ -40,6 +41,7 @@ var ( AppConfigPaths []*file.File availableTaskRunner = []snowblock.TaskRunner{ + &clean.Clean{}, &link.Link{}, } diff --git a/pkg/snowblock/task/clean/clean.go b/pkg/snowblock/task/clean/clean.go new file mode 100644 index 0000000..203b642 --- /dev/null +++ b/pkg/snowblock/task/clean/clean.go @@ -0,0 +1,213 @@ +// Copyright (C) 2017-present Arctic Ice Studio +// Copyright (C) 2017-present Sven Greb +// +// Project: snowsaw +// Repository: https://github.com/arcticicestudio/snowsaw +// License: MIT + +// Author: Arctic Ice Studio +// Author: Sven Greb +// Since: 0.4.0 + +// Package clean provides a task runner implementation check for broken symbolic links and automatically remove them if +// they point to the snowblock directory. +package clean + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/fatih/color" + + "github.com/arcticicestudio/snowsaw/pkg/api/snowblock" + "github.com/arcticicestudio/snowsaw/pkg/prt" + "github.com/arcticicestudio/snowsaw/pkg/util/filesystem" +) + +// Clean is a task runner check for broken symbolic links and automatically remove them if they point to the snowblock +// directory. +type Clean struct { + absPaths []string + targets []*target + snowblockAbsPath string +} + +type target struct { + absPath string + isSymlink bool + nodeInfo os.FileInfo + path string +} + +// GetTaskName returns the name of the task this runner can process. +func (c Clean) GetTaskName() string { + return "clean" +} + +// Run processes a task using the given task instructions. +// The snowblockAbsPath parameter is the absolute path of the snowblock used as contextual information. +func (c *Clean) Run(configuration snowblock.TaskConfiguration, snowblockAbsPath string) error { + c.snowblockAbsPath = snowblockAbsPath + + // Try to assert the type of the given task configurations and process the paths if all values are converted + // successfully. + switch configType := configuration.(type) { + // Handle the only support JSON data structure of type `array` that stores `string` values by converting + // the given values to strings. + case []interface{}: + c.absPaths = []string{} + c.targets = []*target{} + for _, value := range configType { + path, converted := value.(string) + if !converted { + prt.Debugf("Invalid clean configuration value %s of type %s", + color.CyanString("%s", value), color.RedString("%T", value)) + return fmt.Errorf("invalid clean configuration value: %s", color.RedString("%s", value)) + } + // Expand environment variables and special characters in the target paths,... + expPath, expPathErr := filesystem.ExpandPath(path) + if expPathErr != nil { + return fmt.Errorf("could not expand target path %s: %v", color.CyanString(path), expPathErr) + } + var absPath string + // ...ensure relative paths are dissolved from to absolute paths... + if !filepath.IsAbs(expPath) { + relToAbsPath, relToAbsPathErr := filepath.Abs(filepath.Join(c.snowblockAbsPath, expPath)) + if relToAbsPathErr != nil { + return fmt.Errorf("could not dissolve clean target path relative to snowblock path: %v", relToAbsPathErr) + } + absPath = relToAbsPath + } else { + dissolvedPath, dissolvePathErr := filepath.Abs(expPath) + if dissolvePathErr != nil { + return fmt.Errorf("could not dissolve absolute clean target path: %v", dissolvePathErr) + } + absPath = dissolvedPath + } + c.absPaths = append(c.absPaths, absPath) + } + // ...and deduplicate possible duplicates to prevent to process and traverse same paths multiple times. + prt.Debugf("Filtering possible duplicate clean targets: %s", color.YellowString("%v", c.absPaths)) + c.absPaths = removeDuplicatesTargets(c.absPaths) + prt.Debugf("Processing deduplicated clean targets: %s", color.CyanString("%v", c.absPaths)) + if execErr := c.execute(); execErr != nil { + return execErr + } + + // Reject invalid or unsupported JSON data structures. + default: + prt.Debugf("unsupported clean configuration type: %s", color.RedString("%T", configType)) + return fmt.Errorf("unsupported clean configuration") + } + + return nil +} + +func (c *Clean) execute() error { + for _, targetAbsPath := range c.absPaths { + // Ignore targets where the directory or file does not exist... + nodeInfo, nodeInfoErr := os.Lstat(targetAbsPath) + if os.IsNotExist(nodeInfoErr) { + prt.Debugf("Ignoring non-existent clean target: %s", color.RedString(targetAbsPath)) + continue + // ...and fail if any error occurs while trying to describe the node at the given path. + } else if nodeInfoErr != nil { + return nodeInfoErr + } + + t := &target{absPath: targetAbsPath, nodeInfo: nodeInfo, path: targetAbsPath} + isSymlink, symlinkChkErr := filesystem.IsSymlink(t.absPath) + if symlinkChkErr != nil { + return symlinkChkErr + } + if isSymlink { + t.isSymlink = true + } + c.targets = append(c.targets, t) + } + + for _, t := range c.targets { + // Handle the target when it is a symbolic link... + if t.isSymlink { + if brokenSymlinkErr := c.handleBrokenSnowblockSymlink(t.absPath); brokenSymlinkErr != nil { + return brokenSymlinkErr + } + continue + } + + // ...or traverse all nodes when it is a directory. + if t.nodeInfo.IsDir() { + nodes, nodesListErr := ioutil.ReadDir(t.absPath) + if nodesListErr != nil { + return fmt.Errorf("could not read clean target directory content: %s", color.RedString("%v", nodesListErr)) + } + for _, targetNode := range nodes { + nodeAbsPath := filepath.Join(t.absPath, targetNode.Name()) + isSymlink, symlinkChkErr := filesystem.IsSymlink(nodeAbsPath) + if symlinkChkErr != nil { + return symlinkChkErr + } + if isSymlink { + if brokenSymlinkErr := c.handleBrokenSnowblockSymlink(nodeAbsPath); brokenSymlinkErr != nil { + return brokenSymlinkErr + } + } + } + } + } + + return nil +} + +// isSnowblockSymlink checks if the symbolic link at the given absolute path is a broken link of a snowblock node. +// Returns any error that might occur during the process, nil otherwise. +func (c *Clean) handleBrokenSnowblockSymlink(absPath string) error { + // Dissolve the absolute path of the symbolic link and remove it... + destPath, destPathErr := os.Readlink(absPath) + if destPathErr != nil { + return fmt.Errorf("could not read symbolic link: %v", destPathErr) + } + if !filepath.IsAbs(destPath) { + destAbsPath, destAbsPathErr := filepath.Abs(filepath.Join(filepath.Dir(absPath), destPath)) + if destAbsPathErr != nil { + return fmt.Errorf("could not dissolve absolute path: %v", destAbsPathErr) + } + destPath = destAbsPath + } + nodeExists, nodeExistsErr := filesystem.NodeExists(destPath) + if nodeExistsErr != nil { + return nodeExistsErr + } + // ...when the underlying node does not exist... + if !nodeExists { + // ...and the path is a subdirectory of the snowblock directory. + if strings.HasPrefix(destPath, c.snowblockAbsPath) { + if removeErr := os.Remove(absPath); removeErr != nil { + return removeErr + } + prt.Infof("Removed broken symbolic link: %s → %s", + color.YellowString(absPath), color.RedString(destPath)) + } + } + + return nil +} + +// removeDuplicatesTargets removes all duplicate target paths. +func removeDuplicatesTargets(targets []string) []string { + encountered := map[string]bool{} + // Create a map of all unique targets... + for t := range targets { + encountered[targets[t]] = true + } + var result []string + // ... and convert all keys from the map into a slice. + for key := range encountered { + result = append(result, key) + } + + return result +}