mirror of https://github.com/aldy505/cheapcash.git
feat: initialize
This commit is contained in:
commit
9d74097a50
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Reinaldy Rafli <aldy505@tutanota.com>
|
||||
|
||||
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.
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue