Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add install method to plugin CLIManager #364

Merged
merged 38 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e561df3
added install to plugin CLIManager
Two-Hearts Nov 24, 2023
6aabbeb
update
Two-Hearts Nov 24, 2023
aa458a8
update
Two-Hearts Nov 24, 2023
59101ed
updated error messages
Two-Hearts Nov 24, 2023
24aef8a
updated error messages
Two-Hearts Nov 24, 2023
b1e0084
updated err msg
Two-Hearts Nov 27, 2023
67bf256
Merge branch 'notaryproject:main' into plugin
Two-Hearts Nov 27, 2023
75a090e
update per code review
Two-Hearts Nov 28, 2023
bd92473
updated per code review
Two-Hearts Dec 6, 2023
c2f7ef8
fix tests
Two-Hearts Dec 6, 2023
371eaa1
updated function doc
Two-Hearts Dec 6, 2023
b845115
updated function doc
Two-Hearts Dec 6, 2023
3eae658
Merge branch 'notaryproject:main' into plugin
Two-Hearts Dec 11, 2023
002fd51
update
Two-Hearts Dec 11, 2023
14aaaa9
updated per code review
Two-Hearts Dec 11, 2023
b179d54
update
Two-Hearts Dec 11, 2023
c44b738
update
Two-Hearts Dec 11, 2023
c9aa8f1
fix tests
Two-Hearts Dec 11, 2023
6fb5ac6
fix test
Two-Hearts Dec 11, 2023
3f4aa31
add tests
Two-Hearts Dec 12, 2023
dd24043
fix tests
Two-Hearts Dec 12, 2023
1fca1b4
update error msg
Two-Hearts Dec 12, 2023
e8ee2ba
update
Two-Hearts Dec 12, 2023
79ea6b7
fix tests
Two-Hearts Dec 12, 2023
a611ee9
fix tests
Two-Hearts Dec 12, 2023
edd0166
fix tests
Two-Hearts Dec 12, 2023
50ba674
update
Two-Hearts Dec 12, 2023
90bfb95
added clean up before installation
Two-Hearts Dec 12, 2023
c8b1d6e
updated comments
Two-Hearts Dec 12, 2023
f939964
added clean up on failure
Two-Hearts Dec 14, 2023
c7ded4a
update
Two-Hearts Dec 14, 2023
d6eb530
update
Two-Hearts Dec 14, 2023
f623ed9
updated per code review
Two-Hearts Dec 15, 2023
f339741
updated per code review
Two-Hearts Dec 15, 2023
c16b6c3
updated per code review
Two-Hearts Dec 15, 2023
cb32182
updated per code review
Two-Hearts Dec 15, 2023
67d267e
updated per code review
Two-Hearts Dec 18, 2023
d6ab20f
updated error message
Two-Hearts Dec 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 90 additions & 1 deletion internal/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,98 @@

package file

import "regexp"
import (
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
)

// ErrNotRegularFile is returned when the file is not an regular file.
var ErrNotRegularFile = errors.New("not regular file")

// ErrNotDirectory is returned when the path is not a directory.
var ErrNotDirectory = errors.New("not directory")

