diff --git a/.env.example b/.env.example index 19e6136..667392e 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,4 @@ MAIL_PASSWORD='password' MAIL_TLS=true MAIL_TLS_CERT=./cert/certificate.crt MAIL_TLS_KEY=./cert/certificate.key +LRU_CACHE_SIZE=10000 diff --git a/cmd/main.go b/cmd/main.go index e1984a8..b3afc1f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,6 +5,7 @@ import ( "os" "os/signal" "runtime" + "strconv" "syscall" "github.com/joho/godotenv" @@ -16,6 +17,7 @@ import ( ) var ( + cacheSize = 10000 shutdowns []func() error auth *mail.Auth tls *mail.TLS @@ -49,8 +51,18 @@ func run() { httpPort = os.Getenv("HTTP_PORT") } + if cs := os.Getenv("LRU_CACHE_SIZE"); cs != "" { + i, err := strconv.Atoi(cs) + if err == nil { + cacheSize = i + } + } + // TODO implement multiple data store sqlite/postgresql - db := repository.NewMessageInMemory(repository.NewMessageInMemoryStore()) + db, err := repository.NewLRUCacheStore(cacheSize, logger) + if err != nil { + panic(err) + } if os.Getenv("MAIL_AUTH") == "true" { auth = &mail.Auth{ diff --git a/domain/attachment.go b/domain/attachment.go index cebf013..04f8479 100644 --- a/domain/attachment.go +++ b/domain/attachment.go @@ -28,13 +28,15 @@ func CreateAttachment(filename, contentType string, content []byte) (Attachment, } // write into file - f, err := os.Create(fmt.Sprintf("%s/%s%s", mail.AssetFilePath, fid, filepath.Ext(filename))) + f, err := os.Create(fmt.Sprintf("%s/%s", mail.AssetFilePath, attch.Filepath)) + if err != nil { + return Attachment{}, err + } defer f.Close() + _, err = f.Write(content) if err != nil { return Attachment{}, err } - f.Write(content) - return attch, nil } diff --git a/go.mod b/go.mod index c143499..8cc10bf 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( github.com/emersion/go-smtp v0.14.0 github.com/gorilla/sessions v1.2.1 + github.com/hashicorp/golang-lru v0.5.4 github.com/jhillyerd/enmime v0.8.3 github.com/joho/godotenv v1.3.0 github.com/labstack/echo-contrib v0.9.0 diff --git a/go.sum b/go.sum index f282d1c..0c4dd25 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE= github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jhillyerd/enmime v0.8.3 h1:aZGfnnvl4m4FJhvoCrCeAwJWEb3GNm+WhaQ9Nr0Oec8= diff --git a/repository/lrucache_store.go b/repository/lrucache_store.go new file mode 100644 index 0000000..f708e98 --- /dev/null +++ b/repository/lrucache_store.go @@ -0,0 +1,156 @@ +package repository + +import ( + "context" + "errors" + "fmt" + "os" + "sort" + "time" + + lru "github.com/hashicorp/golang-lru" + "github.com/purwandi/mail" + "github.com/purwandi/mail/domain" + "github.com/segmentio/ksuid" + "go.uber.org/zap" +) + +type lruCache struct { + cache *lru.Cache +} + +func evictionFn(logger *zap.Logger) func(key interface{}, value interface{}) { + return func(key interface{}, value interface{}) { + m, ok := value.(domain.Message) + if !ok { + return + } + for _, att := range m.Attachments { + fullPath := fmt.Sprintf("%s/%s", mail.AssetFilePath, att.Filepath) + err := os.Remove(fullPath) + if err != nil { + msg := fmt.Sprintf("failed deleting file: %s from message id: %s", fullPath, key) + logger.Error(msg, zap.Error(err)) + } + } + } + +} + +func NewLRUCacheStore(size int, logger *zap.Logger) (MessageRepository, error) { + c, err := lru.NewWithEvict(size, evictionFn(logger)) + if err != nil { + return nil, err + } + m1 := ksuid.New().String() + m2 := ksuid.New().String() + c.Add(m1, domain.Message{ + ID: m1, + Subject: "7 Quotes by Albert Einstein That Will Change How You Think ", + Sender: "noreply@medium.com", + From: []domain.Contact{ + {Email: "noreply@medium.com", Name: "Medium"}, + }, + To: []domain.Contact{ + {Email: "foo@bar.com", Name: "Fooabar"}, + }, + TextBody: "We’ve almost made it to 2021. Have you built any particularly cool projects this year? We’d love to hear about them.", + HTMLBody: "

hello world

", + Attachments: []domain.Attachment{ + {ID: "1lNigjr8fsntbweehfBLpkQRoMh", Filename: "attachment.txt", Filepath: "1lNigjr8fsntbweehfBLpkQRoMh.txt"}, + {ID: "1lNip3BucJnkDGo2uAChhgib20T", Filename: "attachment.png", Filepath: "1lNip3BucJnkDGo2uAChhgib20T.png"}, + }, + Date: time.Now().Add(-20 * time.Minute), + }) + c.Add(m2, domain.Message{ + ID: m2, + Subject: "Change How You Think | Sinem Günel in Age of Awareness", + Sender: "noreply@google.com", + From: []domain.Contact{ + {Email: "noreply@google.com", Name: "google"}, + }, + To: []domain.Contact{ + {Email: "bar@bar.com", Name: "Foobar"}, + }, + TextBody: "Collaborating with multiple people can be difficult, especially with lots of back and forth across email and chat.", + HTMLBody: "

hello world

", + Attachments: []domain.Attachment{ + {ID: "1lOs2EOqStjiMlDINR44KnMV9De", Filename: "attachment.txt", Filepath: "1lOs2EOqStjiMlDINR44KnMV9D.txt"}, + {ID: "1lOs60waIw6LoEqWNPmL7LRR06M", Filename: "attachment.png", Filepath: "1lOs60waIw6LoEqWNPmL7LRR06M.png"}, + }, + Date: time.Now(), + }) + return &lruCache{ + cache: c, + }, nil +} + +func (c *lruCache) Save(ctx context.Context, m *domain.Message) <-chan error { + ch := make(chan error) + go func() { + c.cache.Add(m.ID, *m) + ch <- nil + close(ch) + }() + + return ch +} +func (c *lruCache) Delete(ctx context.Context, id string) <-chan error { + ch := make(chan error) + go func() { + present := c.cache.Remove(id) + if present { + ch <- nil + close(ch) + return + } + ch <- errors.New("Message not found") + close(ch) + }() + + return ch +} +func (c *lruCache) Reset(ctx context.Context) <-chan error { + ch := make(chan error) + go func() { + c.cache.Purge() + ch <- nil + close(ch) + }() + + return ch +} +func (c *lruCache) Find(ctx context.Context, id string) <-chan QueryResult { + ch := make(chan QueryResult) + go func() { + var qr QueryResult + inf, ok := c.cache.Get(id) + if !ok { + qr.Error = errors.New("Message not found") + ch <- qr + return + } + qr.Result = inf + ch <- qr + close(ch) + }() + return ch +} +func (c *lruCache) FindAll(context.Context) <-chan QueryResult { + ch := make(chan QueryResult) + go func() { + var ( + qr QueryResult + msgs []domain.Message + ) + for _, k := range c.cache.Keys() { + inf, _ := c.cache.Get(k) + msgs = append(msgs, inf.(domain.Message)) + } + sort.Sort(ByDateDesc(msgs)) + qr.Result = msgs + ch <- qr + close(ch) + }() + return ch +}