2022-01-07 13:49:28 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2022-07-08 14:00:04 +00:00
|
|
|
"fmt"
|
2022-01-07 13:49:28 +00:00
|
|
|
"heckel.io/ntfy/util"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"sync"
|
2022-07-08 14:00:04 +00:00
|
|
|
"time"
|
2022-01-07 13:49:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2022-07-08 14:00:04 +00:00
|
|
|
fileIDRegex = regexp.MustCompile(fmt.Sprintf(`^[-_A-Za-z0-9]{%d}$`, messageIDLength))
|
2022-01-07 13:49:28 +00:00
|
|
|
errInvalidFileID = errors.New("invalid file ID")
|
2022-01-08 17:14:43 +00:00
|
|
|
errFileExists = errors.New("file exists")
|
2022-01-07 13:49:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type fileCache struct {
|
|
|
|
dir string
|
|
|
|
totalSizeCurrent int64
|
|
|
|
totalSizeLimit int64
|
|
|
|
fileSizeLimit int64
|
|
|
|
mu sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
func newFileCache(dir string, totalSizeLimit int64, fileSizeLimit int64) (*fileCache, error) {
|
|
|
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-01-07 14:15:33 +00:00
|
|
|
size, err := dirSize(dir)
|
2022-01-07 13:49:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &fileCache{
|
|
|
|
dir: dir,
|
|
|
|
totalSizeCurrent: size,
|
|
|
|
totalSizeLimit: totalSizeLimit,
|
|
|
|
fileSizeLimit: fileSizeLimit,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-01-12 22:03:28 +00:00
|
|
|
func (c *fileCache) Write(id string, in io.Reader, limiters ...util.Limiter) (int64, error) {
|
2022-01-07 13:49:28 +00:00
|
|
|
if !fileIDRegex.MatchString(id) {
|
|
|
|
return 0, errInvalidFileID
|
|
|
|
}
|
|
|
|
file := filepath.Join(c.dir, id)
|
2022-01-08 17:14:43 +00:00
|
|
|
if _, err := os.Stat(file); err == nil {
|
|
|
|
return 0, errFileExists
|
|
|
|
}
|
2022-01-07 13:49:28 +00:00
|
|
|
f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
2022-01-12 22:03:28 +00:00
|
|
|
limiters = append(limiters, util.NewFixedLimiter(c.Remaining()), util.NewFixedLimiter(c.fileSizeLimit))
|
2022-01-07 13:49:28 +00:00
|
|
|
limitWriter := util.NewLimitWriter(f, limiters...)
|
|
|
|
size, err := io.Copy(limitWriter, in)
|
|
|
|
if err != nil {
|
|
|
|
os.Remove(file)
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
if err := f.Close(); err != nil {
|
|
|
|
os.Remove(file)
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
c.mu.Lock()
|
|
|
|
c.totalSizeCurrent += size
|
|
|
|
c.mu.Unlock()
|
|
|
|
return size, nil
|
|
|
|
}
|
|
|
|
|
2022-01-08 17:14:43 +00:00
|
|
|
func (c *fileCache) Remove(ids ...string) error {
|
2022-01-07 14:15:33 +00:00
|
|
|
for _, id := range ids {
|
2022-01-10 03:06:31 +00:00
|
|
|
if !fileIDRegex.MatchString(id) {
|
|
|
|
return errInvalidFileID
|
2022-01-07 14:15:33 +00:00
|
|
|
}
|
2022-01-10 03:06:31 +00:00
|
|
|
file := filepath.Join(c.dir, id)
|
|
|
|
_ = os.Remove(file) // Best effort delete
|
2022-01-07 14:15:33 +00:00
|
|
|
}
|
|
|
|
size, err := dirSize(c.dir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
c.mu.Lock()
|
|
|
|
c.totalSizeCurrent = size
|
|
|
|
c.mu.Unlock()
|
2022-01-10 03:06:31 +00:00
|
|
|
return nil
|
2022-01-07 14:15:33 +00:00
|
|
|
}
|
|
|
|
|
2022-07-08 14:00:04 +00:00
|
|
|
// Expired returns a list of file IDs for expired files
|
|
|
|
func (c *fileCache) Expired(olderThan time.Time) ([]string, error) {
|
|
|
|
entries, err := os.ReadDir(c.dir)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
var ids []string
|
|
|
|
for _, e := range entries {
|
|
|
|
info, err := e.Info()
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if info.ModTime().Before(olderThan) && fileIDRegex.MatchString(e.Name()) {
|
|
|
|
ids = append(ids, e.Name())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ids, nil
|
|
|
|
}
|
|
|
|
|
2022-01-07 14:15:33 +00:00
|
|
|
func (c *fileCache) Size() int64 {
|
|
|
|
c.mu.Lock()
|
|
|
|
defer c.mu.Unlock()
|
|
|
|
return c.totalSizeCurrent
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *fileCache) Remaining() int64 {
|
2022-01-07 13:49:28 +00:00
|
|
|
c.mu.Lock()
|
|
|
|
defer c.mu.Unlock()
|
|
|
|
remaining := c.totalSizeLimit - c.totalSizeCurrent
|
|
|
|
if remaining < 0 {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return remaining
|
|
|
|
}
|
2022-01-07 14:15:33 +00:00
|
|
|
|
|
|
|
func dirSize(dir string) (int64, error) {
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
var size int64
|
|
|
|
for _, e := range entries {
|
|
|
|
info, err := e.Info()
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
size += info.Size()
|
|
|
|
}
|
|
|
|
return size, nil
|
|
|
|
}
|