// IsValidFileName checks if a file name is cross-platform compatible
func IsValidFileName(fileName string) bool {
return regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`).MatchString(fileName)
}

// CopyToDir copies the src file to dst. Existing file will be overwritten.
func CopyToDir(src, dst string) error {
sourceFileStat, err := os.Stat(src)
if err != nil {
return err
}

if !sourceFileStat.Mode().IsRegular() {
return ErrNotRegularFile
}

source, err := os.Open(src)
if err != nil {
return err
}
defer source.Close()

if err := os.MkdirAll(dst, 0700); err != nil {
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
return err
}
dstFile := filepath.Join(dst, filepath.Base(src))
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
destination, err := os.Create(dstFile)
if err != nil {
return err
}
defer destination.Close()
err = destination.Chmod(0600)
if err != nil {
return err
}
_, err = io.Copy(destination, source)
return err
}

// CopyDirToDir copies contents in src dir to dst dir. Only regular files are
// copied. Existing files will be overwritten.
func CopyDirToDir(src, dst string) error {
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
fi, err := os.Stat(src)
if err != nil {
return err
}
if !fi.Mode().IsDir() {
return ErrNotDirectory
}
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// skip sub-directories
if d.IsDir() && d.Name() != filepath.Base(path) {
return fs.SkipDir
}
info, err := d.Info()
if err != nil {
return err
}
// only copy regular files
if info.Mode().IsRegular() {
return CopyToDir(path, dst)
}
return nil
})
}

// TrimFileExtension returns the file name without extension.
//
// For example,
//
// when input is xyz.exe, output is xyz
//
// when input is xyz.tar.gz, output is xyz.tar
func TrimFileExtension(fileName string) string {
return strings.TrimSuffix(fileName, filepath.Ext(fileName))
}
179 changes: 179 additions & 0 deletions internal/file/file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package file

import (
"bytes"
"os"
"path/filepath"
"runtime"
"testing"
)

func TestCopyToDir(t *testing.T) {
t.Run("copy file", func(t *testing.T) {
tempDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}

destDir := filepath.Join(tempDir, "b")
if err := CopyToDir(filename, destDir); err != nil {
t.Fatal(err)
}
})

t.Run("source directory permission error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}

tempDir := t.TempDir()
destDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}

if err := os.Chmod(tempDir, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(tempDir, 0700)

if err := CopyToDir(filename, destDir); err == nil {
t.Fatal("should have error")
}
})

t.Run("not a regular file", func(t *testing.T) {
tempDir := t.TempDir()
destDir := t.TempDir()
if err := CopyToDir(tempDir, destDir); err == nil {
t.Fatal("should have error")
}
})

t.Run("source file permission error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}

tempDir := t.TempDir()
destDir := t.TempDir()
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}
// forbid reading
if err := os.Chmod(filename, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(filename, 0600)
if err := CopyToDir(filename, destDir); err == nil {
t.Fatal("should have error")
}
})

t.Run("dest directory permission error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}

tempDir := t.TempDir()
destTempDir := t.TempDir()
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}
// forbid dest directory operation
if err := os.Chmod(destTempDir, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(destTempDir, 0700)
if err := CopyToDir(filename, filepath.Join(destTempDir, "a")); err == nil {
t.Fatal("should have error")
}
})

t.Run("dest directory permission error 2", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}

tempDir := t.TempDir()
destTempDir := t.TempDir()
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}
// forbid writing to destTempDir
if err := os.Chmod(destTempDir, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(destTempDir, 0700)
if err := CopyToDir(filename, destTempDir); err == nil {
t.Fatal("should have error")
}
})

t.Run("copy file and check content", func(t *testing.T) {
tempDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}

destDir := filepath.Join(tempDir, "b")
if err := CopyToDir(filename, destDir); err != nil {
t.Fatal(err)
}
validFileContent(t, filepath.Join(destDir, "file.txt"), data)
})
}

func TestFileNameWithoutExtension(t *testing.T) {
input := "testfile.tar.gz"
expectedOutput := "testfile.tar"
actualOutput := TrimFileExtension(input)
if actualOutput != expectedOutput {
t.Errorf("expected '%s', but got '%s'", expectedOutput, actualOutput)
}
}

func validFileContent(t *testing.T, filename string, content []byte) {
b, err := os.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(content, b) {
t.Fatal("file content is not correct")
}
}

func writeFile(path string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}
53 changes: 53 additions & 0 deletions internal/semver/semver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package semver provides functions related to semanic version.
// This package is based on "golang.org/x/mod/semver"
package semver
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved

import (
"fmt"
"strings"

"golang.org/x/mod/semver"
)

// IsSemverValid returns true if version is a valid semantic version
func IsSemverValid(version string) bool {
// a valid semanic version MUST not have prefix 'v'
if strings.HasPrefix(version, "v") {
return false
}
// golang package "golang.org/x/mod/semver" requires prefix 'v'
return semver.IsValid("v" + version)
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
}

// ComparePluginVersion validates and compares two plugin semantic versions.
//
// The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
func ComparePluginVersion(v, w string) (int, error) {
// sanity check
// a valid semantic version should not have prefix `v`
// Reference: https://semver.org/#semantic-versioning-200
if !IsSemverValid(v) {
return 0, fmt.Errorf("%s is not a valid semantic version", v)
}
if !IsSemverValid(w) {
return 0, fmt.Errorf("%s is not a valid semantic version", w)
}

// golang.org/x/mod/semver requires semantic version strings must begin
// with a leading "v". Adding prefix "v" to the inputs.
// Reference: https://pkg.go.dev/golang.org/x/mod/semver#pkg-overview
return semver.Compare("v"+v, "v"+w), nil
}
40 changes: 40 additions & 0 deletions internal/semver/semver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package semver

import "testing"

func TestComparePluginVersion(t *testing.T) {
t.Run("compare with lower version", func(t *testing.T) {
comp, err := ComparePluginVersion("1.0.0", "1.0.1")
if err != nil || comp >= 0 {
t.Fatal("expected nil err and negative comp")
}
})

t.Run("compare with equal version", func(t *testing.T) {
comp, err := ComparePluginVersion("1.0.1", "1.0.1")
if err != nil || comp != 0 {
t.Fatal("expected nil err and comp equal to 0")
}
})

t.Run("failed due to invalid semantic version", func(t *testing.T) {
expectedErrMsg := "v1.0.0 is not a valid semantic version"
_, err := ComparePluginVersion("v1.0.0", "1.0.1")
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected err %s, but got %s", expectedErrMsg, err)
}
})
}
Loading
Loading