summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2023-04-05 23:44:02 +0700
committerShulhan <ms@kilabit.info>2023-04-08 16:56:26 +0700
commitd8ed8a6b7d788eb6bb59023e6e6fe93190b8a2c8 (patch)
treeadaca1b1e11401e52624ae13ca39dceb95a83cc0
parent78e301986dceacc8daa7cb3f549859a005b2b9b8 (diff)
downloadpakakeh.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--.gitignore1
-rw-r--r--lib/io/dir.go2
-rw-r--r--lib/io/io.go2
-rw-r--r--lib/io/is.go6
-rw-r--r--lib/os/example_test.go19
-rw-r--r--lib/os/os.go164
-rw-r--r--lib/os/os_test.go200
-rw-r--r--lib/os/testdata/.empty0
-rw-r--r--lib/os/testdata/Copy/input.txt27
9 files changed, 421 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 2f044a93..d1f10d7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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.