diff --git a/cacheFs/cacheFs.go b/cacheFs/cacheFs.go new file mode 100644 index 0000000..0a0bdb2 --- /dev/null +++ b/cacheFs/cacheFs.go @@ -0,0 +1,98 @@ +package cachefs + +import ( + "os" + "time" + + "github.com/spf13/afero" +) + +// ensure CacheFs implements afero.Fs. +var _ afero.Fs = &CacheFs{} + +// CacheStatus represents the status of a file in the cache. +type CacheStatus int + +const ( + // CacheHit indicates that the requested file was found in the cache. + CacheHit CacheStatus = iota + + // CacheMiss indicates that the requested file was not found in the cache. + CacheMiss +) + +type CacheFs struct { + // source filesystem + source afero.Fs + + // cache filesystem + cache afero.Fs +} + +func NewCacheFs(source, cache afero.Fs) *CacheFs { + return &CacheFs{ + source: source, + cache: cache, + } +} + +// Create implements the Create method of afero.Fs, ensuring we don't exceed storage limits. +func (cfs *CacheFs) Create(name string) (afero.File, error) { + return cfs.source.Create(name) +} + +func (cfs *CacheFs) Mkdir(name string, perm os.FileMode) error { + return cfs.source.Mkdir(name, perm) +} + +func (cfs *CacheFs) MkdirAll(path string, perm os.FileMode) error { + return cfs.source.MkdirAll(path, perm) +} + +func (cfs *CacheFs) Open(name string) (afero.File, error) { + return cfs.source.Open(name) +} + +func (cfs *CacheFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { + return cfs.source.OpenFile(name, flag, perm) +} + +func (cfs *CacheFs) Remove(name string) error { + return cfs.source.Remove(name) +} + +func (cfs *CacheFs) RemoveAll(path string) error { + return cfs.source.RemoveAll(path) +} + +func (cfs *CacheFs) Rename(oldname, newname string) error { + return cfs.source.Rename(oldname, newname) +} + +func (cfs *CacheFs) Stat(name string) (os.FileInfo, error) { + return cfs.source.Stat(name) +} + +func (cfs *CacheFs) Name() string { + return "CacheFS" +} + +func (cfs *CacheFs) Chmod(name string, mode os.FileMode) error { + return cfs.Chmod(name, mode) +} + +func (cfs *CacheFs) Chown(name string, uid, gid int) error { + return cfs.source.Chown(name, uid, gid) +} + +func (cfs *CacheFs) Chtimes(name string, atime time.Time, mtime time.Time) error { + return cfs.source.Chtimes(name, atime, mtime) +} + +// ensure CacheFile implements afero.File. +var _ afero.File = &CacheFile{} + +// CacheFile wraps an afero.File to manage space usage on write operations. +type CacheFile struct { + afero.File +} diff --git a/go.mod b/go.mod index 8a27992..efb7de3 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module s1d3sw1ped/SteamCache2 go 1.23.0 + +require ( + github.com/docker/go-units v0.5.0 + github.com/spf13/afero v1.12.0 +) + +require golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1d0c67c --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= diff --git a/limitedFs/limitedFs.go b/limitedFs/limitedFs.go new file mode 100644 index 0000000..5126f9e --- /dev/null +++ b/limitedFs/limitedFs.go @@ -0,0 +1,192 @@ +package limitedFs + +import ( + "errors" + "os" + "sync" + "time" + + "github.com/labstack/gommon/log" + "github.com/spf13/afero" +) + +var ( + // Ensure LimitedFs implements afero.Fs. + _ afero.Fs = &LimitedFs{} + + // ErrNotEnoughSpace is returned when there's not enough space to write. + ErrNotEnoughSpace = errors.New("not enough space") +) + +// LimitedFs wraps an afero.Fs to limit the storage space. +type LimitedFs struct { + fs afero.Fs + + mu sync.Mutex + usedSpace int64 + maxSpace int64 +} + +// NewLimitedFs creates a new LimitedFs with the specified max storage space. +func NewLimitedFs(source afero.Fs, maxSpace int64) *LimitedFs { + lfs := &LimitedFs{ + fs: source, + usedSpace: 0, + maxSpace: maxSpace, + } + + // If the source filesystem is a memory filesystem, there is no need to scan it. + if _, ok := source.(*afero.MemMapFs); !ok { + // Scan the source filesystem to calculate the used space. + log.Infof("Scanning source filesystem to calculate used space...") + err := afero.Walk(source, "/", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + lfs.usedSpace += info.Size() + return nil + }) + if err != nil { + panic(err) + } + } + + return lfs +} + +// Create implements the Create method of afero.Fs, ensuring we don't exceed storage limits. +func (lfs *LimitedFs) Create(name string) (afero.File, error) { + file, err := lfs.fs.Create(name) + if err != nil { + return nil, err + } + return &LimitedFile{ + File: file, + lfs: lfs, + }, nil +} + +func (lfs *LimitedFs) Mkdir(name string, perm os.FileMode) error { + return lfs.fs.Mkdir(name, perm) +} + +func (lfs *LimitedFs) MkdirAll(path string, perm os.FileMode) error { + return lfs.fs.MkdirAll(path, perm) +} + +func (lfs *LimitedFs) Open(name string) (afero.File, error) { + file, err := lfs.fs.Open(name) + if err != nil { + return nil, err + } + return &LimitedFile{ + File: file, + lfs: lfs, + }, nil +} + +func (lfs *LimitedFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { + file, err := lfs.fs.OpenFile(name, flag, perm) + if err != nil { + return nil, err + } + return &LimitedFile{ + File: file, + lfs: lfs, + }, nil +} + +func (lfs *LimitedFs) Remove(name string) error { + info, err := lfs.fs.Stat(name) + if err != nil { + return err + } + lfs.mu.Lock() + lfs.usedSpace -= info.Size() + lfs.mu.Unlock() + return lfs.fs.Remove(name) +} + +func (lfs *LimitedFs) RemoveAll(path string) error { + // Calculate the space used by the directory and its children. + var size int64 + err := afero.Walk(lfs.fs, path, func(subpath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + size += info.Size() + return nil + }) + + if err != nil { + return err + } + + lfs.mu.Lock() + lfs.usedSpace -= size + lfs.mu.Unlock() + + return lfs.RemoveAll(path) +} + +func (lfs *LimitedFs) Rename(oldname, newname string) error { + return lfs.fs.Rename(oldname, newname) +} + +func (lfs *LimitedFs) Stat(name string) (os.FileInfo, error) { + return lfs.fs.Stat(name) +} + +func (lfs *LimitedFs) Name() string { + return "LimitedFS" +} + +func (lfs *LimitedFs) Chmod(name string, mode os.FileMode) error { + return lfs.fs.Chmod(name, mode) +} + +func (lfs *LimitedFs) Chown(name string, uid, gid int) error { + return lfs.fs.Chown(name, uid, gid) +} + +func (lfs *LimitedFs) Chtimes(name string, atime time.Time, mtime time.Time) error { + return lfs.fs.Chtimes(name, atime, mtime) +} + +// ensure LimitedFile implements afero.File. +var _ afero.File = &LimitedFile{} + +// LimitedFile wraps an afero.File to manage space usage on write operations. +type LimitedFile struct { + afero.File + lfs *LimitedFs +} + +// Write checks if there's enough space before writing. +func (lf *LimitedFile) Write(p []byte) (n int, err error) { + lf.lfs.mu.Lock() + if int64(len(p)) > lf.lfs.maxSpace-lf.lfs.usedSpace { + lf.lfs.mu.Unlock() + return 0, ErrNotEnoughSpace + } + lf.lfs.mu.Unlock() + n, err = lf.File.Write(p) + if err == nil { + lf.lfs.mu.Lock() + lf.lfs.usedSpace += int64(n) + lf.lfs.mu.Unlock() + } + return +} + +// Close updates used space and calls the underlying file's Close method. +func (lf *LimitedFile) Close() error { + info, err := lf.File.Stat() + if err != nil { + return err + } + lf.lfs.mu.Lock() + lf.lfs.usedSpace -= info.Size() + lf.lfs.mu.Unlock() + return lf.File.Close() +} diff --git a/limitedFs/limited_test.go b/limitedFs/limited_test.go new file mode 100644 index 0000000..97b1075 --- /dev/null +++ b/limitedFs/limited_test.go @@ -0,0 +1,44 @@ +package limitedFs + +import ( + "fmt" + "testing" + + "github.com/spf13/afero" +) + +func TestLimitedCreate(t *testing.T) { + t.Parallel() + + memfs := afero.NewMemMapFs() + lfs := NewLimitedFs(memfs, 10) + + for i := 0; i < 11; i++ { + _, err := lfs.Create(fmt.Sprintf("file.%d", i)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } +} + +func TestLimitedWrite(t *testing.T) { + t.Parallel() + + memfs := afero.NewMemMapFs() + lfs := NewLimitedFs(memfs, 10) + + file, err := lfs.Create("file") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + for i := 0; i < 11; i++ { + _, err = file.Write([]byte("1")) + if i < 10 && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if i == 10 && err == nil { + t.Fatal("expected error, got nil") + } + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..7905807 --- /dev/null +++ b/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/steamcache/config.go b/steamcache/config.go new file mode 100644 index 0000000..aa98bba --- /dev/null +++ b/steamcache/config.go @@ -0,0 +1,51 @@ +package steamcache + +import "github.com/docker/go-units" + +type StorageType string + +type Config struct { + Address string `yaml:"address"` // Address to listen on + Storage []StorageConfig `yaml:"storage"` // Storage configuration + // Ordered by speed so memory is first, then ssd, then hdd, etc +} + +func NewConfig() *Config { + return &Config{ + Address: ":8080", + Storage: []StorageConfig{ + { + Name: "memory", + MaxSizeStr: "1GB", + }, + { + Name: "filesystem", + Path: "/example/path", + MaxSizeStr: "1TB", + }, + }, + } +} + +type StorageConfig struct { + Name string `yaml:"name"` // Name of the storage + Path string `yaml:"path,omitempty"` // Path to the storage (if applicable) - empty for memory + MaxSizeStr string `yaml:"max_size"` // Maximum size of the storage in human readable format (e.g. 1GB) +} + +func (sc *StorageConfig) IsMemory() bool { + return sc.Path == "" +} + +func (sc *StorageConfig) IsFilesystem() bool { + return sc.Path != "" +} + +func (sc *StorageConfig) MaxSize() int64 { + x, err := units.FromHumanSize(sc.MaxSizeStr) + if err != nil { + return -1 // Unlimited + } + + return x // Bytes +} diff --git a/steamcache/steamcache.go b/steamcache/steamcache.go new file mode 100644 index 0000000..f36fe10 --- /dev/null +++ b/steamcache/steamcache.go @@ -0,0 +1,21 @@ +package steamcache + +import "net/http" + +type SteamCache struct { +} + +func New() *SteamCache { + return &SteamCache{} +} + +func (sc *SteamCache) Run() { + http.ListenAndServe(":8080", sc) +} + +func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { + //TODO: proxy request to steam servers and cache the response + + // for now, just return a simple response + w.Write([]byte("Hello, World!")) +}