diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9a7a469..e486e49 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,9 +3,12 @@ name: CI on: push: branches: - - master + - main pull_request: +permissions: + contents: read + jobs: test: name: Test @@ -15,9 +18,9 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - name: Checkout code - uses: actions/checkout@main + uses: actions/checkout@v3 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: 1.x - name: Test diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0c39c5e..2ec95ca 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -5,28 +5,24 @@ on: tags: - 'v*' +permissions: + contents: write + jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@main + uses: actions/checkout@v3 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: 1.x - name: Cross build run: make cross - name: Create Release - id: create_release - uses: actions/create-release@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: ncipollo/release-action@v1 with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - - name: Upload - run: make upload - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + name: Release ${{ github.ref_name }} + artifacts: 'goxz/*' diff --git a/CHANGELOG.md b/CHANGELOG.md index da9bd98..aa8e677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -## [v0.1.2](https://github.com/itchyny/gojq/compare/v0.1.1..v0.1.2) (2020-09-19) +## [v0.1.6](https://github.com/itchyny/mmv/compare/v0.1.5..v0.1.6) (2023-04-26) +* Remove dependency on shell for splitting EDITOR with spaces. + +## [v0.1.5](https://github.com/itchyny/mmv/compare/v0.1.4..v0.1.5) (2023-04-09) +* Support EDITOR with spaces in the editor path. + +## [v0.1.4](https://github.com/itchyny/mmv/compare/v0.1.3..v0.1.4) (2021-09-18) +* Release `arm64` artifacts. + +## [v0.1.3](https://github.com/itchyny/mmv/compare/v0.1.2..v0.1.3) (2021-01-10) +* Support renaming when one of the paths is a parent directory of another. + +## [v0.1.2](https://github.com/itchyny/mmv/compare/v0.1.1..v0.1.2) (2020-09-19) * Fix for EDITOR configured with arguments. -## [v0.1.1](https://github.com/itchyny/gojq/compare/v0.1.0..v0.1.1) (2020-01-09) +## [v0.1.1](https://github.com/itchyny/mmv/compare/v0.1.0..v0.1.1) (2020-01-09) * Undo the processed renames on error not to leave the temporary file. * Return the stat source error on failure. diff --git a/LICENSE b/LICENSE index 7085e92..3e03a16 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020 itchyny +Copyright (c) 2020-2023 itchyny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index cee0572..6301fb1 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,9 @@ BIN := mmv VERSION := $$(make -s show-version) VERSION_PATH := cmd/$(BIN) -CURRENT_REVISION := $(shell git rev-parse --short HEAD) -BUILD_LDFLAGS := "-s -w -X main.revision=$(CURRENT_REVISION)" +CURRENT_REVISION = $(shell git rev-parse --short HEAD) +BUILD_LDFLAGS = "-s -w -X main.revision=$(CURRENT_REVISION)" GOBIN ?= $(shell go env GOPATH)/bin -export GO111MODULE=on .PHONY: all all: build @@ -15,40 +14,40 @@ build: .PHONY: install install: - go install -ldflags=$(BUILD_LDFLAGS) ./... + go install -ldflags=$(BUILD_LDFLAGS) ./cmd/$(BIN) .PHONY: show-version show-version: $(GOBIN)/gobump - @gobump show -r $(VERSION_PATH) + @gobump show -r "$(VERSION_PATH)" $(GOBIN)/gobump: - @cd && go get github.com/x-motemen/gobump/cmd/gobump + @go install github.com/x-motemen/gobump/cmd/gobump@latest .PHONY: cross cross: $(GOBIN)/goxz CREDITS goxz -n $(BIN) -pv=v$(VERSION) -build-ldflags=$(BUILD_LDFLAGS) ./cmd/$(BIN) $(GOBIN)/goxz: - cd && go get github.com/Songmu/goxz/cmd/goxz + go install github.com/Songmu/goxz/cmd/goxz@latest CREDITS: $(GOBIN)/gocredits go.sum go mod tidy gocredits -w . $(GOBIN)/gocredits: - cd && go get github.com/Songmu/gocredits/cmd/gocredits + go install github.com/Songmu/gocredits/cmd/gocredits@latest .PHONY: test test: build - go test -v ./... + go test -v -race ./... .PHONY: lint -lint: $(GOBIN)/golint +lint: $(GOBIN)/staticcheck go vet ./... - golint -set_exit_status ./... + staticcheck -checks all ./... -$(GOBIN)/golint: - cd && go get golang.org/x/lint/golint +$(GOBIN)/staticcheck: + go install honnef.co/go/tools/cmd/staticcheck@latest .PHONY: clean clean: @@ -57,21 +56,9 @@ clean: .PHONY: bump bump: $(GOBIN)/gobump -ifneq ($(shell git status --porcelain),) - $(error git workspace is dirty) -endif -ifneq ($(shell git rev-parse --abbrev-ref HEAD),master) - $(error current branch is not master) -endif + test -z "$$(git status --porcelain || echo .)" + test "$$(git branch --show-current)" = "main" @gobump up -w "$(VERSION_PATH)" git commit -am "bump up version to $(VERSION)" git tag "v$(VERSION)" - git push origin master - git push origin "refs/tags/v$(VERSION)" - -.PHONY: upload -upload: $(GOBIN)/ghr - ghr "v$(VERSION)" goxz - -$(GOBIN)/ghr: - cd && go get github.com/tcnksm/ghr + git push --atomic origin main tag "v$(VERSION)" diff --git a/README.md b/README.md index d345be5..5f6efa9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # mmv [![CI Status](https://github.com/itchyny/mmv/workflows/CI/badge.svg)](https://github.com/itchyny/mmv/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/itchyny/mmv)](https://goreportcard.com/report/github.com/itchyny/mmv) -[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/itchyny/mmv/blob/master/LICENSE) +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/itchyny/mmv/blob/main/LICENSE) [![release](https://img.shields.io/github/release/itchyny/mmv/all.svg)](https://github.com/itchyny/mmv/releases) [![pkg.go.dev](https://pkg.go.dev/badge/github.com/itchyny/mmv)](https://pkg.go.dev/github.com/itchyny/mmv) @@ -24,7 +24,7 @@ brew install itchyny/tap/mmv ### Build from source ```bash -go get github.com/itchyny/mmv/cmd/mmv +go install github.com/itchyny/mmv/cmd/mmv@latest ``` ## Features diff --git a/cmd/mmv/main.go b/cmd/mmv/main.go index 30e1e61..6e854c4 100644 --- a/cmd/mmv/main.go +++ b/cmd/mmv/main.go @@ -4,12 +4,12 @@ import ( "errors" "flag" "fmt" - "io/ioutil" "os" "os/exec" "runtime" "strings" + "github.com/kballard/go-shellquote" _ "github.com/mattn/getwild" "github.com/mattn/go-tty" @@ -18,7 +18,7 @@ import ( const name = "mmv" -const version = "0.1.2" +const version = "0.1.6" var revision = "HEAD" @@ -79,42 +79,45 @@ func rename(args []string) error { } xs[src] = true } - f, err := ioutil.TempFile("", name+"-") + + f, err := os.CreateTemp("", name+"-") if err != nil { return err } - defer func() { - f.Close() - os.Remove(f.Name()) - }() + defer os.Remove(f.Name()) for _, arg := range args { f.WriteString(arg) f.WriteString("\n") } - editor := os.Getenv("EDITOR") - if editor == "" { - editor = "vi" + if err = f.Close(); err != nil { + return err } + tty, err := tty.Open() if err != nil { return err } defer tty.Close() - editorWithArgs := strings.Fields(editor) + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "vi" + } + editorWithArgs, err := shellquote.Split(editor) + if err != nil { + return fmt.Errorf("%s: %s", err, editor) + } editorWithArgs = append(editorWithArgs, f.Name()) cmd := exec.Command(editorWithArgs[0], editorWithArgs[1:]...) cmd.Stdin = tty.Input() cmd.Stdout = tty.Output() cmd.Stderr = tty.Output() - if err := cmd.Run(); err != nil { + if err = cmd.Run(); err != nil { return fmt.Errorf("abort renames: %s", err) } - if err := f.Close(); err != nil { - return err - } - cnt, err := ioutil.ReadFile(f.Name()) + + cnt, err := os.ReadFile(f.Name()) if err != nil { return err } @@ -126,5 +129,6 @@ func rename(args []string) error { for i, src := range args { files[src] = got[i] } + return mmv.Rename(files) } diff --git a/go.mod b/go.mod index 98d0dd0..f7b9d35 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,14 @@ module github.com/itchyny/mmv -go 1.15 +go 1.20 require ( + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/getwild v0.0.2-0.20200919000855-c2e221927ad6 - github.com/mattn/go-isatty v0.0.12 // indirect - github.com/mattn/go-tty v0.0.3 - golang.org/x/sys v0.0.0-20200918174421-af09f7315aff // indirect + github.com/mattn/go-tty v0.0.4 +) + +require ( + github.com/mattn/go-isatty v0.0.18 // indirect + golang.org/x/sys v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index a784711..2933528 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,18 @@ +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/mattn/getwild v0.0.2-0.20200919000855-c2e221927ad6 h1:uWR+2CTTaHQzDS/DApbJ2H8UEPQl90atrKtczXj2xcs= github.com/mattn/getwild v0.0.2-0.20200919000855-c2e221927ad6/go.mod h1:AG+GKQydHp7iLJn+VV+D7y8LeYs5bQ0Xz4fmKd5o1Sg= github.com/mattn/go-colorable v0.1.4/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.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= -github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-tty v0.0.4 h1:NVikla9X8MN0SQAqCYzpGyXv0jY7MNl3HOWD2dkle7E= +github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/mmv.go b/mmv.go index 5cf5731..bd5ba36 100644 --- a/mmv.go +++ b/mmv.go @@ -1,10 +1,12 @@ +// Package mmv provides a method to rename multiple files. package mmv import ( - "fmt" - "math/rand" + "crypto/rand" + "encoding/base64" "os" "path/filepath" + "strings" ) // Rename multiple files. @@ -63,7 +65,7 @@ type sameSourceError struct { } func (err *sameSourceError) Error() string { - return fmt.Sprintf("duplicate source: %s", err.path) + return "duplicate source: " + err.path } type sameDestinationError struct { @@ -71,7 +73,23 @@ type sameDestinationError struct { } func (err *sameDestinationError) Error() string { - return fmt.Sprintf("duplicate destination: %s", err.path) + return "duplicate destination: " + err.path +} + +type invalidRenameError struct { + src, dst string +} + +func (err *invalidRenameError) Error() string { + return "invalid rename: " + err.src + ", " + err.dst +} + +type temporaryPathError struct { + dir string +} + +func (err *temporaryPathError) Error() string { + return "failed to create a temporary path: " + err.dir } func buildRenames(files map[string]string) ([]rename, error) { @@ -104,76 +122,144 @@ func buildRenames(files map[string]string) ([]rename, error) { if _, ok := revs[dst]; ok { return nil, &sameDestinationError{dst} } + if k, l := len(src), len(dst); k > l && src[l] == filepath.Separator && src[:l] == dst || + k < l && dst[k] == filepath.Separator && dst[:k] == src { + return nil, &invalidRenameError{src, dst} + } revs[dst] = src } - // remove source == destination + // group paths by directory depth + srcdepths := make([][]string, 1) + dstdepths := make([][]string, 1) for src, dst := range files { - if src == dst { - delete(files, src) - delete(revs, dst) + // group source paths by directory depth + i := strings.Count(src, string(filepath.Separator)) + if len(srcdepths) <= i { + xs := make([][]string, i*2) + copy(xs, srcdepths) + srcdepths = xs + } + srcdepths[i] = append(srcdepths[i], src) + // group destination paths by directory depth + i = strings.Count(dst, string(filepath.Separator)) + if len(dstdepths) <= i { + xs := make([][]string, i*2) + copy(xs, dstdepths) + dstdepths = xs + } + dstdepths[i] = append(dstdepths[i], dst) + } + + // result renames + count := len(files) + rs := make([]rename, 0, 2*count) + + // check if any parent directory will be moved + for i := len(srcdepths) - 1; i >= 0; i-- { + L: + for _, src := range srcdepths[i] { + for j := 0; j < i; j++ { + for _, s := range srcdepths[j] { + if k := len(s); len(src) > k && src[k] == filepath.Separator && src[:k] == s { + if d := files[s]; s != d { + if dst, l := files[src], len(d); i == j+1 && len(dst) > l && dst[:l] == d && dst[l:] == src[k:] { + // skip moving a file when it moves along with the closest parent directory + delete(files, src) + delete(revs, dst) + } else { + // move to a temporary path before any parent directory is moved + tmp, err := temporaryPath(filepath.Dir(s)) + if err != nil { + return nil, err + } + rs = append(rs, rename{src, tmp}) + files[tmp] = files[dst] + delete(files, src) + revs[dst] = tmp + } + continue L + } + } + } + } + // remove if source path is equal to destination path + if dst := files[src]; src == dst { + delete(files, src) + delete(revs, dst) + } } } - // list the renames - var i int - rs := make([]rename, 0, 2*len(files)) - vs := make(map[string]int, len(files)) - for _, dst := range files { - if vs[dst] > 0 { - continue - } - i++ // connected component identifier + // list renames in increasing destination directory depth order + i, vs := 0, make(map[string]int, count) + for _, dsts := range dstdepths { + for _, dst := range dsts { + if vs[dst] > 0 { + continue + } + i++ // connected component identifier - // mark the nodes in the connected component and check cycle - var cycle bool - for { - vs[dst] = i - if x, ok := files[dst]; ok { - dst = x - if vs[x] > 0 { - cycle = vs[x] == i + // mark the nodes in the connected component and check cycle + var cycle bool + for { + vs[dst] = i + if x, ok := files[dst]; ok { + dst = x + if vs[x] > 0 { + cycle = vs[x] == i + break + } + } else { break } - } else { - break } - } - // if there is a cycle, rename to a temporary file - var tmp string - if cycle { - tmp = randomPath(filepath.Dir(dst)) - rs = append(rs, rename{dst, tmp}) - vs[dst]-- - } - - // rename from the leaf node - for { - if src, ok := revs[dst]; ok && (!cycle || vs[src] == i) { - rs = append(rs, rename{src, dst}) - if !cycle { - vs[dst] = i + // if there is a cycle, rename to a temporary file + var tmp string + if cycle { + var err error + tmp, err = temporaryPath(filepath.Dir(dst)) + if err != nil { + return nil, err } - dst = src - } else { - break + rs = append(rs, rename{dst, tmp}) + vs[dst]-- } - } - // if there is a cycle, rename the temporary file - if cycle { - rs = append(rs, rename{tmp, dst}) + // rename from the leaf node + for { + if src, ok := revs[dst]; ok && (!cycle || vs[src] == i) { + rs = append(rs, rename{src, dst}) + if !cycle { + vs[dst] = i + } + dst = src + } else { + break + } + } + + // if there is a cycle, rename the temporary file + if cycle { + rs = append(rs, rename{tmp, dst}) + } } } return rs, nil } -func randomPath(dir string) string { - for { - path := filepath.Join(dir, fmt.Sprint(rand.Uint64())) +// create a temporary path where there is no file currently +func temporaryPath(dir string) (string, error) { + bs := make([]byte, 16) + for i := 0; i < 256; i++ { + if _, err := rand.Read(bs); err != nil { + return "", err + } + path := filepath.Join(dir, base64.RawURLEncoding.EncodeToString(bs)) if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { - return path + return path, nil } } + return "", &temporaryPathError{dir} } diff --git a/mmv_test.go b/mmv_test.go index 2e73e47..f8d93ce 100644 --- a/mmv_test.go +++ b/mmv_test.go @@ -1,7 +1,6 @@ package mmv import ( - "io/ioutil" "os" "path/filepath" "reflect" @@ -296,12 +295,98 @@ func TestRename(t *testing.T) { "a/b/c/baz": "2", }, }, + { + name: "invalid rename error", + files: map[string]string{ + "x/y": "x", + }, + contents: map[string]string{ + "x/y": "0", + }, + expected: map[string]string{ + "x/y": "0", + }, + err: "invalid rename: x", + }, + { + name: "invalid rename error", + files: map[string]string{ + "x/y": "x/y/z", + }, + contents: map[string]string{ + "x/y": "0", + }, + expected: map[string]string{ + "x/y": "0", + }, + err: "invalid rename: x", + }, + { + name: "directory renames", + files: map[string]string{ + "x/foo": "y/bar", + "x/bar": "z/baz", + "x/qux": "z/qux", + "x/quy": "z/baz/qux", + "x/": "z/", + "y/bar": "x/foo", + "y/qux": "x/qux", + "y/": "w/", + "w/": "y/", + "w/x/": "y/y/", + "w/x/x/": "x/z/", + "w/x/y": "y/x/y", + "w/x/z": "w/x/z", + "w/x/w": "x/x/w", + "v/": "v/", + "v/x": "v/y", + "v/x/x": "v/y/x", + "xxxxx": "yyyyy", + }, + count: 26, + contents: map[string]string{ + "x/foo": "0", + "x/bar/a": "1", + "x/qux": "2", + "x/quy": "3", + "x/quz": "4", + "y/bar": "5", + "y/baz": "6", + "y/qux": "7", + "w/a": "8", + "w/x/a": "9", + "w/x/x/a": "10", + "w/x/y": "11", + "w/x/z": "12", + "w/x/w": "13", + "v/x/x": "14", + "xxxxx": "15", + }, + expected: map[string]string{ + "y/bar": "0", + "z/baz/a": "1", + "z/qux": "2", + "z/baz/qux": "3", + "z/quz": "4", + "x/foo": "5", + "w/baz": "6", + "x/qux": "7", + "y/a": "8", + "y/y/a": "9", + "x/z/a": "10", + "y/x/y": "11", + "w/x/z": "12", + "x/x/w": "13", + "v/y/x": "14", + "yyyyy": "15", + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - dir, err := ioutil.TempDir("", "mmv-") + dir, err := os.MkdirTemp("", "mmv-") if err != nil { - t.Fatalf("ioutil.TempDir returned an error: %s", err) + t.Fatalf("os.MkdirTemp returned an error: %s", err) } t.Cleanup(func() { os.RemoveAll(dir) }) if err := os.Chdir(dir); err != nil { @@ -331,7 +416,13 @@ func TestRename(t *testing.T) { func setupFiles(contents map[string]string) error { for f, cnt := range contents { - if err := ioutil.WriteFile(f, []byte(cnt), 0o600); err != nil { + dir := filepath.Dir(f) + if dir != "." { + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + } + if err := os.WriteFile(f, []byte(cnt), 0o600); err != nil { return err } } @@ -340,7 +431,7 @@ func setupFiles(contents map[string]string) error { func fileContents(dir string) map[string]string { m := make(map[string]string) - fis, _ := ioutil.ReadDir(dir) + fis, _ := os.ReadDir(dir) for _, fi := range fis { if fi.IsDir() { for k, v := range fileContents(filepath.Join(dir, fi.Name())) { @@ -348,7 +439,7 @@ func fileContents(dir string) map[string]string { } } else { path := filepath.Join(dir, fi.Name()) - cnt, _ := ioutil.ReadFile(path) + cnt, _ := os.ReadFile(path) m[filepath.ToSlash(path)] = string(cnt) } }