diff --git a/.devcontainer/.dockerignore b/.devcontainer/.dockerignore new file mode 100644 index 0000000..92bc670 --- /dev/null +++ b/.devcontainer/.dockerignore @@ -0,0 +1,3 @@ +.go +.dockerignore +devcontainer.json diff --git a/.devcontainer/.go/.gitignore b/.devcontainer/.go/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/.devcontainer/.go/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..6920071 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,82 @@ +ARG UBUNTU_VERSION=22.04 +FROM ubuntu:${UBUNTU_VERSION} + +# apt mirror server +ARG APT_MIRROR="" +RUN set -x \ + && [ "${APT_MIRROR:-}" != "" ] && sed -i -r "s@http://(\\w+.)?archive\.ubuntu\.com/ubuntu/@${APT_MIRROR}@" || : + +# update ca-certificates +RUN set -x \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + && apt-get clean && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* + +# timezone +RUN set -x \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + tzdata \ + && apt-get clean && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* \ + && ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \ + && echo 'Asia/Tokyo' >/etc/timezone + +# language-pack +RUN set -x \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + language-pack-ja-base \ + language-pack-ja \ + && apt-get clean && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* +ENV LANG=ja_JP.UTF-8 + +# vscode user +RUN set -x \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + sudo \ + && apt-get clean && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* \ + && echo "vscode ALL=(ALL) NOPASSWD:ALL" >>/etc/sudoers.d/ALL \ + && groupadd \ + --gid 5000 \ + vscode \ + && useradd \ + --uid 5000 \ + --gid 5000 \ + --home-dir /home/vscode \ + --create-home \ + --shell /bin/bash \ + vscode + +# common dev tools +RUN set -x \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + bash-completion \ + curl \ + git \ + gnupg2 \ + iputils-ping \ + less \ + net-tools \ + tar \ + time \ + unzip \ + xz-utils \ + zip \ + && apt-get clean && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* + +# golang +ARG GO_VERSION=1.20.5 +RUN set -x \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + && apt-get clean && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* \ + && curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" | tar -xz -C /usr/local +ENV GOROOT=/usr/local/go \ + GOPATH=/home/vscode/go \ + PATH=/home/vscode/go/bin:/usr/local/go/bin:${PATH} + +USER vscode diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..51f3c1e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,51 @@ +{ + "name": "files", + "build": { + "dockerfile": "Dockerfile", + "args": { + "UBUNTU_VERSION": "22.04", + "APT_MIRROR": "http://jp.archive.ubuntu.com/ubuntu/", + "GO_VERSION": "1.20.6" + } + }, + "mounts": [ + "source=${localWorkspaceFolder}/.devcontainer/.go,target=/home/vscode/go,type=bind" + ], + "postStartCommand": "/bin/bash .devcontainer/post-start.sh", + "customizations": { + "vscode": { + "extensions": [ + "golang.go", + "streetsidesoftware.code-spell-checker" + ], + "settings": { + "go.useLanguageServer": true, + "go.formatTool": "goimports", + "go.lintTool": "golangci-lint", + "go.lintFlags": [ + "--fast", + "-exclude=vendor/..." + ], + "go.lintOnSave": "workspace", + "go.vetOnSave": "workspace", + "[go]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + "editor.snippetSuggestions": "bottom" + }, + "[go.mod]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, + "gopls": { + "usePlaceholders": true, + "staticcheck": false + } + } + } + } +} diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh new file mode 100755 index 0000000..98c7002 --- /dev/null +++ b/.devcontainer/post-start.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -eu +[ -f "$(go env GOPATH)/bin/gotests" ] || go install -v github.com/cweill/gotests/gotests@latest +[ -f "$(go env GOPATH)/bin/gomodifytags" ] || go install -v github.com/fatih/gomodifytags@latest +[ -f "$(go env GOPATH)/bin/impl" ] || go install -v github.com/josharian/impl@latest +[ -f "$(go env GOPATH)/bin/goplay" ] || go install -v github.com/haya14busa/goplay/cmd/goplay@latest +[ -f "$(go env GOPATH)/bin/dlv" ] || go install -v github.com/go-delve/delve/cmd/dlv@latest +[ -f "$(go env GOPATH)/bin/golangci-lint" ] || go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest +[ -f "$(go env GOPATH)/bin/gopls" ] || go install -v golang.org/x/tools/gopls@latest diff --git a/.vscode/extension.json b/.vscode/extension.json new file mode 100644 index 0000000..a533634 --- /dev/null +++ b/.vscode/extension.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-vscode-remote.remote-containers" + ], + "unwantedRecommendations": [ + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ad45b04 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,25 @@ +{ + "terminal.integrated.allowChords": false, + "files.exclude": { + "**/.devcontainer/.go": true + }, + "files.watcherExclude": { + "**/.devcontainer/.go": true + }, + "search.exclude": { + "**/.devcontainer/.go": true + }, + "files.autoSave": "afterDelay", + "files.autoSaveDelay": 1000, + "cSpell.ignorePaths": [ + ".vscode", + ".devcontainer" + ], + "cSpell.words": [ + "dstpath", + "newpath", + "oldpath", + "srcpath", + "thamaji" + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..338ab7d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 thamaji + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d35f211 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +files +==== + +一般的なファイル操作を提供する go ライブラリ。 + +``` +$ go get github.com/thamaji/files +``` diff --git a/dir.go b/dir.go new file mode 100644 index 0000000..08f9df6 --- /dev/null +++ b/dir.go @@ -0,0 +1,121 @@ +package files + +import ( + "io" + "io/fs" + "os" +) + +func Mkdir(name string, opt ...Option) error { + o := getOptions(opt...) + return os.Mkdir(name, o.DirPerm) +} + +func MkdirAll(path string, opt ...Option) error { + o := getOptions(opt...) + return os.MkdirAll(path, o.DirPerm) +} + +func Readdir(name string) ([]fs.FileInfo, error) { + f, err := os.OpenFile(name, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + list, err := f.Readdir(-1) + _ = f.Close() + if err != nil { + return nil, err + } + return list, nil +} + +func MustReaddir(name string) []fs.FileInfo { + list, err := Readdir(name) + if err != nil { + return nil + } + return list +} + +func ReadDirnames(name string) ([]string, error) { + f, err := os.OpenFile(name, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + list, err := f.Readdirnames(-1) + _ = f.Close() + if err != nil { + return nil, err + } + return list, nil +} + +func MustReadDirnames(name string) []string { + list, err := ReadDirnames(name) + if err != nil { + return nil + } + return list +} + +func ReadDir(name string) ([]fs.DirEntry, error) { + f, err := os.OpenFile(name, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + list, err := f.ReadDir(-1) + _ = f.Close() + if err != nil { + return nil, err + } + return list, nil +} + +func MustReadDir(name string) []fs.DirEntry { + list, err := ReadDir(name) + if err != nil { + return nil + } + return list +} + +func RemoveAll(path string) error { + return os.RemoveAll(path) +} + +func IsDir(name string) (bool, error) { + fi, err := os.Stat(name) + if err != nil { + return true, err + } + return fi.IsDir(), nil +} + +func MustIsDir(name string) bool { + ok, err := IsDir(name) + if err != nil { + return false + } + return ok +} + +func IsEmptyDir(name string) (bool, error) { + f, err := os.OpenFile(name, os.O_RDONLY, 0) + if err != nil { + return false, err + } + _, err = f.Readdirnames(1) + _ = f.Close() + if err == io.EOF { + return true, nil + } + return false, err +} + +func MustIsEmptyDir(name string) bool { + ok, err := IsEmptyDir(name) + if err != nil { + return false + } + return ok +} diff --git a/file.go b/file.go new file mode 100644 index 0000000..11c307d --- /dev/null +++ b/file.go @@ -0,0 +1,52 @@ +package files + +import ( + "errors" + "os" + "path/filepath" +) + +func Open(name string, opt ...Option) (*os.File, error) { + return OpenFile(name, os.O_RDONLY, opt...) +} + +func Create(name string, opt ...Option) (*os.File, error) { + return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, opt...) +} + +func OpenFile(name string, flag int, opt ...Option) (*os.File, error) { + o := getOptions(opt...) + f, err := os.OpenFile(name, flag, o.FilePerm) + if err != nil { + if !errors.Is(err, os.ErrNotExist) || flag&os.O_CREATE == 0 { + return nil, err + } + if err = os.MkdirAll(filepath.Dir(name), o.DirPerm); err != nil { + return nil, err + } + f, err = os.OpenFile(name, flag, o.FilePerm) + if err != nil { + return nil, err + } + } + return f, nil +} + +func ReadFile(name string) ([]byte, error) { + return os.ReadFile(name) +} + +func WriteFile(name string, data []byte, opt ...Option) error { + f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, opt...) + if err != nil { + return err + } + _, err = f.Write(data) + if err1 := f.Sync(); err1 != nil && err == nil { + err = err1 + } + if err1 := f.Close(); err1 != nil && err == nil { + err = err1 + } + return err +} diff --git a/files.go b/files.go new file mode 100644 index 0000000..93e06f8 --- /dev/null +++ b/files.go @@ -0,0 +1,148 @@ +package files + +import ( + "errors" + "io" + "os" + "path/filepath" +) + +func Remove(name string) error { + return os.Remove(name) +} + +func Exists(name string) (bool, error) { + _, err := os.Stat(name) + if err == nil { + return true, nil + } + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + return false, err +} + +func MustExists(name string) bool { + ok, err := Exists(name) + if err != nil { + return false + } + return ok +} + +func Rename(oldpath, newpath string) error { + return os.Rename(oldpath, newpath) +} + +func Move(oldpath, newpath string) error { + if err := os.Rename(oldpath, newpath); err == nil { + return nil + } + + if err := Copy(oldpath, newpath); err != nil { + return err + } + + return os.Remove(oldpath) +} + +func Copy(srcpath, dstpath string) error { + sf, err := os.OpenFile(srcpath, os.O_RDONLY, 0) + if err != nil { + return err + } + + sfi, err := sf.Stat() + if err != nil { + _ = sf.Close() + return err + } + + if sfi.IsDir() { + temp, err := MkdirTemp(filepath.Dir(dstpath), filepath.Base(dstpath)+"_*", WithDirPerm(sfi.Mode().Perm())) + if err != nil { + return err + } + + names, err := sf.Readdirnames(-1) + _ = sf.Close() + if err != nil { + return err + } + + for _, name := range names { + if err := copy(filepath.Join(srcpath, name), filepath.Join(temp, name)); err != nil { + _ = os.RemoveAll(temp) + return err + } + } + + if err := os.Rename(temp, dstpath); err != nil { + _ = os.RemoveAll(temp) + return err + } + + return nil + } + + df, err := OpenFileWriter(dstpath, WithFilePerm(sfi.Mode().Perm())) + if err != nil { + return err + } + _, err = io.Copy(df, sf) + if err1 := df.Close(); err1 != nil && err == nil { + err = err1 + } + if err != nil { + return err + } + + return nil +} + +func copy(srcpath, dstpath string) error { + sf, err := os.OpenFile(srcpath, os.O_RDONLY, 0) + if err != nil { + return err + } + + sfi, err := sf.Stat() + if err != nil { + _ = sf.Close() + return err + } + + if sfi.IsDir() { + if err := os.Mkdir(dstpath, sfi.Mode().Perm()); err != nil { + _ = sf.Close() + return err + } + + names, err := sf.Readdirnames(-1) + _ = sf.Close() + if err != nil { + return err + } + for _, name := range names { + if err := copy(filepath.Join(srcpath, name), filepath.Join(dstpath, name)); err != nil { + return err + } + } + return nil + } + + df, err := os.OpenFile(dstpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, sfi.Mode().Perm()) + if err != nil { + _ = sf.Close() + return err + } + _, err = io.Copy(df, sf) + _ = sf.Close() + if err1 := df.Sync(); err1 != nil && err == nil { + err = err1 + } + if err1 := df.Close(); err1 != nil && err == nil { + err = err1 + } + return err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..57c4c62 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/thamaji/files + +go 1.16 diff --git a/options.go b/options.go new file mode 100644 index 0000000..bf82996 --- /dev/null +++ b/options.go @@ -0,0 +1,46 @@ +package files + +import "io/fs" + +var DefaultFilePerm fs.FileMode = 0644 +var DefaultDirPerm fs.FileMode = 0755 + +func SetDefaultFileMode(perm fs.FileMode) { + DefaultDirPerm = perm +} + +func SetDefaultDirFileMode(perm fs.FileMode) { + DefaultDirPerm = perm +} + +type options struct { + FilePerm fs.FileMode + DirPerm fs.FileMode +} + +func getOptions(opts ...Option) options { + o := options{ + FilePerm: DefaultFilePerm, + DirPerm: DefaultDirPerm, + } + for _, of := range opts { + o = of(o) + } + return o +} + +type Option func(options) options + +func WithFilePerm(perm fs.FileMode) Option { + return func(o options) options { + o.FilePerm = perm + return o + } +} + +func WithDirPerm(perm fs.FileMode) Option { + return func(o options) options { + o.FilePerm = perm + return o + } +} diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..18a22a6 --- /dev/null +++ b/reader.go @@ -0,0 +1,30 @@ +package files + +import ( + "os" +) + +func OpenFileReader(name string) (*FileReader, error) { + f, err := os.OpenFile(name, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + r := &FileReader{f: f} + return r, nil +} + +type FileReader struct { + f *os.File +} + +func (r *FileReader) Read(p []byte) (int, error) { + return r.f.Read(p) +} + +func (r *FileReader) ReadAt(p []byte, off int64) (int, error) { + return r.f.ReadAt(p, off) +} + +func (r *FileReader) Close() error { + return r.f.Close() +} diff --git a/temp.go b/temp.go new file mode 100644 index 0000000..0f16e93 --- /dev/null +++ b/temp.go @@ -0,0 +1,42 @@ +package files + +import ( + "errors" + "os" +) + +func CreateTemp(dir string, pattern string, opt ...Option) (*os.File, error) { + o := getOptions(opt...) + f, err := os.CreateTemp(dir, pattern) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return nil, err + } + if err = os.MkdirAll(dir, o.DirPerm); err != nil { + return nil, err + } + f, err = os.CreateTemp(dir, pattern) + if err != nil { + return nil, err + } + } + if err := f.Chmod(o.FilePerm); err != nil { + _ = f.Close() + _ = os.Remove(f.Name()) + return nil, err + } + return f, nil +} + +func MkdirTemp(dir string, pattern string, opt ...Option) (string, error) { + o := getOptions(opt...) + name, err := os.MkdirTemp(dir, pattern) + if err != nil { + return "", err + } + if err := os.Chmod(name, o.DirPerm); err != nil { + _ = os.Remove(name) + return "", err + } + return name, nil +} diff --git a/writer.go b/writer.go new file mode 100644 index 0000000..5958348 --- /dev/null +++ b/writer.go @@ -0,0 +1,56 @@ +package files + +import ( + "os" + "path/filepath" +) + +func OpenFileWriter(name string, opt ...Option) (*FileWriter, error) { + f, err := CreateTemp(filepath.Dir(name), filepath.Base(name)+"_*", opt...) + if err != nil { + return nil, err + } + w := &FileWriter{name: name, f: f, err: nil} + return w, nil +} + +type FileWriter struct { + name string + f *os.File + err error +} + +func (w *FileWriter) Write(p []byte) (int, error) { + n, err := w.f.Write(p) + if err != nil { + w.err = err + } + return n, err +} + +func (w *FileWriter) WriteAt(b []byte, off int64) (int, error) { + n, err := w.f.WriteAt(b, off) + if err != nil { + w.err = err + } + return n, err +} + +func (w *FileWriter) Close() error { + err := w.err + if err1 := w.f.Sync(); err1 != nil && err == nil { + err = err1 + } + if err1 := w.f.Close(); err1 != nil && err == nil { + err = err1 + } + if err != nil { + _ = os.Remove(w.f.Name()) + return err + } + if err := os.Rename(w.f.Name(), w.name); err != nil { + _ = os.Remove(w.f.Name()) + return err + } + return nil +}