commit 9d74097a50d76c74437020107f5f62b18685c647 Author: Reinaldy Rafli Date: Tue Nov 16 11:56:33 2021 +0700 feat: initialize diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..030c6dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 80 + +[*.go] +indent_size = 2 +indent_style = "tabs" +tab_width = 4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5595772 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: [push, pull_request] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.17.x + - name: Go vet + run: go vet -v + - name: Go test + run: go test -v -race -coverprofile=coverage.out -covermode=atomic -failfast + - uses: github/codeql-action/init@v1 + with: + languages: go + - uses: github/codeql-action/analyze@v1 + - uses: codecov/codecov-action@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0267ffa --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go Patch ### +/vendor/ +/Godeps/ + +# Environment variables +.env diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7fe0727 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Reinaldy Rafli + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed4c5db --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Cheapcash + +[![Go Reference](https://pkg.go.dev/badge/github.com/aldy505/cheapcash.svg)](https://pkg.go.dev/github.com/aldy505/cheapcash) [![Go Report Card](https://goreportcard.com/badge/github.com/aldy505/cheapcash)](https://goreportcard.com/report/github.com/aldy505/cheapcash) ![GitHub](https://img.shields.io/github/license/aldy505/cheapcash) [![CodeFactor](https://www.codefactor.io/repository/github/aldy505/cheapcash/badge)](https://www.codefactor.io/repository/github/aldy505/cheapcash) [![codecov](https://codecov.io/gh/aldy505/cheapcash/branch/master/graph/badge.svg?token=Noeexg5xEJ)](https://codecov.io/gh/aldy505/cheapcash) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/9b78970127c74c1a923533e05f65848d)](https://www.codacy.com/gh/aldy505/cheapcash/dashboard?utm_source=github.com&utm_medium=referral&utm_content=aldy505/cheapcash&utm_campaign=Badge_Grade) [![Test and coverage](https://github.com/aldy505/cheapcash/actions/workflows/ci.yml/badge.svg)](https://github.com/aldy505/cheapcash/actions/workflows/ci.yml) + +SSD is cheap. Why don't we use it for caching? + +## Install + +```go +import "github.com/aldy505/cheapcash" +``` + +## Usage + +It has simple API for reading & storing cache. + +```go +package main + +import ( + "log" + + "github.com/aldy505/cheapcash" +) + +func main() { + // Create a Cheapcash instance. + // Of course you can make multiple instance for multiple + // root directories. + cache := cheapcash.New("/tmp/cheapcash") + // or if you are feeling lazy + cache = cheapcash.Default() + // path defaults to /tmp/cheapcash + + err := cache.Write("users:list", usersList) + if err != nil { + log.Fatal(err) + } + + val, err := cache.Read("users:list") + if err != nil { + log.Fatal(err) + } + + log.Println(string(val)) + + err = cache.Append("users:list", []byte("\nMarcel")) + if err != nil { + + } + +} diff --git a/append.go b/append.go new file mode 100644 index 0000000..2f18c14 --- /dev/null +++ b/append.go @@ -0,0 +1,27 @@ +package cheapcash + +import "os" + +func (c *Cache) Append(key string, value []byte) error { + check, err := c.Exists(c.Path + key) + if err != nil { + return err + } + + if !check { + return ErrNotExists + } + + file, err := os.OpenFile(sanitizePath(c.Path+key), os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write(value) + if err != nil { + return err + } + + return nil +} diff --git a/append_test.go b/append_test.go new file mode 100644 index 0000000..10eb215 --- /dev/null +++ b/append_test.go @@ -0,0 +1,33 @@ +package cheapcash_test + +import ( + "math/rand" + "strconv" + "testing" + + "github.com/aldy505/cheapcash" +) + +func TestAppend(t *testing.T) { + rand := strconv.Itoa(rand.Int()) + c := cheapcash.Default() + + err := c.Write(rand, []byte("Hello")) + if err != nil { + t.Error("an error was thrown:", err) + } + + err = c.Append(rand, []byte("World")) + if err != nil { + t.Error("an error was thrown:", err) + } + + r, err := c.Read(rand) + if err != nil { + t.Error("an error was thrown:", err) + } + + if string(r) != "HelloWorld" { + t.Errorf("expected %s, got %v", "HelloWorld", string(r)) + } +} diff --git a/cheapcash.go b/cheapcash.go new file mode 100644 index 0000000..fc75eb6 --- /dev/null +++ b/cheapcash.go @@ -0,0 +1,28 @@ +package cheapcash + +import ( + "errors" + + "sync" +) + +type Cache struct { + sync.Mutex + Path string +} + +var ErrNotExists = errors.New("key does not exist") +var ErrInvalidPath = errors.New("path supplied is invalid") +var ErrDiskFull = errors.New("there was no space left on the device") + +func Default() *Cache { + return &Cache{ + Path: "/tmp/cheapcash/", + } +} + +func New(path string) *Cache { + return &Cache{ + Path: path, + } +} diff --git a/cheapcash_test.go b/cheapcash_test.go new file mode 100644 index 0000000..0bd1cda --- /dev/null +++ b/cheapcash_test.go @@ -0,0 +1,42 @@ +package cheapcash_test + +import ( + "log" + "os" + "testing" + + "github.com/aldy505/cheapcash" +) + +func TestMain(m *testing.M) { + removeDirIfExists("/tmp/cheapcash") + defer removeDirIfExists("/tmp/cheapcash") + + os.Exit(m.Run()) +} + +func TestDefault(t *testing.T) { + c := cheapcash.Default() + if c.Path != "/tmp/cheapcash/" { + t.Error("expected path to return /tmp/cheapcash/, got:", c.Path) + } +} + +func TestNew(t *testing.T) { + c := cheapcash.New("/somewhere") + if c.Path != "/somewhere" { + t.Error("expected path to return /somewhere, got:", c.Path) + } +} + +func removeDirIfExists(path string) { + dir, err := os.Stat(path) + if err == nil { + if dir.IsDir() { + err = os.RemoveAll(path) + if err != nil { + log.Fatal("unable to remove temp directory:", err) + } + } + } +} diff --git a/check.go b/check.go new file mode 100644 index 0000000..1916623 --- /dev/null +++ b/check.go @@ -0,0 +1,66 @@ +package cheapcash + +import ( + "errors" + "io/fs" + "os" + "strings" +) + +// Check whether or not a key exists. +// Returns true if the key exists, false otherwise. +// +// WARNING: You should provide your c.Path value yourself. +// +// check, err := cache.Exists("something.txt") +// // will search in ./something.txt +// +// check, err = cache.Exists(c.Path + "something.txt") +// // will search relative to c.Path value +func (c *Cache) Exists(key string) (bool, error) { + file, err := os.Open(sanitizePath(key)) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + + return false, err + } + defer file.Close() + + return true, nil +} + +func checkDir(path string) error { + // Remove / from path + path = strings.TrimSuffix(path, "/") + + dir, err := os.Stat(path) + if err == nil { + if dir.IsDir() { + return nil + } + } + + // Create directory with a loop + separated := strings.Split(path, "/") + + for i := 0; i < len(separated); i++ { + if separated[i] == "" { + os.Chdir("/") + continue + } else { + os.Chdir(separated[i-1]) + } + + err = os.Mkdir(separated[i], fs.ModePerm) + if err != nil { + if errors.Is(err, os.ErrExist) { + continue + } + return err + } + } + + return nil +} diff --git a/check_test.go b/check_test.go new file mode 100644 index 0000000..23121bc --- /dev/null +++ b/check_test.go @@ -0,0 +1,115 @@ +package cheapcash_test + +import ( + "math/rand" + "strconv" + "testing" + + "github.com/aldy505/cheapcash" +) + +func TestExists(t *testing.T) { + rand := strconv.Itoa(rand.Int()) + c := cheapcash.Default() + b, err := c.Exists(c.Path + "/" + rand) + if err != nil { + t.Error("an error was thrown:", err) + } + + if b == true { + t.Error("expected false, got true") + } + + err = c.Write(rand, []byte("value")) + if err != nil { + t.Error("an error was thrown:", err) + } + + b, err = c.Exists(c.Path + "/" + rand) + if err != nil { + t.Error("an error was thrown:", err) + } + + if b == false { + t.Error("expected true, got false") + } +} + +func TestExists_Conccurency(t *testing.T) { + rand := strconv.Itoa(rand.Int()) + c := cheapcash.Default() + + res := make(chan bool, 5) + + go func() { + b, err := c.Exists(c.Path + "/" + rand) + if err != nil { + t.Error("an error was thrown:", err) + } + if b == true { + t.Error("expected false, got true") + } + res <- true + }() + + go func() { + b, err := c.Exists(c.Path + "/" + rand) + if err != nil { + t.Error("an error was thrown:", err) + } + if b == true { + t.Error("expected false, got true") + } + res <- true + }() + + go func() { + b, err := c.Exists(c.Path + "/" + rand) + if err != nil { + t.Error("an error was thrown:", err) + } + if b == true { + t.Error("expected false, got true") + } + res <- true + }() + + go func() { + b, err := c.Exists(c.Path + "/" + rand) + if err != nil { + t.Error("an error was thrown:", err) + } + if b == true { + t.Error("expected false, got true") + } + res <- true + }() + + go func() { + b, err := c.Exists(c.Path + "/" + rand) + if err != nil { + t.Error("an error was thrown:", err) + } + if b == true { + t.Error("expected false, got true") + } + res <- true + }() + + go func() { + b, err := c.Exists(c.Path + "/" + rand) + if err != nil { + t.Error("an error was thrown:", err) + } + if b == true { + t.Error("expected false, got true") + } + res <- true + }() + + <-res + <-res + <-res + <-res + <-res +} diff --git a/delete.go b/delete.go new file mode 100644 index 0000000..92e481d --- /dev/null +++ b/delete.go @@ -0,0 +1,21 @@ +package cheapcash + +import "os" + +func (c *Cache) Delete(key string) error { + check, err := c.Exists(c.Path + "/" + key) + if err != nil { + return err + } + + if !check { + return ErrNotExists + } + + err = os.Remove(sanitizePath(c.Path + "/" + key)) + if err != nil { + return err + } + + return nil +} diff --git a/delete_test.go b/delete_test.go new file mode 100644 index 0000000..c1a1cb1 --- /dev/null +++ b/delete_test.go @@ -0,0 +1,53 @@ +package cheapcash_test + +import ( + "errors" + "math/rand" + "strconv" + "sync" + "testing" + + "github.com/aldy505/cheapcash" +) + +func TestDelete(t *testing.T) { + rand := strconv.Itoa(rand.Int()) + c := cheapcash.Default() + err := c.Write(rand, []byte("value")) + if err != nil { + t.Error("an error was thrown:", err) + } + + err = c.Delete(rand) + if err != nil { + t.Error("an error was thrown:", err) + } +} + +func TestDelete_Conccurency(t *testing.T) { + rand := strconv.Itoa(rand.Int()) + c := cheapcash.Default() + err := c.Write(rand, []byte("value")) + if err != nil { + t.Error("an error was thrown:", err) + } + + var wg sync.WaitGroup + + deleteFunc := func() { + err = c.Delete(rand) + if err != nil && !errors.Is(err, cheapcash.ErrNotExists) { + t.Error("an error was thrown:", err) + } + wg.Done() + } + + wg.Add(5) + go deleteFunc() + go deleteFunc() + go deleteFunc() + go deleteFunc() + go deleteFunc() + + wg.Wait() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7aa438e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/aldy505/cheapcash + +go 1.17 diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..43c861d --- /dev/null +++ b/reader.go @@ -0,0 +1,30 @@ +package cheapcash + +import ( + "io/ioutil" + "os" +) + +func (c *Cache) Read(key string) ([]byte, error) { + check, err := c.Exists(c.Path + key) + if err != nil { + return []byte{}, err + } + + if !check { + return []byte{}, ErrNotExists + } + + file, err := os.Open(sanitizePath(c.Path + key)) + if err != nil { + return []byte{}, err + } + defer file.Close() + + data, err := ioutil.ReadAll(file) + if err != nil { + return []byte{}, err + } + + return data, nil +} diff --git a/reader_test.go b/reader_test.go new file mode 100644 index 0000000..39b69af --- /dev/null +++ b/reader_test.go @@ -0,0 +1,59 @@ +package cheapcash_test + +import ( + "math/rand" + "strconv" + "sync" + "testing" + + "github.com/aldy505/cheapcash" +) + +func TestRead(t *testing.T) { + rand := strconv.Itoa(rand.Int()) + c := cheapcash.Default() + err := c.Write(rand, []byte("value")) + if err != nil { + t.Error("an error was thrown:", err) + } + + v, err := c.Read(rand) + if err != nil { + t.Error("an error was thrown:", err) + } + if string(v) != "value" { + t.Errorf("expected %s, got %v", "value", v) + } +} + +func TestRead_Conccurency(t *testing.T) { + rand := strconv.Itoa(rand.Int()) + c := cheapcash.Default() + + err := c.Write(rand, []byte("value")) + if err != nil { + t.Error("an error was thrown:", err) + } + + var wg sync.WaitGroup + + readFunc := func(){ + r, err := c.Read(rand) + if err != nil { + t.Error("an error was thrown:", err) + } + if string(r) != "value" { + t.Error("expected value, got:", string(r)) + } + wg.Done() + } + + wg.Add(5) + go readFunc() + go readFunc() + go readFunc() + go readFunc() + go readFunc() + + wg.Wait() +} diff --git a/sanitize.go b/sanitize.go new file mode 100644 index 0000000..c981c5c --- /dev/null +++ b/sanitize.go @@ -0,0 +1,26 @@ +package cheapcash + +import "strings" + +type restriction struct { + Key string + ReplaceWith string +} + +func sanitizePath(path string) string { + restricted := []restriction{ + {Key: " ", ReplaceWith: "_s_"}, + {Key: "^", ReplaceWith: "_p_"}, + {Key: "*", ReplaceWith: "_a_"}, + {Key: "\"", ReplaceWith: "_dq_"}, + {Key: "'", ReplaceWith: "_sq_"}, + {Key: "?", ReplaceWith: "_qm_"}, + {Key: ">", ReplaceWith: "_gt_"}, + {Key: "<", ReplaceWith: "_lt_"}, + } + + for _, v := range restricted { + path = strings.ReplaceAll(path, v.Key, v.ReplaceWith) + } + return path +} diff --git a/writer.go b/writer.go new file mode 100644 index 0000000..74318dc --- /dev/null +++ b/writer.go @@ -0,0 +1,35 @@ +package cheapcash + +import "os" + +func (c *Cache) Write(key string, value []byte) error { + err := checkDir(sanitizePath(c.Path)) + if err != nil { + return err + } + + check, err := c.Exists(key) + if err != nil { + return err + } + + if check { + err = c.Delete(key) + if err != nil { + return err + } + } + + file, err := os.Create(sanitizePath(c.Path + key)) + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write(value) + if err != nil { + return err + } + + return nil +} diff --git a/writer_test.go b/writer_test.go new file mode 100644 index 0000000..bbda709 --- /dev/null +++ b/writer_test.go @@ -0,0 +1,39 @@ +package cheapcash_test + +import ( + "sync" + "testing" + + "github.com/aldy505/cheapcash" +) + +func TestWrite(t *testing.T) { + c := cheapcash.Default() + err := c.Write("key", []byte("value")) + if err != nil { + t.Error("an error was thrown:", err) + } +} + +func TestWrite_Conccurency(t *testing.T) { + c := cheapcash.Default() + + var wg sync.WaitGroup + + writeFunc := func() { + err := c.Write("key1", []byte("value1")) + if err != nil { + t.Error("an error was thrown:", err) + } + wg.Done() + } + + wg.Add(5) + go writeFunc() + go writeFunc() + go writeFunc() + go writeFunc() + go writeFunc() + + wg.Wait() +}