diff options
| author | Shulhan <ms@kilabit.info> | 2023-04-05 23:44:02 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2023-04-08 16:56:26 +0700 |
| commit | d8ed8a6b7d788eb6bb59023e6e6fe93190b8a2c8 (patch) | |
| tree | adaca1b1e11401e52624ae13ca39dceb95a83cc0 | |
| parent | 78e301986dceacc8daa7cb3f549859a005b2b9b8 (diff) | |
| download | pakakeh.go-d8ed8a6b7d788eb6bb59023e6e6fe93190b8a2c8.tar.xz | |
lib/os: merge some functions from lib/io
Functions like Copy, IsBinary, IsDirEmpty, IsFileExist, RmdirEmptyAll
are read and operate on file and directory on operating system level, so
it is not correct to put it in package io.
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | lib/io/dir.go | 2 | ||||
| -rw-r--r-- | lib/io/io.go | 2 | ||||
| -rw-r--r-- | lib/io/is.go | 6 | ||||
| -rw-r--r-- | lib/os/example_test.go | 19 | ||||
| -rw-r--r-- | lib/os/os.go | 164 | ||||
| -rw-r--r-- | lib/os/os_test.go | 200 | ||||
| -rw-r--r-- | lib/os/testdata/.empty | 0 | ||||
| -rw-r--r-- | lib/os/testdata/Copy/input.txt | 27 |
9 files changed, 421 insertions, 0 deletions
@@ -46,6 +46,7 @@ /lib/mining/resampling/smote/synthetic.csv /lib/mining/resampling/smote/testdata/phoneme.csv /lib/mining/testdata/phoneme/phoneme.csv +/lib/os/testdata/Copy/output.txt /lib/ssh/sftp/testdata/*.get /lib/websocket/examples/cmd/client/client /lib/websocket/examples/cmd/server/server diff --git a/lib/io/dir.go b/lib/io/dir.go index 49e7de03..c607145e 100644 --- a/lib/io/dir.go +++ b/lib/io/dir.go @@ -11,6 +11,8 @@ import ( // RmdirEmptyAll remove directory in path if it's empty until one of the // parent is not empty. +// +// DEPRECATED: moved to [lib/os#RmdirEmptyAll]. func RmdirEmptyAll(path string) error { if len(path) == 0 { return nil diff --git a/lib/io/io.go b/lib/io/io.go index 6162c06e..f8437996 100644 --- a/lib/io/io.go +++ b/lib/io/io.go @@ -16,6 +16,8 @@ import ( // If the output file is already exist, it will be truncated. // If the file is not exist, it will created with permission set to user's // read-write only. +// +// DEPRECATED: moved to lib/os#Copy. func Copy(out, in string) (err error) { fin, err := os.Open(in) if err != nil { diff --git a/lib/io/is.go b/lib/io/is.go index d5d280ea..371de602 100644 --- a/lib/io/is.go +++ b/lib/io/is.go @@ -15,6 +15,8 @@ import ( // IsBinary will return true if content of file is binary. // If file is not exist or there is an error when reading or closing the file, // it will return false. +// +// DEPRECATED: moved to [lib/os#IsBinary]. func IsBinary(file string) bool { var ( total int @@ -59,6 +61,8 @@ func IsBinary(file string) bool { // IsDirEmpty will return true if directory is not exist or empty; otherwise // it will return false. +// +// DEPRECATED: moved to [lib/os#IsDirEmpty]. func IsDirEmpty(dir string) (ok bool) { d, err := os.Open(dir) if err != nil { @@ -80,6 +84,8 @@ func IsDirEmpty(dir string) (ok bool) { // IsFileExist will return true if relative path is exist on parent directory; // otherwise it will return false. +// +// DEPRECATED: moved to [lib/os#IsFileExist]. func IsFileExist(parent, relpath string) bool { path := filepath.Join(parent, relpath) diff --git a/lib/os/example_test.go b/lib/os/example_test.go new file mode 100644 index 00000000..00c82638 --- /dev/null +++ b/lib/os/example_test.go @@ -0,0 +1,19 @@ +// Copyright 2023, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os_test + +import ( + "fmt" + + libos "github.com/shuLhan/share/lib/os" +) + +func ExampleIsBinary() { + fmt.Println(libos.IsBinary("/bin/bash")) + fmt.Println(libos.IsBinary("io.go")) + // Output: + // true + // false +} diff --git a/lib/os/os.go b/lib/os/os.go index 9eb23f64..ed2c197e 100644 --- a/lib/os/os.go +++ b/lib/os/os.go @@ -5,3 +5,167 @@ // Package os extend the standard os package to provide additional // functionalities. package os + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + + "github.com/shuLhan/share/lib/ascii" +) + +// Copy file from in to out. +// If the output file is already exist, it will be truncated. +// If the file is not exist, it will created with permission set to user's +// read-write only. +func Copy(out, in string) (err error) { + fin, err := os.Open(in) + if err != nil { + return fmt.Errorf(`Copy: failed to open input file: %s`, err) + } + + fout, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf(`Copy: failed to open output file: %s`, err) + } + + defer func() { + err := fout.Close() + if err != nil { + log.Printf(`Copy: failed to close output file: %s`, err) + } + }() + defer func() { + err := fin.Close() + if err != nil { + log.Printf(`Copy: failed to close input file: %s`, err) + } + }() + + buf := make([]byte, 1024) + for { + n, err := fin.Read(buf) + if err != nil { + if err == io.EOF { + break + } + return err + } + if n == 0 { + break + } + _, err = fout.Write(buf[:n]) + if err != nil { + return err + } + } + + return nil +} + +// IsBinary will return true if content of file is binary. +// If file is not exist or there is an error when reading or closing the file, +// it will return false. +func IsBinary(file string) bool { + var ( + total int + printable int + ) + + f, err := os.Open(file) + if err != nil { + return false + } + + content := make([]byte, 768) + + for total < 512 { + n, err := f.Read(content) + if err != nil { + break + } + + content = content[:n] + + for x := 0; x < len(content); x++ { + if ascii.IsSpace(content[x]) { + continue + } + if content[x] >= 33 && content[x] <= 126 { + printable++ + } + total++ + } + } + + err = f.Close() + if err != nil { + return false + } + + ratio := float64(printable) / float64(total) + + return ratio <= float64(0.75) +} + +// IsDirEmpty will return true if directory is not exist or empty; otherwise +// it will return false. +func IsDirEmpty(dir string) (ok bool) { + d, err := os.Open(dir) + if err != nil { + ok = true + return + } + + _, err = d.Readdirnames(1) + if err != nil { + if err == io.EOF { + ok = true + } + } + + _ = d.Close() + + return +} + +// IsFileExist will return true if relative path is exist on parent directory; +// otherwise it will return false. +func IsFileExist(parent, relpath string) bool { + path := filepath.Join(parent, relpath) + + fi, err := os.Stat(path) + if err != nil { + return false + } + if fi.IsDir() { + return false + } + return true +} + +// RmdirEmptyAll remove directory in path if it's empty until one of the +// parent is not empty. +func RmdirEmptyAll(path string) error { + if len(path) == 0 { + return nil + } + fi, err := os.Stat(path) + if err != nil { + return RmdirEmptyAll(filepath.Dir(path)) + } + if !fi.IsDir() { + return nil + } + if !IsDirEmpty(path) { + return nil + } + err = os.Remove(path) + if err != nil { + return err + } + + return RmdirEmptyAll(filepath.Dir(path)) +} diff --git a/lib/os/os_test.go b/lib/os/os_test.go index 36818767..2d0d7a3f 100644 --- a/lib/os/os_test.go +++ b/lib/os/os_test.go @@ -3,8 +3,208 @@ package os import ( "os" "testing" + + "github.com/shuLhan/share/lib/test" ) +func TestCopy(t *testing.T) { + cases := []struct { + desc string + in string + out string + expErr string + }{{ + desc: `Without output file`, + in: `testdata/Copy/input.txt`, + expErr: `Copy: failed to open output file: open : no such file or directory`, + }, { + desc: `Without input file`, + out: `testdata/Copy/output.txt`, + expErr: `Copy: failed to open input file: open : no such file or directory`, + }, { + desc: `With input and output`, + in: `testdata/Copy/input.txt`, + out: `testdata/Copy/output.txt`, + }} + + for _, c := range cases { + err := Copy(c.out, c.in) + if err != nil { + test.Assert(t, c.desc, c.expErr, err.Error()) + continue + } + + exp, err := os.ReadFile(c.in) + if err != nil { + t.Fatal(err) + } + + got, err := os.ReadFile(c.out) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.desc, string(exp), string(got)) + } +} + +func TestIsDirEmpty(t *testing.T) { + emptyDir := "testdata/dirempty" + err := os.MkdirAll(emptyDir, 0700) + if err != nil { + t.Fatal(err) + } + + cases := []struct { + desc string + path string + exp bool + }{{ + desc: `With dir not exist`, + path: `testdata/notexist`, + exp: true, + }, { + desc: `With dir exist and not empty`, + path: `testdata`, + }, { + desc: `With dir exist and empty`, + path: `testdata/dirempty`, + exp: true, + }} + + for _, c := range cases { + t.Log(c.desc) + + got := IsDirEmpty(c.path) + + test.Assert(t, "", c.exp, got) + } +} + +func TestIsFileExist(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + cases := []struct { + desc, parent, relpath string + exp bool + }{{ + desc: "With directory", + relpath: "testdata", + }, { + desc: "With non existen path", + parent: "/random", + relpath: "file", + }, { + desc: "With file exist without parent", + relpath: "testdata/.empty", + exp: true, + }, { + desc: "With file exist", + parent: wd, + relpath: "testdata/.empty", + exp: true, + }} + + for _, c := range cases { + t.Log(c.desc) + + got := IsFileExist(c.parent, c.relpath) + + test.Assert(t, "", c.exp, got) + } +} + +func TestRmdirEmptyAll(t *testing.T) { + t.Cleanup(func() { + _ = os.Remove(`testdata/file`) + _ = os.RemoveAll(`testdata/a`) + _ = os.RemoveAll(`testdata/dirempty`) + }) + + cases := []struct { + desc string + createDir string + createFile string + path string + expExist string + expNotExist string + }{{ + desc: `With path as file`, + path: `testdata/file`, + createFile: `testdata/file`, + expExist: `testdata/file`, + }, { + desc: `With empty path`, + createDir: `testdata/a/b/c/d`, + expExist: `testdata/a/b/c/d`, + }, { + desc: `With non empty at middle`, + createDir: `testdata/a/b/c/d`, + createFile: `testdata/a/b/file`, + path: `testdata/a/b/c/d`, + expExist: `testdata/a/b/file`, + expNotExist: `testdata/a/b/c`, + }, { + desc: `With first path not exist`, + createDir: `testdata/a/b/c`, + path: `testdata/a/b/c/d`, + expExist: `testdata/a/b/file`, + expNotExist: `testdata/a/b/c`, + }, { + desc: `With non empty at parent`, + createDir: `testdata/dirempty/a/b/c/d`, + path: `testdata/dirempty/a/b/c/d`, + expExist: `testdata`, + expNotExist: `testdata/dirempty`, + }} + + var ( + err error + f *os.File + ) + for _, c := range cases { + t.Log(c.desc) + + if len(c.createDir) > 0 { + err = os.MkdirAll(c.createDir, 0700) + if err != nil { + t.Fatal(err) + } + } + if len(c.createFile) > 0 { + f, err = os.Create(c.createFile) + if err != nil { + t.Fatal(err) + } + err = f.Close() + if err != nil { + t.Fatal(err) + } + } + + err = RmdirEmptyAll(c.path) + if err != nil { + t.Fatal(err) + } + + if len(c.expExist) > 0 { + _, err = os.Stat(c.expExist) + if err != nil { + t.Fatal(err) + } + } + if len(c.expNotExist) > 0 { + _, err = os.Stat(c.expNotExist) + if !os.IsNotExist(err) { + t.Fatal(err) + } + } + } +} + // TestStat test to see the difference between Stat and Lstat. func TestStat(t *testing.T) { t.Skip() diff --git a/lib/os/testdata/.empty b/lib/os/testdata/.empty new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/lib/os/testdata/.empty diff --git a/lib/os/testdata/Copy/input.txt b/lib/os/testdata/Copy/input.txt new file mode 100644 index 00000000..46820079 --- /dev/null +++ b/lib/os/testdata/Copy/input.txt @@ -0,0 +1,27 @@ +Copyright (c) 2018 M. Shulhan (ms@kilabit.info). All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of M. Shulhan, nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
