├── cmd └── main.go ├── Makefile ├── .gitignore ├── .github └── workflows │ └── build.yml ├── internal ├── utils │ ├── parseduration.go │ └── parseduration_test.go ├── store │ ├── setup.go │ ├── disk.go │ └── setup_test.go ├── server │ └── start.go └── process │ └── process.go └── README.md /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | s "paar/internal/server" 5 | ) 6 | 7 | func main() { 8 | server := s.NewServer(":8081") 9 | err := server.Start() 10 | if err != nil { 11 | panic(err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @go build -o bin/paar cmd/main.go 3 | 4 | run:build 5 | @bin/paar 6 | 7 | test: 8 | @go test ./... 9 | 10 | testv: 11 | @go test -v ./... 12 | 13 | testcover: 14 | @go test -cover ./... 15 | 16 | testrace: 17 | @go test -race ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Бинарные файлы 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Кэш и объектные файлы 9 | *.obj 10 | *.o 11 | *.a 12 | *.lo 13 | *.la 14 | *.lai 15 | *.rej 16 | *~ 17 | 18 | # Папка с зависимостями 19 | vendor/ 20 | 21 | # Лог файлы и профайлы 22 | *.log 23 | *.test 24 | *.out 25 | *.prof 26 | 27 | # Кэширование модуля 28 | go.sum 29 | go.mod 30 | 31 | # Папка с кэшем и временные файлы 32 | *.tmp 33 | *.swp 34 | *.swo 35 | .idea/ 36 | .vscode/ 37 | .DS_Store 38 | 39 | # Тестовые данные 40 | testdata/ 41 | data.json 42 | 43 | # Сборки 44 | bin/ 45 | build/ 46 | 47 | # Примеры, если есть 48 | examples/ 49 | 50 | # Папка с кэшом модулей Go 51 | GOPATH/pkg/mod/ 52 | 53 | # Конфигурационные файлы 54 | .env 55 | config.yaml 56 | 57 | # IDE специфические файлы 58 | *.iml 59 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image 2 | 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - main 9 | 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | - name: Install nixpacks 18 | run: curl -sSL https://nixpacks.com/install.sh | bash 19 | - name: Build Docker image with Nixpacks 20 | run: nixpacks build . --name c1b62bb7-c23a-42ed-b8b8-06d1fb4205fd:${{ github.sha }} 21 | - name: Push Docker image 22 | run: docker tag c1b62bb7-c23a-42ed-b8b8-06d1fb4205fd:${{ github.sha }} ${{ secrets.REGISTERY_URL }}/c1b62bb7-c23a-42ed-b8b8-06d1fb4205fd:${{ github.sha }} && docker push ${{ secrets.REGISTERY_URL }}/c1b62bb7-c23a-42ed-b8b8-06d1fb4205fd:${{ github.sha }} 23 | - name: Notify server about new version 24 | run: curl -X GET ${{ secrets.NOTIFICATION_URL }}/c1b62bb7-c23a-42ed-b8b8-06d1fb4205fd:${{ github.sha }} 25 | 26 | 27 | -------------------------------------------------------------------------------- /internal/utils/parseduration.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | func ParseDuration(durationStr string) (time.Duration, error) { 10 | var totalDuration time.Duration 11 | // Регулярное выражение для извлечения чисел и их единиц измерения (s, m, h, d) 12 | re := regexp.MustCompile(`(\d+)([smhd])`) 13 | matches := re.FindAllStringSubmatch(durationStr, -1) 14 | 15 | // Парсинг найденных подстрок и суммирование их в общую продолжительность 16 | for _, match := range matches { 17 | value, err := strconv.Atoi(match[1]) 18 | if err != nil { 19 | return 0, err 20 | } 21 | 22 | switch match[2] { 23 | case "s": 24 | totalDuration += time.Duration(value) * time.Second 25 | case "m": 26 | totalDuration += time.Duration(value) * time.Minute 27 | case "h": 28 | totalDuration += time.Duration(value) * time.Hour 29 | case "d": 30 | totalDuration += time.Duration(value*24) * time.Hour 31 | } 32 | } 33 | 34 | return totalDuration, nil 35 | } -------------------------------------------------------------------------------- /internal/store/setup.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type Values struct { 9 | Value string 10 | ExpireTo time.Time 11 | } 12 | 13 | type Storage struct { 14 | m sync.Map 15 | } 16 | 17 | func NewStorage() *Storage { 18 | return &Storage{ 19 | m: sync.Map{}, 20 | } 21 | } 22 | 23 | func (s *Storage) GetMap() *sync.Map { 24 | return &s.m 25 | } 26 | 27 | func (s *Storage) Initialize(m map[string]Values) { 28 | s.m = sync.Map{} 29 | for k, v := range m { 30 | s.m.Store(k, v) 31 | } 32 | } 33 | 34 | func (s *Storage) Load(key string) (Values, bool) { 35 | v, ok := s.m.Load(key) 36 | if !ok { 37 | return Values{}, ok 38 | } 39 | 40 | str, ok := v.(Values) 41 | if !ok { 42 | return Values{}, ok 43 | } 44 | 45 | return str, ok 46 | } 47 | 48 | func (s *Storage) Store(key string, value Values) { 49 | s.m.Store(key, value) 50 | } 51 | 52 | func (s *Storage) Delete(key string) { 53 | s.m.Delete(key) 54 | } 55 | 56 | func (s *Storage) Range(f func(key string, value Values) bool) { 57 | s.m.Range(func(key, value interface{}) bool { 58 | return f(key.(string), value.(Values)) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /internal/store/disk.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | type Disk struct{ 10 | sm *sync.Map 11 | } 12 | 13 | func NewDisk(sm *sync.Map) *Disk { 14 | return &Disk{ 15 | sm: sm, 16 | } 17 | } 18 | 19 | type DataDTO struct { 20 | Data map[string]Values 21 | } 22 | 23 | 24 | func (d *Disk) syncMapToMap(syncMap *sync.Map) map[string]Values { 25 | result := make(map[string]Values) 26 | syncMap.Range(func(key, value interface{}) bool { 27 | k, ok1 := key.(string) 28 | if !ok1 { 29 | return false 30 | } 31 | result[k] = value.(Values) 32 | return true 33 | }) 34 | return result 35 | } 36 | 37 | func (d *Disk) Load(path string) (map[string]Values, error) { 38 | if !fileExists(path) { 39 | return make(map[string]Values), nil 40 | } 41 | file, err := os.Open(path) 42 | if err != nil { 43 | return make(map[string]Values) ,err 44 | } 45 | defer file.Close() 46 | 47 | var m DataDTO 48 | err = json.NewDecoder(file).Decode(&m) 49 | if err != nil { 50 | return make(map[string]Values), err 51 | } 52 | return m.Data, nil 53 | } 54 | 55 | func fileExists(filename string) bool { 56 | info, err := os.Stat(filename) 57 | if os.IsNotExist(err) { 58 | return false 59 | } 60 | return !info.IsDir() 61 | } 62 | 63 | func (d *Disk) Save(path string) error { 64 | m := DataDTO{ 65 | Data: d.syncMapToMap(d.sm), 66 | } 67 | jsonData, err := json.Marshal(m) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | file, err := os.Create(path) 73 | if err != nil { 74 | return err 75 | } 76 | defer file.Close() 77 | 78 | _, err = file.Write(jsonData) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | return nil 84 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paar 2 | 3 | Paar - это простое key/value хранилище, реализующее команды GET, SET, DELETE, EXPIRE и другие, с возможностью сохранения данных на диск. 4 | 5 | ## Описание 6 | 7 | Paar был создан для улучшения навыков программирования на Go. Этот проект представляет собой простое и эффективное хранилище ключ-значение, которое можно использовать для различных задач хранения данных. Paar поддерживает основные команды для работы с данными и позволяет сохранять состояние на диск для долговременного хранения. 8 | 9 | ## Возможности 10 | 11 | - **GET**: Получить значение по ключу. 12 | - **SET**: Установить значение по ключу. 13 | - **DELETE**: Удалить значение по ключу. 14 | - **EXPIRE**: Установить время жизни для ключа. 15 | 16 | ## Установка 17 | 18 | Для установки Paar потребуется Go версии 1.20 или выше. 19 | 20 | ```sh 21 | git clone https://github.com/ethaningenium/paar.git 22 | cd paar 23 | make run 24 | ``` 25 | 26 | ## Использование 27 | 28 | ## Команды 29 | 30 | - **GET key**: Возвращает значение, связанное с `key`. 31 | - **SET key value**: Устанавливает `value` для `key`. 32 | - **DELETE key**: Удаляет значение, связанное с `key`. 33 | - **EXPIRE key 1s1m1h1d**: Устанавливает время жизни ключа `key` в секундах/минутах/часах/дней. 34 | 35 | ## Вклад 36 | 37 | Вклад в проект приветствуется! Пожалуйста, создайте форк репозитория и отправьте PR с вашими изменениями. 38 | 39 | --- 40 | 41 | Спасибо за использование Paar! Если у вас есть какие-либо вопросы или предложения, не стесняйтесь обращаться. 42 | 43 | ``` 44 | 45 | Этот README.md файл включает основную информацию о проекте, его установке, использовании и доступных командах. Вы можете адаптировать его в соответствии с вашими потребностями и особенностями вашего проекта. 46 | ``` 47 | -------------------------------------------------------------------------------- /internal/utils/parseduration_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "paar/internal/utils" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestParseDuration(t *testing.T) { 10 | // 1s 11 | duration, err := utils.ParseDuration("1s") 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | if duration!= time.Second { 16 | t.Errorf("duration is not 1s, but %s", duration) 17 | } 18 | // 11s 19 | duration, err = utils.ParseDuration("11s") 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | if duration!= time.Second * 11 { 24 | t.Errorf("duration is not 11s, but %s", duration) 25 | } 26 | 27 | // 1m 28 | duration, err = utils.ParseDuration("1m") 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | if duration!= time.Minute { 33 | t.Errorf("duration is not 1m, but %s", duration) 34 | } 35 | // 11m 36 | duration, err = utils.ParseDuration("11m") 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | if duration!= time.Minute *11 { 41 | t.Errorf("duration is not 11m, but %s", duration) 42 | } 43 | 44 | // 1h 45 | duration, err = utils.ParseDuration("1h") 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | if duration!= time.Hour { 50 | t.Errorf("duration is not 1h, but %s", duration) 51 | } 52 | // 11h 53 | duration, err = utils.ParseDuration("11h") 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | if duration!= time.Hour *11 { 58 | t.Errorf("duration is not 11h, but %s", duration) 59 | } 60 | 61 | // 1d 62 | duration, err = utils.ParseDuration("1d") 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | if duration!= 24*time.Hour { 67 | t.Errorf("duration is not 1d, but %s", duration) 68 | } 69 | 70 | // 11d 71 | duration, err = utils.ParseDuration("11d") 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | if duration!= 24*11*time.Hour { 76 | t.Errorf("duration is not 11d, but %s", duration) 77 | } 78 | } -------------------------------------------------------------------------------- /internal/store/setup_test.go: -------------------------------------------------------------------------------- 1 | package store_test 2 | 3 | import ( 4 | "paar/internal/store" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestStore(t *testing.T) { 10 | storage := store.NewStorage() 11 | infinite := time.Now().Add(time.Duration(1<<63 - 1)) 12 | 13 | //Set key="key1" value="value1" 14 | storage.Store("key1", store.Values{Value: "value1", ExpireTo: infinite}) 15 | 16 | //Get key="key1" 17 | value, ok := storage.Load("key1") 18 | if!ok { 19 | t.Errorf("key1 not found") 20 | } 21 | if value.Value!= "value1" { 22 | t.Errorf("value1 expected, but %s", value.Value) 23 | } 24 | if value.ExpireTo.Before(time.Now()) { 25 | t.Errorf("key1 expired") 26 | } 27 | 28 | //Delete key="key1" 29 | storage.Delete("key1") 30 | 31 | //Get key="key1" 32 | value, ok = storage.Load("key1") 33 | if ok { 34 | t.Errorf("key1 not deleted") 35 | } 36 | if value.Value != "" { 37 | t.Errorf("value1 expected, but %s", value.Value) 38 | } 39 | 40 | //Set 3 keys 41 | storage.Store("key2", store.Values{Value: "value2", ExpireTo: infinite}) 42 | storage.Store("key3", store.Values{Value: "value3", ExpireTo: infinite}) 43 | storage.Store("key4", store.Values{Value: "value4", ExpireTo: infinite}) 44 | 45 | //Range 46 | keys := map[string]store.Values{ 47 | "key2": {Value: "value2", ExpireTo: infinite}, 48 | "key3": {Value: "value3", ExpireTo: infinite}, 49 | "key4": {Value: "value4", ExpireTo: infinite}, 50 | } 51 | result := make(map[string]store.Values) 52 | storage.Range(func(key string, value store.Values) bool { 53 | if value.ExpireTo.Before(time.Now()) { 54 | t.Errorf("key %s expired", key) 55 | }else{ 56 | result[key] = value 57 | } 58 | return true 59 | }) 60 | if len(keys)!= 3 { 61 | t.Errorf("result expected 3, but %d", len(keys)) 62 | } 63 | 64 | for k, v := range keys { 65 | if k == "key2" { 66 | if v.Value!= "value2" { 67 | t.Errorf("value2 expected, but %s", v.Value) 68 | } 69 | }else if k == "key3" { 70 | if v.Value!= "value3" { 71 | t.Errorf("value3 expected, but %s", v.Value) 72 | } 73 | } else if k == "key4" { 74 | if v.Value!= "value4" { 75 | t.Errorf("value4 expected, but %s", v.Value) 76 | } 77 | }else{ 78 | t.Errorf("key %s not found", k) 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /internal/server/start.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "os/signal" 10 | "paar/internal/process" 11 | "paar/internal/store" 12 | "sync" 13 | "syscall" 14 | ) 15 | 16 | type Server struct { 17 | listeningPort string 18 | ln net.Listener 19 | quitCh chan struct{} 20 | wg sync.WaitGroup 21 | process *process.Process 22 | } 23 | 24 | func NewServer(listeningPort string) *Server { 25 | return &Server{ 26 | listeningPort: listeningPort, 27 | quitCh: make(chan struct{}), 28 | process: process.New(), 29 | } 30 | } 31 | 32 | func (s *Server) Start() error { 33 | ln, err := net.Listen("tcp", s.listeningPort) 34 | if err != nil { 35 | return err 36 | } 37 | s.ln = ln 38 | 39 | ctx, cancel := context.WithCancel(context.Background()) 40 | defer cancel() 41 | 42 | disk := store.NewDisk(s.process.Store.GetMap()) 43 | m, err := disk.Load("data.json") 44 | if err != nil { 45 | return err 46 | } 47 | s.process.Store.Initialize(m) 48 | 49 | go s.handleSignals(cancel) 50 | go s.Accept(ctx) 51 | 52 | <-s.quitCh 53 | s.ln.Close() 54 | s.wg.Wait() 55 | disk.Save("data.json") 56 | 57 | return nil 58 | } 59 | 60 | func (s *Server) handleSignals(cancel context.CancelFunc) { 61 | sigCh := make(chan os.Signal, 1) 62 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 63 | <-sigCh 64 | cancel() 65 | close(s.quitCh) 66 | } 67 | 68 | func (s *Server) Accept(ctx context.Context) { 69 | for { 70 | conn, err := s.ln.Accept() 71 | if err != nil { 72 | select { 73 | case <-ctx.Done(): 74 | return 75 | default: 76 | fmt.Println("Error accepting connection:", err) 77 | continue 78 | } 79 | } 80 | fmt.Println("Accepted connection from", conn.RemoteAddr()) 81 | s.wg.Add(1) 82 | go s.handle(ctx, conn) 83 | 84 | } 85 | } 86 | 87 | func (s *Server) handle(ctx context.Context, conn net.Conn) { 88 | defer s.wg.Done() 89 | defer conn.Close() 90 | buf := make([]byte, 1024) 91 | for { 92 | select { 93 | case <-ctx.Done(): 94 | return 95 | default: 96 | n, err := conn.Read(buf) 97 | if err != nil { 98 | if err == io.EOF { 99 | fmt.Println("Client closed the connection") 100 | return 101 | } 102 | fmt.Println("Error reading from connection:", err) 103 | return 104 | } 105 | fmt.Println("Received data:", string(buf[:n])) 106 | s.process.Handle(conn, buf[:n]) 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /internal/process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "paar/internal/store" 6 | "paar/internal/utils" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type Process struct { 12 | Store *store.Storage 13 | } 14 | 15 | func New() *Process{ 16 | return &Process{ 17 | Store: store.NewStorage(), 18 | } 19 | } 20 | 21 | 22 | 23 | type Conn interface { 24 | Write([]byte) (int, error) 25 | Close() error 26 | } 27 | 28 | func (p *Process) Handle(conn Conn, buf []byte) { 29 | command := strings.Fields(string(buf)) 30 | 31 | switch command[0] { 32 | case "GET": 33 | p.handleGet(conn, command) 34 | return 35 | case "SET": 36 | p.handleSet(conn, command) 37 | return 38 | case "DEL": 39 | p.handleDel(conn, command) 40 | return 41 | case "KEYS": 42 | p.handleKeys(conn, command) 43 | return 44 | case "QUIT": 45 | p.handleQuit(conn, command) 46 | return 47 | case "PING": 48 | p.handlePing(conn, command) 49 | return 50 | case "EXPIRE": 51 | p.handleExpire(conn, command) 52 | return 53 | default: 54 | conn.Write([]byte(fmt.Sprintf("UNKNOWN COMMAND %s\n", command[0]))) 55 | return 56 | } 57 | } 58 | 59 | func (p *Process) handlePing(conn Conn, command []string) { 60 | conn.Write([]byte("PONG\n")) 61 | } 62 | 63 | func (p *Process) handleGet(conn Conn, command []string) { 64 | if len(command) < 2 { 65 | conn.Write([]byte("KEY MISSING\n")) 66 | return 67 | } 68 | key := command[1] 69 | value, ok := p.Store.Load(key) 70 | if !ok { 71 | conn.Write([]byte("NOT FOUND\n")) 72 | return 73 | } 74 | if value.ExpireTo.Before(time.Now()) { 75 | p.Store.Delete(key) 76 | conn.Write([]byte("NOT FOUND\n")) 77 | return 78 | } 79 | conn.Write([]byte(value.Value + "\n")) 80 | } 81 | 82 | func (p *Process) handleSet(conn Conn, command []string) { 83 | if len(command) < 2 { 84 | conn.Write([]byte("KEY MISSING\n")) 85 | return 86 | } 87 | key := command[1] 88 | if len(command) < 3 { 89 | conn.Write([]byte("VALUE MISSING\n")) 90 | return 91 | } 92 | value := command[2] 93 | p.Store.Store(key, store.Values{ 94 | Value: value, 95 | ExpireTo: time.Now().Add(time.Duration(1<<63 - 1)), 96 | }) 97 | conn.Write([]byte("STORED\n")) 98 | } 99 | 100 | func (p *Process) handleDel(conn Conn, command []string) { 101 | if len(command) < 2 { 102 | conn.Write([]byte("KEY MISSING\n")) 103 | return 104 | } 105 | key := command[1] 106 | p.Store.Delete(key) 107 | conn.Write([]byte("DELETED\n")) 108 | } 109 | 110 | func (p *Process) handleQuit(conn Conn, command []string) { 111 | conn.Write([]byte("OK\n")) 112 | conn.Close() 113 | } 114 | 115 | func (p *Process) handleKeys(conn Conn, command []string) { 116 | key := "" 117 | if len(command) > 1 { 118 | key = command[1] 119 | } 120 | keys := []string{} 121 | p.Store.Range(func(k string, v store.Values) bool { 122 | if strings.HasPrefix(k, key) { 123 | if v.ExpireTo.Before(time.Now()) { 124 | p.Store.Delete(k) 125 | } else { 126 | keys = append(keys, k) 127 | } 128 | } 129 | return true 130 | }) 131 | conn.Write([]byte(strings.Join(keys, "\n"))) 132 | conn.Write([]byte("\n")) 133 | } 134 | 135 | func (p *Process) handleExpire(conn Conn, command []string) { 136 | if len(command) < 3 { 137 | conn.Write([]byte("EXPIRE DATA MISSING\n")) 138 | return 139 | } 140 | key := command[1] 141 | value , ok := p.Store.Load(key) 142 | if !ok { 143 | conn.Write([]byte("KEY NOT FOUND\n")) 144 | return 145 | } 146 | expireToSrt := command[2] 147 | now := time.Now() 148 | duration, err := utils.ParseDuration(expireToSrt) 149 | if err!= nil { 150 | conn.Write([]byte("EXPIRE DATA INVALID\n")) 151 | return 152 | } 153 | expireTo := now.Add(duration) 154 | value.ExpireTo = expireTo 155 | p.Store.Store(key, value) 156 | conn.Write([]byte(fmt.Sprintf("EXPIRE TO %v\n", expireTo.Format("2006-01-02 15:04:05")))) 157 | } 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | --------------------------------------------------------------------------------