Кэш хранит картинки в файловой системе
4
.gitignore
vendored
@ -14,7 +14,7 @@
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
examples
|
||||
.idea
|
||||
*.log
|
||||
bin
|
||||
bin
|
||||
assets/cache/*.*
|
8
LICENSE
Normal file
@ -0,0 +1,8 @@
|
||||
Copyright (c) 2020, Andrey "Tiburon" Ivanov 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 names of 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 HOLDER 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.
|
@ -1,3 +1,5 @@
|
||||
[](https://travis-ci.org/tiburon-777/OTUS_Project)
|
||||
[](https://goreportcard.com/report/github.com/tiburon-777/OTUS_Project)
|
||||
# Проектная работа Image previewer service
|
||||
|
||||
## Общее описание
|
||||
|
0
assets/cache/nofile
vendored
Normal file
@ -21,6 +21,10 @@ func main() {
|
||||
}
|
||||
|
||||
app := application.New(conf)
|
||||
err = app.Cache.Clear()
|
||||
if err != nil {
|
||||
log.Fatalf("can't clean cache: %s", err.Error())
|
||||
}
|
||||
go func() {
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals)
|
||||
|
@ -17,7 +17,7 @@ const testPortBase = 3000
|
||||
func TestIntegrationPositive(t *testing.T) {
|
||||
testPort := strconv.Itoa(testPortBase + 1)
|
||||
wg := sync.WaitGroup{}
|
||||
server := &http.Server{Addr: "localhost:" + testPort, Handler: http.FileServer(http.Dir("../assets"))}
|
||||
server := &http.Server{Addr: "localhost:" + testPort, Handler: http.FileServer(http.Dir("../test/data"))}
|
||||
go func() {
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
@ -53,7 +53,7 @@ func TestIntegrationPositive(t *testing.T) {
|
||||
func TestIntegrationNegative(t *testing.T) {
|
||||
testPort := strconv.Itoa(testPortBase + 2)
|
||||
wg := sync.WaitGroup{}
|
||||
server := &http.Server{Addr: "localhost:" + testPort, Handler: http.FileServer(http.Dir("../assets"))}
|
||||
server := &http.Server{Addr: "localhost:" + testPort, Handler: http.FileServer(http.Dir("../test/data"))}
|
||||
go func() {
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
|
@ -22,7 +22,7 @@ func New(conf config.Config) *App {
|
||||
if err != nil {
|
||||
oslog.Fatal("не удалось прикрутить логгер: ", err.Error())
|
||||
}
|
||||
c := cache.NewCache(conf.Cache.Capacity)
|
||||
c := cache.NewCache(conf.Cache.Capacity, conf.Cache.StoragePath)
|
||||
return &App{Server: &http.Server{Addr: net.JoinHostPort(conf.Server.Address, conf.Server.Port)}, Log: loger, Cache: c, Conf: conf}
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ func buildQuery(u *url.URL) (q Query, err error) {
|
||||
}
|
||||
|
||||
func (q Query) id() string {
|
||||
return strconv.Itoa(q.Width) + "/" + strconv.Itoa(q.Height) + "/" + q.URL.Path
|
||||
return strings.Replace(strconv.Itoa(q.Width)+"/"+strconv.Itoa(q.Height)+q.URL.Path, "/", "_", -1)
|
||||
}
|
||||
|
||||
func (q Query) fromOrigin(headers http.Header, timeout time.Duration) ([]byte, *http.Response, error) {
|
||||
|
93
internal/cache/cache.go
vendored
@ -2,6 +2,10 @@ package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@ -10,11 +14,12 @@ type Key string
|
||||
type Cache interface {
|
||||
Set(key Key, value interface{}) (bool, error) // Добавить значение в кэш по ключу
|
||||
Get(key Key) (interface{}, bool, error) // Получить значение из кэша по ключу
|
||||
Clear() // Очистить кэш
|
||||
Clear() error // Очистить кэш
|
||||
}
|
||||
|
||||
type lruCache struct {
|
||||
capacity int
|
||||
path string
|
||||
queue *List
|
||||
items map[Key]*ListItem
|
||||
mx sync.RWMutex
|
||||
@ -25,9 +30,13 @@ type Item struct {
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func NewCache(capacity int) Cache {
|
||||
func NewCache(capacity int, path string) Cache {
|
||||
if _, err := ioutil.ReadDir(path); err != nil {
|
||||
log.Fatalf("cache directory %s not exists: %s", path, err.Error())
|
||||
}
|
||||
return &lruCache{
|
||||
capacity: capacity,
|
||||
path: path,
|
||||
queue: NewList(),
|
||||
items: make(map[Key]*ListItem),
|
||||
}
|
||||
@ -37,6 +46,10 @@ func (l *lruCache) Set(key Key, value interface{}) (bool, error) {
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
if _, exists := l.items[key]; exists {
|
||||
err := l.loadOut(key, value)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("can't replace file %s: %w", path.Join([]string{l.path, string(key)}...), err)
|
||||
}
|
||||
l.items[key].Value = Item{Value: value, Key: key}
|
||||
l.queue.MoveToFront(l.items[key])
|
||||
return exists, nil
|
||||
@ -46,10 +59,21 @@ func (l *lruCache) Set(key Key, value interface{}) (bool, error) {
|
||||
if !ok {
|
||||
return false, fmt.Errorf("can't cast type")
|
||||
}
|
||||
err := l.remove(k.Value.(Key))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("can't delete file %s: %w", path.Join([]string{l.path, k.Value.(string)}...), err)
|
||||
}
|
||||
delete(l.items, k.Key)
|
||||
l.queue.Remove(l.queue.Back())
|
||||
}
|
||||
l.items[key] = l.queue.PushFront(Item{Value: value, Key: key})
|
||||
err := l.loadOut(key, value)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("can't save file %s: %w", path.Join([]string{l.path, string(key)}...), err)
|
||||
}
|
||||
if l.items == nil {
|
||||
l.items = make(map[Key]*ListItem)
|
||||
}
|
||||
l.items[key] = l.queue.PushFront(Item{Value: key, Key: key})
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@ -64,13 +88,72 @@ func (l *lruCache) Get(key Key) (interface{}, bool, error) {
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("can't cast type")
|
||||
}
|
||||
return s.Value, true, nil
|
||||
pic, err := l.loadIn(s.Key)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("can't load file %s: %w", path.Join([]string{l.path, string(s.Key)}...), err)
|
||||
}
|
||||
return pic, true, nil
|
||||
}
|
||||
|
||||
func (l *lruCache) Clear() {
|
||||
func (l *lruCache) Clear() error {
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
err := l.drop()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't remove files from %s: %w", l.path, err)
|
||||
}
|
||||
l.items = nil
|
||||
l.queue.len = 0
|
||||
l.queue.Info = ListItem{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lruCache) loadOut(name Key, pic interface{}) error {
|
||||
filename := path.Join([]string{l.path, string(name)}...)
|
||||
err := ioutil.WriteFile(filename, pic.([]byte), 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create or write file %s: %w", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lruCache) loadIn(name Key) ([]byte, error) {
|
||||
filename := path.Join([]string{l.path, string(name)}...)
|
||||
f, err := os.Open(filename)
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't open file %s: %w", filename, err)
|
||||
}
|
||||
res, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read file %s: %w", filename, err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (l *lruCache) remove(name Key) error {
|
||||
filename := path.Join([]string{l.path, string(name)}...)
|
||||
err := os.RemoveAll(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't remove file %s: %w", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lruCache) drop() error {
|
||||
dir, err := ioutil.ReadDir(l.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't read directory %s: %w", l.path, err)
|
||||
}
|
||||
for _, d := range dir {
|
||||
if d.Name() != "nofile" {
|
||||
err := os.Remove(path.Join([]string{l.path, d.Name()}...))
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't remove file %s/%s: %w", l.path, d.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
58
internal/cache/cache_test.go
vendored
@ -11,7 +11,9 @@ import (
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
t.Run("empty cache", func(t *testing.T) {
|
||||
c := NewCache(10)
|
||||
c := NewCache(10, "../../assets/cache")
|
||||
err := c.Clear()
|
||||
require.NoError(t, err, err)
|
||||
|
||||
_, ok, err := c.Get("aaa")
|
||||
require.NoError(t, err)
|
||||
@ -20,56 +22,66 @@ func TestCache(t *testing.T) {
|
||||
_, ok, err = c.Get("bbb")
|
||||
require.NoError(t, err)
|
||||
require.False(t, ok)
|
||||
|
||||
err = c.Clear()
|
||||
require.NoError(t, err, err)
|
||||
})
|
||||
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
c := NewCache(5)
|
||||
c := NewCache(5, "../../assets/cache")
|
||||
err := c.Clear()
|
||||
require.NoError(t, err, err)
|
||||
|
||||
wasInCache, err := c.Set("aaa", 100)
|
||||
wasInCache, err := c.Set("aaa", []byte("pic #1111"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, wasInCache)
|
||||
|
||||
wasInCache, err = c.Set("bbb", 200)
|
||||
wasInCache, err = c.Set("bbb", []byte("pic #2222"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, wasInCache)
|
||||
|
||||
val, ok, err := c.Get("aaa")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 100, val)
|
||||
require.Equal(t, []byte("pic #1111"), val)
|
||||
|
||||
val, ok, err = c.Get("bbb")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 200, val)
|
||||
require.Equal(t, []byte("pic #2222"), val)
|
||||
|
||||
wasInCache, err = c.Set("aaa", 300)
|
||||
wasInCache, err = c.Set("aaa", []byte("pic #3333"))
|
||||
require.NoError(t, err)
|
||||
require.True(t, wasInCache)
|
||||
|
||||
val, ok, err = c.Get("aaa")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 300, val)
|
||||
require.Equal(t, []byte("pic #3333"), val)
|
||||
|
||||
val, ok, err = c.Get("ccc")
|
||||
require.NoError(t, err)
|
||||
require.False(t, ok)
|
||||
require.Nil(t, val)
|
||||
|
||||
err = c.Clear()
|
||||
require.NoError(t, err, err)
|
||||
})
|
||||
|
||||
t.Run("purge logic", func(t *testing.T) {
|
||||
c := NewCache(3)
|
||||
c := NewCache(3, "../../assets/cache")
|
||||
err := c.Clear()
|
||||
require.NoError(t, err, err)
|
||||
|
||||
wasInCache, err := c.Set("aaa", 100)
|
||||
wasInCache, err := c.Set("aaa", []byte("pic #1111"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, wasInCache)
|
||||
|
||||
wasInCache, err = c.Set("bbb", 200)
|
||||
wasInCache, err = c.Set("bbb", []byte("pic #2222"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, wasInCache)
|
||||
|
||||
wasInCache, err = c.Set("ccc", 300)
|
||||
wasInCache, err = c.Set("ccc", []byte("pic #3333"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, wasInCache)
|
||||
|
||||
@ -81,7 +93,7 @@ func TestCache(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
wasInCache, err = c.Set("ddd", 400)
|
||||
wasInCache, err = c.Set("ddd", []byte("pic #4444"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, wasInCache)
|
||||
|
||||
@ -92,28 +104,38 @@ func TestCache(t *testing.T) {
|
||||
_, ok, err = c.Get("ccc")
|
||||
require.NoError(t, err)
|
||||
require.False(t, ok)
|
||||
|
||||
err = c.Clear()
|
||||
require.NoError(t, err, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCacheMultithreading(t *testing.T) {
|
||||
c := NewCache(10, "../../assets/cache")
|
||||
err := c.Clear()
|
||||
require.NoError(t, err, err)
|
||||
|
||||
c := NewCache(10)
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
c.Set(Key(strconv.Itoa(i)), i)
|
||||
for i := 0; i < 10_000; i++ {
|
||||
itm := strconv.Itoa(i)
|
||||
c.Set(Key(itm), []byte(itm))
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
c.Get(Key(strconv.Itoa(rand.Intn(1_000_000))))
|
||||
for i := 0; i < 10_000; i++ {
|
||||
itm := strconv.Itoa(rand.Intn(10_000))
|
||||
c.Get(Key(itm))
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
err = c.Clear()
|
||||
require.NoError(t, err, err)
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ type Config struct {
|
||||
Port string
|
||||
}
|
||||
Cache struct {
|
||||
Capacity int
|
||||
Capacity int
|
||||
StoragePath string
|
||||
}
|
||||
Query struct {
|
||||
Timeout int
|
||||
@ -45,7 +46,10 @@ func (c *Config) SetDefault() {
|
||||
Address string
|
||||
Port string
|
||||
}{Address: "localhost", Port: "8080"}
|
||||
c.Cache = struct{ Capacity int }{Capacity: 20}
|
||||
c.Cache = struct {
|
||||
Capacity int
|
||||
StoragePath string
|
||||
}{Capacity: 20, StoragePath: "../assets/cache"}
|
||||
c.Query = struct{ Timeout int }{Timeout: 15}
|
||||
c.Log = struct {
|
||||
File string
|
||||
|
@ -23,7 +23,7 @@ func TestLoggerLogic(t *testing.T) {
|
||||
oslog.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("Messages arround the level", func(t *testing.T) {
|
||||
t.Run("Messages around the level", func(t *testing.T) {
|
||||
log.Debugf("debug message")
|
||||
log.Errorf("error message")
|
||||
|
||||
|
@ -4,6 +4,7 @@ Port = "80"
|
||||
|
||||
[Cache]
|
||||
Capacity = 20
|
||||
StoragePath = "./assets/cache"
|
||||
|
||||
[Query]
|
||||
Timeout = 15
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |