Compare commits
39 Commits
1.0.2
..
cd4b5e0598
| Author | SHA1 | Date | |
|---|---|---|---|
| cd4b5e0598 | |||
| e583780b9c | |||
| c0ed8a590e | |||
| 09d3450738 | |||
| f01b15c8c6 | |||
| 27ac30d28a | |||
| 0424de1fca | |||
| 32c0d85bad | |||
| 6b51e61a87 | |||
| 1094505f20 | |||
| 93d04410bf | |||
| 62f7a38295 | |||
| c370e80888 | |||
| 9d0f4894e1 | |||
| 745ee37184 | |||
| 5a9b07f609 | |||
| d88db3d9c1 | |||
| 91075232cd | |||
| 1f588fa20a | |||
| d39a2ad4fa | |||
| 80cc75f372 | |||
| 53f5d1577c | |||
| 1186e14c56 | |||
| 3bb57b7741 | |||
| 8f2d1eff9b | |||
| 27242fdf05 | |||
| 55330ebd3c | |||
| e37dc930dd | |||
| 79f0814745 | |||
| 1feee2758c | |||
| 22f9196960 | |||
| 9d0a09d8be | |||
| f6f007bf47 | |||
| 232b290a44 | |||
| b55b95f5d8 | |||
| ca6c9a5861 | |||
| 16dce1f0c2 | |||
| 2be7b117ea | |||
| 88ecb1bc24 |
@@ -1,4 +1,4 @@
|
||||
name: Release Tag
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build versioned release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: PR Check
|
||||
name: CI
|
||||
on:
|
||||
- pull_request
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ changelog:
|
||||
- "^test:"
|
||||
|
||||
release:
|
||||
name_template: '{{.ProjectName}}-{{.Version}}'
|
||||
footer: >-
|
||||
|
||||
---
|
||||
|
||||
Vendored
+1
-34
@@ -5,7 +5,7 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Memory & Disk",
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
@@ -17,39 +17,6 @@
|
||||
"10G",
|
||||
"--disk-path",
|
||||
"tmp/disk",
|
||||
"--upstream",
|
||||
"http://192.168.2.88:80",
|
||||
"--verbose",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Launch Disk Only",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"args": [
|
||||
"--disk",
|
||||
"10G",
|
||||
"--disk-path",
|
||||
"tmp/disk",
|
||||
"--upstream",
|
||||
"http://192.168.2.88:80",
|
||||
"--verbose",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Launch Memory Only",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"args": [
|
||||
"--memory",
|
||||
"1G",
|
||||
"--upstream",
|
||||
"http://192.168.2.88:80",
|
||||
"--verbose",
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
+5
-22
@@ -4,7 +4,6 @@ import (
|
||||
"os"
|
||||
"s1d3sw1ped/SteamCache2/steamcache"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -14,10 +13,6 @@ var (
|
||||
disk string
|
||||
diskmultiplier int
|
||||
diskpath string
|
||||
upstream string
|
||||
|
||||
pprof bool
|
||||
verbose bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -29,10 +24,6 @@ var rootCmd = &cobra.Command{
|
||||
By caching game files, SteamCache2 ensures that subsequent downloads of the same files are served from the local cache,
|
||||
significantly improving download times and reducing the load on the internet connection.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if verbose {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
|
||||
sc := steamcache.New(
|
||||
":80",
|
||||
memory,
|
||||
@@ -40,8 +31,6 @@ var rootCmd = &cobra.Command{
|
||||
disk,
|
||||
diskmultiplier,
|
||||
diskpath,
|
||||
upstream,
|
||||
pprof,
|
||||
)
|
||||
sc.Run()
|
||||
},
|
||||
@@ -57,15 +46,9 @@ func Execute() {
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().StringVarP(&memory, "memory", "m", "0", "The size of the memory cache")
|
||||
rootCmd.Flags().IntVarP(&memorymultiplier, "memory-gc", "M", 10, "The gc value for the memory cache")
|
||||
rootCmd.Flags().StringVarP(&disk, "disk", "d", "0", "The size of the disk cache")
|
||||
rootCmd.Flags().IntVarP(&diskmultiplier, "disk-gc", "D", 100, "The gc value for the disk cache")
|
||||
rootCmd.Flags().StringVarP(&diskpath, "disk-path", "p", "", "The path to the disk cache")
|
||||
|
||||
rootCmd.Flags().StringVarP(&upstream, "upstream", "u", "", "The upstream server to proxy requests overrides the host header from the client but forwards the original host header to the upstream server")
|
||||
|
||||
rootCmd.Flags().BoolVarP(&pprof, "pprof", "P", false, "Enable pprof")
|
||||
rootCmd.Flags().MarkHidden("pprof")
|
||||
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
|
||||
rootCmd.Flags().StringVarP(&memory, "memory", "m", "100MB", "The size of the memory cache")
|
||||
rootCmd.Flags().IntVarP(&memorymultiplier, "memory-multiplier", "M", 10, "The multiplier for the memory cache")
|
||||
rootCmd.Flags().StringVarP(&disk, "disk", "d", "10GB", "The size of the disk cache")
|
||||
rootCmd.Flags().IntVarP(&diskmultiplier, "disk-multiplier", "D", 10, "The multiplier for the disk cache")
|
||||
rootCmd.Flags().StringVarP(&diskpath, "disk-path", "p", "tmp/steamcache2-disk", "The path to the disk cache")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
/*
|
||||
Copyright © 2025 s1d3sw1ped
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -14,15 +14,11 @@ type AvgCacheState struct {
|
||||
|
||||
// New creates a new average cache state with the given size.
|
||||
func New(size int) *AvgCacheState {
|
||||
a := &AvgCacheState{
|
||||
return &AvgCacheState{
|
||||
size: size,
|
||||
avgs: make([]cachestate.CacheState, size),
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
|
||||
a.Clear()
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Clear resets the average cache state to zero.
|
||||
@@ -30,9 +26,7 @@ func (a *AvgCacheState) Clear() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
for i := 0; i < len(a.avgs); i++ {
|
||||
a.avgs[i] = cachestate.CacheStateMiss
|
||||
}
|
||||
a.avgs = make([]cachestate.CacheState, a.size) // zeroed
|
||||
}
|
||||
|
||||
// Add adds a cache state to the average cache state.
|
||||
@@ -51,13 +45,16 @@ func (a *AvgCacheState) Avg() float64 {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
var hits int
|
||||
|
||||
var hits, misses int
|
||||
for _, cs := range a.avgs {
|
||||
if cs == cachestate.CacheStateHit {
|
||||
switch cs {
|
||||
case cachestate.CacheStateHit:
|
||||
hits++
|
||||
case cachestate.CacheStateMiss:
|
||||
misses++
|
||||
}
|
||||
}
|
||||
total := hits + misses
|
||||
|
||||
return float64(hits) / float64(len(a.avgs))
|
||||
return float64(hits) / float64(total)
|
||||
}
|
||||
|
||||
+48
-22
@@ -1,49 +1,75 @@
|
||||
package steamcache
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
||||
"s1d3sw1ped/SteamCache2/vfs"
|
||||
"s1d3sw1ped/SteamCache2/vfs/cachestate"
|
||||
"time"
|
||||
|
||||
"github.com/docker/go-units"
|
||||
"golang.org/x/exp/rand"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Set the GC percentage to 50%. This is a good balance between performance and memory usage.
|
||||
debug.SetGCPercent(50)
|
||||
}
|
||||
|
||||
// RandomGC randomly deletes files until we've reclaimed enough space.
|
||||
func randomgc(vfss vfs.VFS, size uint) (uint, uint) {
|
||||
// Randomly delete files until we've reclaimed enough space.
|
||||
random := func(vfss vfs.VFS, stats []*vfs.FileInfo) int64 {
|
||||
randfile := stats[rand.Intn(len(stats))]
|
||||
sz := randfile.Size()
|
||||
err := vfss.Delete(randfile.Name())
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return sz
|
||||
func randomgc(vfss vfs.VFS, stats []*vfs.FileInfo) int64 {
|
||||
// Pick a random file to delete
|
||||
randfile := stats[rand.Intn(len(stats))]
|
||||
sz := randfile.Size()
|
||||
err := vfss.Delete(randfile.Name())
|
||||
if err != nil {
|
||||
// If we failed to delete the file, log it and return 0
|
||||
// logger.Logger.Error().Err(err).Msgf("Failed to delete %s", randfile.Name())
|
||||
return 0
|
||||
}
|
||||
|
||||
return sz
|
||||
}
|
||||
|
||||
func memorygc(vfss vfs.VFS, size int) {
|
||||
tstart := time.Now()
|
||||
deletions := 0
|
||||
targetreclaim := int64(size)
|
||||
var reclaimed int64
|
||||
|
||||
stats := vfss.StatAll()
|
||||
for {
|
||||
reclaimed += randomgc(vfss, stats)
|
||||
deletions++
|
||||
if reclaimed >= targetreclaim {
|
||||
break
|
||||
}
|
||||
reclaimed += random(vfss, stats)
|
||||
deletions++
|
||||
}
|
||||
|
||||
return uint(reclaimed), uint(deletions)
|
||||
logger.Logger.Info().
|
||||
Str("name", vfss.Name()).
|
||||
Str("duration", time.Since(tstart).String()).
|
||||
Str("reclaimed", units.HumanSize(float64(reclaimed))).
|
||||
Int("deletions", deletions).
|
||||
Msgf("GC")
|
||||
}
|
||||
|
||||
func diskgc(vfss vfs.VFS, size int) {
|
||||
tstart := time.Now()
|
||||
deletions := 0
|
||||
targetreclaim := int64(size)
|
||||
var reclaimed int64
|
||||
|
||||
stats := vfss.StatAll()
|
||||
for {
|
||||
reclaimed += randomgc(vfss, stats)
|
||||
deletions++
|
||||
if reclaimed >= targetreclaim {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("name", vfss.Name()).
|
||||
Str("duration", time.Since(tstart).String()).
|
||||
Str("reclaimed", units.HumanSize(float64(reclaimed))).
|
||||
Int("deletions", deletions).
|
||||
Msgf("GC")
|
||||
}
|
||||
|
||||
func cachehandler(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
|
||||
return time.Since(fi.AccessTime()) < time.Second*10 // Put hot files in the fast vfs if equipped
|
||||
return time.Since(fi.AccessTime()) < time.Minute*10 // Put files in the cache if they've been accessed twice in the last 10 minutes
|
||||
}
|
||||
|
||||
@@ -6,8 +6,4 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
|
||||
var Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger()
|
||||
|
||||
+97
-172
@@ -6,7 +6,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"s1d3sw1ped/SteamCache2/steamcache/avgcachestate"
|
||||
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
||||
"s1d3sw1ped/SteamCache2/version"
|
||||
@@ -21,31 +20,23 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
pprof "net/http/pprof"
|
||||
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
type SteamCache struct {
|
||||
pprof bool
|
||||
address string
|
||||
upstream string
|
||||
|
||||
vfs vfs.VFS
|
||||
address string
|
||||
vfs vfs.VFS
|
||||
|
||||
memory *memory.MemoryFS
|
||||
disk *disk.DiskFS
|
||||
|
||||
memorygc *gc.GCFS
|
||||
diskgc *gc.GCFS
|
||||
|
||||
hits *avgcachestate.AvgCacheState
|
||||
|
||||
dirty bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func New(address string, memorySize string, memoryMultiplier int, diskSize string, diskMultiplier int, diskPath, upstream string, pprof bool) *SteamCache {
|
||||
func New(address string, memorySize string, memoryMultiplier int, diskSize string, diskMultiplier int, diskPath string) *SteamCache {
|
||||
memorysize, err := units.FromHumanSize(memorySize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -56,83 +47,49 @@ func New(address string, memorySize string, memoryMultiplier int, diskSize strin
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c := cache.New(
|
||||
cachehandler,
|
||||
)
|
||||
|
||||
var m *memory.MemoryFS
|
||||
var mgc *gc.GCFS
|
||||
if memorysize > 0 {
|
||||
m = memory.New(memorysize)
|
||||
mgc = gc.New(m, memoryMultiplier, randomgc)
|
||||
}
|
||||
|
||||
var d *disk.DiskFS
|
||||
var dgc *gc.GCFS
|
||||
if disksize > 0 {
|
||||
d = disk.New(diskPath, disksize)
|
||||
dgc = gc.New(d, diskMultiplier, randomgc)
|
||||
}
|
||||
|
||||
// configure the cache to match the specified mode (memory only, disk only, or memory and disk) based on the provided sizes
|
||||
if disksize == 0 && memorysize != 0 {
|
||||
//memory only mode - no disk
|
||||
|
||||
c.SetSlow(mgc)
|
||||
} else if disksize != 0 && memorysize == 0 {
|
||||
// disk only mode
|
||||
|
||||
c.SetSlow(dgc)
|
||||
} else if disksize != 0 && memorysize != 0 {
|
||||
// memory and disk mode
|
||||
|
||||
c.SetFast(mgc)
|
||||
c.SetSlow(dgc)
|
||||
} else {
|
||||
// no memory or disk isn't a valid configuration
|
||||
logger.Logger.Error().Bool("memory", false).Bool("disk", false).Msg("configuration invalid :( exiting")
|
||||
os.Exit(1)
|
||||
}
|
||||
m := memory.New(memorysize)
|
||||
d := disk.New(diskPath, disksize)
|
||||
|
||||
sc := &SteamCache{
|
||||
pprof: pprof,
|
||||
upstream: upstream,
|
||||
address: address,
|
||||
vfs: syncfs.New(c),
|
||||
address: address,
|
||||
vfs: syncfs.New(
|
||||
cache.New(
|
||||
gc.New(
|
||||
m,
|
||||
memoryMultiplier,
|
||||
memorygc,
|
||||
),
|
||||
gc.New(
|
||||
d,
|
||||
diskMultiplier,
|
||||
diskgc,
|
||||
),
|
||||
cachehandler,
|
||||
),
|
||||
),
|
||||
|
||||
memory: m,
|
||||
disk: d,
|
||||
|
||||
memorygc: mgc,
|
||||
diskgc: dgc,
|
||||
|
||||
hits: avgcachestate.New(100),
|
||||
hits: avgcachestate.New(10000),
|
||||
}
|
||||
|
||||
if d != nil {
|
||||
if d.Size() > d.Capacity() {
|
||||
randomgc(d, uint(d.Size()-d.Capacity()))
|
||||
}
|
||||
if d.Size() > d.Capacity() {
|
||||
diskgc(d, int(d.Size()-d.Capacity()))
|
||||
}
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *SteamCache) Run() {
|
||||
if sc.upstream != "" {
|
||||
_, err := http.Get(sc.upstream)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Str("upstream", sc.upstream).Msg("Failed to connect to upstream server")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
logger.Logger.Info().Str("address", sc.address).Str("version", version.Version).Msg("listening")
|
||||
|
||||
sc.mu.Lock()
|
||||
sc.dirty = true
|
||||
sc.mu.Unlock()
|
||||
|
||||
sc.LogStats()
|
||||
t := time.NewTicker(1 * time.Second)
|
||||
t := time.NewTicker(10 * time.Second)
|
||||
go func() {
|
||||
for range t.C {
|
||||
sc.LogStats()
|
||||
@@ -154,71 +111,16 @@ func (sc *SteamCache) LogStats() {
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
if sc.dirty {
|
||||
|
||||
logger.Logger.Info().Str("address", sc.address).Str("version", version.Version).Str("upstream", sc.upstream).Msg("listening")
|
||||
if sc.memory != nil { // only log memory if memory is enabled
|
||||
lifetimeBytes, lifetimeFiles, reclaimedBytes, deletedFiles, gcTime := sc.memorygc.Stats()
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("size", units.HumanSize(float64(sc.memory.Size()))).
|
||||
Str("capacity", units.HumanSize(float64(sc.memory.Capacity()))).
|
||||
Str("files", fmt.Sprintf("%d", len(sc.memory.StatAll()))).
|
||||
Msg("memory")
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("data_total", units.HumanSize(float64(lifetimeBytes))).
|
||||
Uint("files_total", lifetimeFiles).
|
||||
Str("data", units.HumanSize(float64(reclaimedBytes))).
|
||||
Uint("files", deletedFiles).
|
||||
Str("gc_time", gcTime.String()).
|
||||
Msg("memory_gc")
|
||||
}
|
||||
|
||||
if sc.disk != nil { // only log disk if disk is enabled
|
||||
lifetimeBytes, lifetimeFiles, reclaimedBytes, deletedFiles, gcTime := sc.diskgc.Stats()
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("size", units.HumanSize(float64(sc.disk.Size()))).
|
||||
Str("capacity", units.HumanSize(float64(sc.disk.Capacity()))).
|
||||
Str("files", fmt.Sprintf("%d", len(sc.disk.StatAll()))).
|
||||
Msg("disk")
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("data_total", units.HumanSize(float64(lifetimeBytes))).
|
||||
Uint("files_total", lifetimeFiles).
|
||||
Str("data", units.HumanSize(float64(reclaimedBytes))).
|
||||
Uint("files", deletedFiles).
|
||||
Str("gc_time", gcTime.String()).
|
||||
Msg("disk_gc")
|
||||
}
|
||||
|
||||
// log golang Garbage Collection stats
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("alloc", units.HumanSize(float64(m.Alloc))).
|
||||
Str("sys", units.HumanSize(float64(m.Sys))).
|
||||
Msg("app_gc")
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("memory", fmt.Sprintf("%s/%s", units.HumanSize(float64(sc.memory.Size())), units.HumanSize(float64(sc.memory.Capacity())))).Int("memory-files", len(sc.memory.StatAll())).
|
||||
Str("disk", fmt.Sprintf("%s/%s", units.HumanSize(float64(sc.disk.Size())), units.HumanSize(float64(sc.disk.Capacity())))).Int("disk-files", len(sc.disk.StatAll())).
|
||||
Str("hitrate", fmt.Sprintf("%.2f%%", sc.hits.Avg()*100)).
|
||||
Msg("cache")
|
||||
|
||||
logger.Logger.Info().Msg("") // empty line to separate log entries for better readability
|
||||
Msg("stats")
|
||||
sc.dirty = false
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if sc.pprof && r.URL.Path == "/debug/pprof/" {
|
||||
pprof.Index(w, r)
|
||||
return
|
||||
} else if sc.pprof && strings.HasPrefix(r.URL.Path, "/debug/pprof/") {
|
||||
pprof.Handler(strings.TrimPrefix(r.URL.Path, "/debug/pprof/")).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Only GET method is supported", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -231,68 +133,54 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("User-Agent") != "Valve/Steam HTTP Client 1.0" {
|
||||
http.Error(w, "Only Valve/Steam HTTP Client 1.0 is supported", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(r.URL.String(), "manifest") {
|
||||
w.Header().Add("X-LanCache-Processed-By", "SteamCache2")
|
||||
forward(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// tstart := time.Now()
|
||||
// defer func() {
|
||||
// logger.Logger.Info().Str("method", r.Method).Str("url", r.URL.String()).Str("status", w.Header().Get("X-LanCache-Status")).Dur("duration", time.Since(tstart)).Msg("Request")
|
||||
// }()
|
||||
|
||||
sc.mu.Lock()
|
||||
sc.dirty = true
|
||||
sc.mu.Unlock()
|
||||
|
||||
w.Header().Add("X-LanCache-Processed-By", "SteamCache2") // SteamPrefill uses this header to determine if the request was processed by the cache maybe steam uses it too
|
||||
|
||||
cacheKey := strings.ReplaceAll(r.URL.String()[1:], "\\", "/") // replace all backslashes with forward slashes shouldn't be necessary but just in case
|
||||
if cacheKey == "" {
|
||||
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
cacheKey := r.URL.String()
|
||||
|
||||
// if vfs is also a vfs.GetSer, we can use it to get the cache state
|
||||
|
||||
data, err := sc.vfs.Get(cacheKey)
|
||||
if err == nil {
|
||||
sc.hits.Add(cachestate.CacheStateHit)
|
||||
w.Header().Add("X-LanCache-Status", "HIT")
|
||||
w.Write(data)
|
||||
logger.Logger.Debug().Str("key", r.URL.String()).Msg("cache")
|
||||
return
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
if sc.upstream != "" { // if an upstream server is configured, proxy the request to the upstream server
|
||||
ur, err := url.JoinPath(sc.upstream, r.URL.String())
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, ur, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
req.Host = r.Host
|
||||
logger.Logger.Debug().Str("key", cacheKey).Str("host", sc.upstream).Msg("upstream")
|
||||
} else { // if no upstream server is configured, proxy the request to the host specified in the request
|
||||
host := r.Host
|
||||
if r.Header.Get("X-Sls-Https") == "enable" {
|
||||
host = "https://" + host
|
||||
} else {
|
||||
host = "http://" + host
|
||||
}
|
||||
|
||||
ur, err := url.JoinPath(host, r.URL.String())
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, ur, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Logger.Debug().Str("key", cacheKey).Str("host", host).Msg("forward")
|
||||
htt := "http://"
|
||||
if r.Header.Get("X-Sls-Https") == "enable" {
|
||||
htt = "https://"
|
||||
}
|
||||
|
||||
req.Header.Add("X-Sls-Https", r.Header.Get("X-Sls-Https"))
|
||||
req.Header.Add("User-Agent", r.Header.Get("User-Agent"))
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
base := htt + r.Host
|
||||
|
||||
hosturl, err := url.JoinPath(base, cacheKey)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Get(hosturl)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -315,3 +203,40 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("X-LanCache-Status", "MISS")
|
||||
w.Write(body)
|
||||
}
|
||||
|
||||
func forward(w http.ResponseWriter, r *http.Request) {
|
||||
htt := "http://"
|
||||
if r.Header.Get("X-Sls-Https") == "enable" {
|
||||
htt = "https://"
|
||||
}
|
||||
|
||||
base := htt + r.Host
|
||||
|
||||
cacheKey := r.URL.String()
|
||||
|
||||
hosturl, err := url.JoinPath(base, cacheKey)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Get(hosturl)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
http.Error(w, "Failed to fetch the requested URL", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read response body", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(body)
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package steamcache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCaching(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
td := t.TempDir()
|
||||
|
||||
os.WriteFile(filepath.Join(td, "key2"), []byte("value2"), 0644)
|
||||
|
||||
sc := New("localhost:8080", "1GB", 10, "1GB", 100, td, "", false)
|
||||
|
||||
sc.dirty = true
|
||||
sc.LogStats()
|
||||
|
||||
if err := sc.vfs.Set("key", []byte("value")); err != nil {
|
||||
t.Errorf("Set failed: %v", err)
|
||||
}
|
||||
if err := sc.vfs.Set("key1", []byte("value1")); err != nil {
|
||||
t.Errorf("Set failed: %v", err)
|
||||
}
|
||||
|
||||
sc.dirty = true
|
||||
sc.LogStats()
|
||||
|
||||
if sc.diskgc.Size() != 17 {
|
||||
t.Errorf("Size failed: got %d, want %d", sc.diskgc.Size(), 17)
|
||||
}
|
||||
|
||||
if sc.vfs.Size() != 17 {
|
||||
t.Errorf("Size failed: got %d, want %d", sc.vfs.Size(), 17)
|
||||
}
|
||||
|
||||
if d, err := sc.vfs.Get("key"); err != nil {
|
||||
t.Errorf("Get failed: %v", err)
|
||||
} else if string(d) != "value" {
|
||||
t.Errorf("Get failed: got %s, want %s", d, "value")
|
||||
}
|
||||
|
||||
if d, err := sc.vfs.Get("key1"); err != nil {
|
||||
t.Errorf("Get failed: %v", err)
|
||||
} else if string(d) != "value1" {
|
||||
t.Errorf("Get failed: got %s, want %s", d, "value1")
|
||||
}
|
||||
|
||||
if d, err := sc.vfs.Get("key2"); err != nil {
|
||||
t.Errorf("Get failed: %v", err)
|
||||
} else if string(d) != "value2" {
|
||||
t.Errorf("Get failed: got %s, want %s", d, "value2")
|
||||
}
|
||||
|
||||
if sc.diskgc.Size() != 17 {
|
||||
t.Errorf("Size failed: got %d, want %d", sc.diskgc.Size(), 17)
|
||||
}
|
||||
|
||||
if sc.vfs.Size() != 17 {
|
||||
t.Errorf("Size failed: got %d, want %d", sc.vfs.Size(), 17)
|
||||
}
|
||||
|
||||
sc.memory.Delete("key2")
|
||||
os.Remove(filepath.Join(td, "key2"))
|
||||
|
||||
if _, err := sc.vfs.Get("key2"); err == nil {
|
||||
t.Errorf("Get failed: got nil, want error")
|
||||
}
|
||||
}
|
||||
Vendored
+12
-13
@@ -21,24 +21,23 @@ type CacheFS struct {
|
||||
type CacheHandler func(*vfs.FileInfo, cachestate.CacheState) bool
|
||||
|
||||
// New creates a new CacheFS. fast is used for caching, and slow is used for storage. fast should obviously be faster than slow.
|
||||
func New(cacheHandler CacheHandler) *CacheFS {
|
||||
func New(fast, slow vfs.VFS, cacheHandler CacheHandler) *CacheFS {
|
||||
if slow == nil {
|
||||
panic("slow is nil")
|
||||
}
|
||||
|
||||
if fast == slow {
|
||||
panic("fast and slow are the same")
|
||||
}
|
||||
|
||||
return &CacheFS{
|
||||
fast: fast,
|
||||
slow: slow,
|
||||
|
||||
cacheHandler: cacheHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFS) SetSlow(vfs vfs.VFS) {
|
||||
if vfs == nil {
|
||||
panic("vfs is nil") // panic if the vfs is nil
|
||||
}
|
||||
|
||||
c.slow = vfs
|
||||
}
|
||||
|
||||
func (c *CacheFS) SetFast(vfs vfs.VFS) {
|
||||
c.fast = vfs
|
||||
}
|
||||
|
||||
// cacheState returns the state of the file at key.
|
||||
func (c *CacheFS) cacheState(key string) cachestate.CacheState {
|
||||
if c.fast != nil {
|
||||
|
||||
Vendored
+8
-23
@@ -20,9 +20,7 @@ func TestNew(t *testing.T) {
|
||||
fast := testMemory()
|
||||
slow := testMemory()
|
||||
|
||||
cache := New(nil)
|
||||
cache.SetFast(fast)
|
||||
cache.SetSlow(slow)
|
||||
cache := New(fast, slow, nil)
|
||||
if cache == nil {
|
||||
t.Fatal("expected cache to be non-nil")
|
||||
}
|
||||
@@ -37,9 +35,7 @@ func TestNewPanics(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
cache := New(nil)
|
||||
cache.SetFast(nil)
|
||||
cache.SetSlow(nil)
|
||||
New(nil, nil, nil)
|
||||
}
|
||||
|
||||
func TestSetAndGet(t *testing.T) {
|
||||
@@ -47,9 +43,7 @@ func TestSetAndGet(t *testing.T) {
|
||||
|
||||
fast := testMemory()
|
||||
slow := testMemory()
|
||||
cache := New(nil)
|
||||
cache.SetFast(fast)
|
||||
cache.SetSlow(slow)
|
||||
cache := New(fast, slow, nil)
|
||||
|
||||
key := "test"
|
||||
value := []byte("value")
|
||||
@@ -72,8 +66,7 @@ func TestSetAndGetNoFast(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slow := testMemory()
|
||||
cache := New(nil)
|
||||
cache.SetSlow(slow)
|
||||
cache := New(nil, slow, nil)
|
||||
|
||||
key := "test"
|
||||
value := []byte("value")
|
||||
@@ -96,11 +89,9 @@ func TestCaching(t *testing.T) {
|
||||
|
||||
fast := testMemory()
|
||||
slow := testMemory()
|
||||
cache := New(func(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
|
||||
cache := New(fast, slow, func(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
|
||||
return true
|
||||
})
|
||||
cache.SetFast(fast)
|
||||
cache.SetSlow(slow)
|
||||
|
||||
key := "test"
|
||||
value := []byte("value")
|
||||
@@ -157,9 +148,7 @@ func TestGetNotFound(t *testing.T) {
|
||||
|
||||
fast := testMemory()
|
||||
slow := testMemory()
|
||||
cache := New(nil)
|
||||
cache.SetFast(fast)
|
||||
cache.SetSlow(slow)
|
||||
cache := New(fast, slow, nil)
|
||||
|
||||
_, err := cache.Get("nonexistent")
|
||||
if !errors.Is(err, vfserror.ErrNotFound) {
|
||||
@@ -172,9 +161,7 @@ func TestDelete(t *testing.T) {
|
||||
|
||||
fast := testMemory()
|
||||
slow := testMemory()
|
||||
cache := New(nil)
|
||||
cache.SetFast(fast)
|
||||
cache.SetSlow(slow)
|
||||
cache := New(fast, slow, nil)
|
||||
|
||||
key := "test"
|
||||
value := []byte("value")
|
||||
@@ -198,9 +185,7 @@ func TestStat(t *testing.T) {
|
||||
|
||||
fast := testMemory()
|
||||
slow := testMemory()
|
||||
cache := New(nil)
|
||||
cache.SetFast(fast)
|
||||
cache.SetSlow(slow)
|
||||
cache := New(fast, slow, nil)
|
||||
|
||||
key := "test"
|
||||
value := []byte("value")
|
||||
|
||||
+22
-74
@@ -1,13 +1,11 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
||||
"s1d3sw1ped/SteamCache2/vfs"
|
||||
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -29,24 +27,6 @@ type DiskFS struct {
|
||||
|
||||
// New creates a new DiskFS.
|
||||
func new(root string, capacity int64, skipinit bool) *DiskFS {
|
||||
if capacity <= 0 {
|
||||
panic("disk capacity must be greater than 0") // panic if the capacity is less than or equal to 0
|
||||
}
|
||||
|
||||
if root == "" {
|
||||
panic("disk root must not be empty") // panic if the root is empty
|
||||
}
|
||||
|
||||
fi, err := os.Stat(root)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
panic(err) // panic if the error is something other than not found
|
||||
}
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
panic("disk root must be a directory") // panic if the root is not a directory
|
||||
}
|
||||
|
||||
dfs := &DiskFS{
|
||||
root: root,
|
||||
info: make(map[string]*vfs.FileInfo),
|
||||
@@ -80,14 +60,7 @@ func (d *DiskFS) init() {
|
||||
d.walk(d.root)
|
||||
d.sg.Wait()
|
||||
|
||||
logger.Logger.Info().
|
||||
Str("name", d.Name()).
|
||||
Str("root", d.root).
|
||||
Str("capacity", units.HumanSize(float64(d.capacity))).
|
||||
Str("size", units.HumanSize(float64(d.Size()))).
|
||||
Str("files", fmt.Sprint(len(d.info))).
|
||||
Str("duration", time.Since(tstart).String()).
|
||||
Msg("init")
|
||||
logger.Logger.Info().Str("name", d.Name()).Str("root", d.root).Str("capacity", units.HumanSize(float64(d.capacity))).Str("duration", time.Since(tstart).String()).Msg("init")
|
||||
}
|
||||
|
||||
func (d *DiskFS) walk(path string) {
|
||||
@@ -109,8 +82,7 @@ func (d *DiskFS) walk(path string) {
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
k := strings.ReplaceAll(npath[len(d.root)+1:], "\\", "/")
|
||||
logger.Logger.Debug().Str("name", k).Str("root", d.root).Msg("walk")
|
||||
k := npath[len(d.root)+1:]
|
||||
d.info[k] = vfs.NewFileInfoFromOS(info, k)
|
||||
d.mu.Unlock()
|
||||
|
||||
@@ -129,10 +101,9 @@ func (d *DiskFS) Name() string {
|
||||
}
|
||||
|
||||
func (d *DiskFS) Size() int64 {
|
||||
var size int64
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
var size int64
|
||||
for _, v := range d.info {
|
||||
size += v.Size()
|
||||
}
|
||||
@@ -140,34 +111,24 @@ func (d *DiskFS) Size() int64 {
|
||||
}
|
||||
|
||||
func (d *DiskFS) Set(key string, src []byte) error {
|
||||
if key == "" {
|
||||
return vfserror.ErrInvalidKey
|
||||
}
|
||||
if key[0] == '/' {
|
||||
return vfserror.ErrInvalidKey
|
||||
}
|
||||
|
||||
if d.capacity > 0 {
|
||||
if size := d.Size() + int64(len(src)); size > d.capacity {
|
||||
return vfserror.ErrDiskFull
|
||||
}
|
||||
}
|
||||
|
||||
logger.Logger.Debug().Str("name", key).Str("root", d.root).Msg("set")
|
||||
|
||||
if _, err := d.Stat(key); err == nil {
|
||||
logger.Logger.Debug().Str("name", key).Str("root", d.root).Msg("delete")
|
||||
d.Delete(key)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
os.MkdirAll(d.root+"/"+filepath.Dir(key), 0755)
|
||||
if err := os.WriteFile(d.root+"/"+key, src, 0644); err != nil {
|
||||
os.MkdirAll(filepath.Join(d.root, filepath.Dir(key)), 0755)
|
||||
if err := os.WriteFile(filepath.Join(d.root, key), src, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(d.root + "/" + key)
|
||||
fi, err := os.Stat(filepath.Join(d.root, key))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -179,13 +140,6 @@ func (d *DiskFS) Set(key string, src []byte) error {
|
||||
|
||||
// Delete deletes the value of key.
|
||||
func (d *DiskFS) Delete(key string) error {
|
||||
if key == "" {
|
||||
return vfserror.ErrInvalidKey
|
||||
}
|
||||
if key[0] == '/' {
|
||||
return vfserror.ErrInvalidKey
|
||||
}
|
||||
|
||||
_, err := d.Stat(key)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -193,7 +147,6 @@ func (d *DiskFS) Delete(key string) error {
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
delete(d.info, key)
|
||||
if err := os.Remove(filepath.Join(d.root, key)); err != nil {
|
||||
return err
|
||||
@@ -204,13 +157,6 @@ func (d *DiskFS) Delete(key string) error {
|
||||
|
||||
// Get gets the value of key and returns it.
|
||||
func (d *DiskFS) Get(key string) ([]byte, error) {
|
||||
if key == "" {
|
||||
return nil, vfserror.ErrInvalidKey
|
||||
}
|
||||
if key[0] == '/' {
|
||||
return nil, vfserror.ErrInvalidKey
|
||||
}
|
||||
|
||||
_, err := d.Stat(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -229,23 +175,25 @@ func (d *DiskFS) Get(key string) ([]byte, error) {
|
||||
|
||||
// Stat returns the FileInfo of key. If key is not found in the cache, it will stat the file on disk. If the file is not found on disk, it will return vfs.ErrNotFound.
|
||||
func (d *DiskFS) Stat(key string) (*vfs.FileInfo, error) {
|
||||
if key == "" {
|
||||
return nil, vfserror.ErrInvalidKey
|
||||
}
|
||||
if key[0] == '/' {
|
||||
return nil, vfserror.ErrInvalidKey
|
||||
}
|
||||
|
||||
logger.Logger.Debug().Str("name", key).Str("root", d.root).Msg("stat")
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
fi, ok := d.info[key]
|
||||
d.mu.Unlock() // unlock before statting the file
|
||||
|
||||
if fi, ok := d.info[key]; !ok {
|
||||
return nil, vfserror.ErrNotFound
|
||||
} else {
|
||||
return fi, nil
|
||||
if !ok {
|
||||
fii, err := os.Stat(filepath.Join(d.root, key))
|
||||
if err != nil {
|
||||
return nil, vfserror.ErrNotFound
|
||||
}
|
||||
|
||||
d.mu.Lock() // relock to update the info map
|
||||
defer d.mu.Unlock() // nothing else needs to unlock before returning
|
||||
|
||||
d.info[key] = vfs.NewFileInfoFromOS(fii, key)
|
||||
fi = d.info[key]
|
||||
// fallthrough to return fi with shiny new info
|
||||
}
|
||||
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
func (m *DiskFS) StatAll() []*vfs.FileInfo {
|
||||
|
||||
@@ -85,62 +85,3 @@ func TestInit(t *testing.T) {
|
||||
t.Errorf("Stat failed: got %s, want %s", s.Name(), "key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskSizeDiscrepancy(t *testing.T) {
|
||||
t.Parallel()
|
||||
td := t.TempDir()
|
||||
|
||||
assumedSize := int64(6 + 5 + 6) // 6 + 5 + 6 bytes for key, key1, key2
|
||||
os.WriteFile(filepath.Join(td, "key2"), []byte("value2"), 0644)
|
||||
|
||||
m := New(td, 1024)
|
||||
if 6 != m.Size() {
|
||||
t.Errorf("Size failed: got %d, want %d", m.Size(), 6)
|
||||
}
|
||||
|
||||
if err := m.Set("key", []byte("value")); err != nil {
|
||||
t.Errorf("Set failed: %v", err)
|
||||
}
|
||||
|
||||
if err := m.Set("key1", []byte("value1")); err != nil {
|
||||
t.Errorf("Set failed: %v", err)
|
||||
}
|
||||
|
||||
if assumedSize != m.Size() {
|
||||
t.Errorf("Size failed: got %d, want %d", m.Size(), assumedSize)
|
||||
}
|
||||
|
||||
if d, err := m.Get("key"); err != nil {
|
||||
t.Errorf("Get failed: %v", err)
|
||||
} else if string(d) != "value" {
|
||||
t.Errorf("Get failed: got %s, want %s", d, "value")
|
||||
}
|
||||
|
||||
if d, err := m.Get("key1"); err != nil {
|
||||
t.Errorf("Get failed: %v", err)
|
||||
} else if string(d) != "value1" {
|
||||
t.Errorf("Get failed: got %s, want %s", d, "value1")
|
||||
}
|
||||
|
||||
m = New(td, 1024)
|
||||
|
||||
if assumedSize != m.Size() {
|
||||
t.Errorf("Size failed: got %d, want %d", m.Size(), assumedSize)
|
||||
}
|
||||
|
||||
if d, err := m.Get("key"); err != nil {
|
||||
t.Errorf("Get failed: %v", err)
|
||||
} else if string(d) != "value" {
|
||||
t.Errorf("Get failed: got %s, want %s", d, "value")
|
||||
}
|
||||
|
||||
if d, err := m.Get("key1"); err != nil {
|
||||
t.Errorf("Get failed: %v", err)
|
||||
} else if string(d) != "value1" {
|
||||
t.Errorf("Get failed: got %s, want %s", d, "value1")
|
||||
}
|
||||
|
||||
if assumedSize != m.Size() {
|
||||
t.Errorf("Size failed: got %d, want %d", m.Size(), assumedSize)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,9 +12,9 @@ type FileInfo struct {
|
||||
ATime time.Time
|
||||
}
|
||||
|
||||
func NewFileInfo(key string, size int64, modTime time.Time) *FileInfo {
|
||||
func NewFileInfo(name string, size int64, modTime time.Time) *FileInfo {
|
||||
return &FileInfo{
|
||||
name: key,
|
||||
name: name,
|
||||
size: size,
|
||||
MTime: modTime,
|
||||
ATime: time.Now(),
|
||||
|
||||
+5
-47
@@ -4,8 +4,6 @@ import (
|
||||
"fmt"
|
||||
"s1d3sw1ped/SteamCache2/vfs"
|
||||
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Ensure GCFS implements VFS.
|
||||
@@ -14,23 +12,14 @@ var _ vfs.VFS = (*GCFS)(nil)
|
||||
// GCFS is a virtual file system that calls a GC handler when the disk is full. The GC handler is responsible for freeing up space on the disk. The GCFS is a wrapper around another VFS.
|
||||
type GCFS struct {
|
||||
vfs.VFS
|
||||
multiplier int
|
||||
|
||||
// protected by mu
|
||||
gcHanderFunc GCHandlerFunc
|
||||
lifetimeBytes, lifetimeFiles uint
|
||||
reclaimedBytes, deletedFiles uint
|
||||
gcTime time.Duration
|
||||
mu sync.Mutex
|
||||
multiplier int
|
||||
gcHanderFunc GCHandlerFunc
|
||||
}
|
||||
|
||||
// GCHandlerFunc is a function that is called when the disk is full and the GCFS needs to free up space. It is passed the VFS and the size of the file that needs to be written. Its up to the implementation to free up space. How much space is freed is also up to the implementation.
|
||||
type GCHandlerFunc func(vfs vfs.VFS, size uint) (reclaimedBytes uint, deletedFiles uint)
|
||||
type GCHandlerFunc func(vfs vfs.VFS, size int)
|
||||
|
||||
func New(vfs vfs.VFS, multiplier int, gcHandlerFunc GCHandlerFunc) *GCFS {
|
||||
if multiplier <= 0 {
|
||||
multiplier = 1 // if the multiplier is less than or equal to 0 set it to 1 will be slow but the user can set it to a higher value if they want
|
||||
}
|
||||
return &GCFS{
|
||||
VFS: vfs,
|
||||
multiplier: multiplier,
|
||||
@@ -38,44 +27,13 @@ func New(vfs vfs.VFS, multiplier int, gcHandlerFunc GCHandlerFunc) *GCFS {
|
||||
}
|
||||
}
|
||||
|
||||
// Stats returns the lifetime bytes, lifetime files, reclaimed bytes and deleted files.
|
||||
// The lifetime bytes and lifetime files are the total bytes and files that have been freed up by the GC handler.
|
||||
// The reclaimed bytes and deleted files are the bytes and files that have been freed up by the GC handler since last call to Stats.
|
||||
// The gc time is the total time spent in the GC handler since last call to Stats.
|
||||
// The reclaimed bytes and deleted files and gc time are reset to 0 after the call to Stats.
|
||||
func (g *GCFS) Stats() (lifetimeBytes, lifetimeFiles, reclaimedBytes, deletedFiles uint, gcTime time.Duration) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
g.lifetimeBytes += g.reclaimedBytes
|
||||
g.lifetimeFiles += g.deletedFiles
|
||||
|
||||
lifetimeBytes = g.lifetimeBytes
|
||||
lifetimeFiles = g.lifetimeFiles
|
||||
reclaimedBytes = g.reclaimedBytes
|
||||
deletedFiles = g.deletedFiles
|
||||
gcTime = g.gcTime
|
||||
|
||||
g.reclaimedBytes = 0
|
||||
g.deletedFiles = 0
|
||||
g.gcTime = time.Duration(0)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Set overrides the Set method of the VFS interface. It tries to set the key and src, if it fails due to disk full error, it calls the GC handler and tries again. If it still fails it returns the error.
|
||||
func (g *GCFS) Set(key string, src []byte) error {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
err := g.VFS.Set(key, src) // try to set the key and src
|
||||
|
||||
if err == vfserror.ErrDiskFull && g.gcHanderFunc != nil { // if the error is disk full and there is a GC handler
|
||||
tstart := time.Now()
|
||||
reclaimedBytes, deletedFiles := g.gcHanderFunc(g.VFS, uint(len(src)*g.multiplier)) // call the GC handler
|
||||
g.gcTime += time.Since(tstart)
|
||||
g.reclaimedBytes += reclaimedBytes
|
||||
g.deletedFiles += deletedFiles
|
||||
err = g.VFS.Set(key, src) // try again after GC if it still fails return the error
|
||||
g.gcHanderFunc(g.VFS, len(src)*g.multiplier) // call the GC handler
|
||||
err = g.VFS.Set(key, src) // try again after GC if it still fails return the error
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
+18
-12
@@ -6,6 +6,7 @@ import (
|
||||
"s1d3sw1ped/SteamCache2/vfs/memory"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/rand"
|
||||
)
|
||||
@@ -14,11 +15,13 @@ func TestGCSmallRandom(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := memory.New(1024 * 1024 * 16)
|
||||
gc := New(m, 10, func(vfs vfs.VFS, size uint) (uint, uint) {
|
||||
gc := New(m, 10, func(vfs vfs.VFS, size int) {
|
||||
tstart := time.Now()
|
||||
deletions := 0
|
||||
var reclaimed uint
|
||||
targetreclaim := int64(size)
|
||||
var reclaimed int64
|
||||
|
||||
t.Logf("GC starting to reclaim %d bytes", size)
|
||||
t.Logf("GC starting to reclaim %d bytes", targetreclaim)
|
||||
|
||||
stats := vfs.StatAll()
|
||||
sort.Slice(stats, func(i, j int) bool {
|
||||
@@ -28,7 +31,7 @@ func TestGCSmallRandom(t *testing.T) {
|
||||
|
||||
// Delete the oldest files until we've reclaimed enough space.
|
||||
for _, s := range stats {
|
||||
sz := uint(s.Size()) // Get the size of the file
|
||||
sz := s.Size() // Get the size of the file
|
||||
err := vfs.Delete(s.Name())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -38,11 +41,12 @@ func TestGCSmallRandom(t *testing.T) {
|
||||
|
||||
// t.Logf("GC deleting %s, %v", s.Name(), s.AccessTime().Format(time.RFC3339Nano))
|
||||
|
||||
if reclaimed >= size { // We've reclaimed enough space
|
||||
if reclaimed >= targetreclaim { // We've reclaimed enough space
|
||||
break
|
||||
}
|
||||
}
|
||||
return uint(reclaimed), uint(deletions)
|
||||
|
||||
t.Logf("GC took %v to reclaim %d bytes by deleting %d files", time.Since(tstart), reclaimed, deletions)
|
||||
})
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
@@ -66,11 +70,13 @@ func TestGCLargeRandom(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := memory.New(1024 * 1024 * 16) // 16MB
|
||||
gc := New(m, 10, func(vfs vfs.VFS, size uint) (uint, uint) {
|
||||
gc := New(m, 10, func(vfs vfs.VFS, size int) {
|
||||
tstart := time.Now()
|
||||
deletions := 0
|
||||
var reclaimed uint
|
||||
targetreclaim := int64(size)
|
||||
var reclaimed int64
|
||||
|
||||
t.Logf("GC starting to reclaim %d bytes", size)
|
||||
t.Logf("GC starting to reclaim %d bytes", targetreclaim)
|
||||
|
||||
stats := vfs.StatAll()
|
||||
sort.Slice(stats, func(i, j int) bool {
|
||||
@@ -80,17 +86,17 @@ func TestGCLargeRandom(t *testing.T) {
|
||||
|
||||
// Delete the oldest files until we've reclaimed enough space.
|
||||
for _, s := range stats {
|
||||
sz := uint(s.Size()) // Get the size of the file
|
||||
sz := s.Size() // Get the size of the file
|
||||
vfs.Delete(s.Name())
|
||||
reclaimed += sz // Track how much space we've reclaimed
|
||||
deletions++ // Track how many files we've deleted
|
||||
|
||||
if reclaimed >= size { // We've reclaimed enough space
|
||||
if reclaimed >= targetreclaim { // We've reclaimed enough space
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return uint(reclaimed), uint(deletions)
|
||||
t.Logf("GC took %v to reclaim %d bytes by deleting %d files", time.Since(tstart), reclaimed, deletions)
|
||||
})
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
|
||||
@@ -25,10 +25,6 @@ type MemoryFS struct {
|
||||
|
||||
// New creates a new MemoryFS.
|
||||
func New(capacity int64) *MemoryFS {
|
||||
if capacity <= 0 {
|
||||
panic("memory capacity must be greater than 0") // panic if the capacity is less than or equal to 0
|
||||
}
|
||||
|
||||
return &MemoryFS{
|
||||
files: make(map[string]*file),
|
||||
capacity: capacity,
|
||||
@@ -73,8 +69,9 @@ func (m *MemoryFS) Set(key string, src []byte) error {
|
||||
int64(len(src)),
|
||||
time.Now(),
|
||||
),
|
||||
data: src,
|
||||
data: make([]byte, len(src)),
|
||||
}
|
||||
copy(m.files[key].data, src)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ package vfserror
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrInvalidKey is returned when a key is invalid.
|
||||
ErrInvalidKey = errors.New("vfs: invalid key")
|
||||
|
||||
// ErrUnreachable is returned when a code path is unreachable.
|
||||
ErrUnreachable = errors.New("unreachable")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user