40 Commits

Author SHA1 Message Date
s1d3sw1ped 6fc83063da chore: remove outdated copyright notice from main.go
Release / Build versioned release (push) Successful in 11s
2025-01-22 17:34:56 -06:00
s1d3sw1ped cd4b5e0598 chore: remove outdated copyright notice from root.go 2025-01-22 17:34:41 -06:00
s1d3sw1ped e583780b9c feat: import version package to access global version variable 2025-01-22 17:34:21 -06:00
s1d3sw1ped c0ed8a590e feat: include application version in logging output 2025-01-22 17:32:45 -06:00
s1d3sw1ped 09d3450738 feat: add version command to display application version 2025-01-22 17:32:30 -06:00
s1d3sw1ped f01b15c8c6 docs: clarify license coverage in README.md 2025-01-22 17:32:04 -06:00
s1d3sw1ped 27ac30d28a chore: add 'dist/' to .gitignore to exclude distribution files 2025-01-22 17:07:06 -06:00
s1d3sw1ped 0424de1fca chore: format launch.json arguments for improved readability 2025-01-22 17:06:35 -06:00
s1d3sw1ped 32c0d85bad chore: remove unused extra files from goreleaser configuration 2025-01-22 10:03:14 -06:00
s1d3sw1ped 6b51e61a87 chore: remove unused goreleaser checksum script 2025-01-22 10:02:19 -06:00
s1d3sw1ped 1094505f20 fix: increase hits buffer size in AvgCacheState initialization
Release / Build versioned release (push) Successful in 8s
2025-01-22 00:02:11 -06:00
s1d3sw1ped 93d04410bf feat: integrate zerolog for structured logging and replace fmt with logger
Release / Build versioned release (push) Successful in 20s
2025-01-21 23:33:14 -06:00
s1d3sw1ped 62f7a38295 refactor: replace log statements with fmt for console output
Release / Build versioned release (push) Successful in 16s
2025-01-21 20:56:58 -06:00
s1d3sw1ped c370e80888 fix: initialize the avgcachestatus
Release / Build versioned release (push) Successful in 14s
2025-01-21 20:20:48 -06:00
s1d3sw1ped 9d0f4894e1 refactor: comment out logging for request duration in ServeHTTP method
Release / Build versioned release (push) Successful in 16s
2025-01-21 20:03:11 -06:00
s1d3sw1ped 745ee37184 feat: add AvgCacheState to track average cache hits and misses 2025-01-21 20:02:34 -06:00
s1d3sw1ped 5a9b07f609 refactor: streamline release and test workflows by removing unnecessary steps 2025-01-21 15:49:50 -06:00
s1d3sw1ped d88db3d9c1 refactor: remove old CI workflows and consolidate into a new test workflow
Release / Build versioned release (push) Successful in 14s
2025-01-21 15:37:31 -06:00
s1d3sw1ped 91075232cd refactor: remove unnecessary go generate step from goreleaser configuration
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 57s
2025-01-21 15:26:30 -06:00
s1d3sw1ped 1f588fa20a fix: handle error from http.ListenAndServe in SteamCache
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 59s
2025-01-21 15:24:08 -06:00
s1d3sw1ped d39a2ad4fa refactor: remove address flag and set default value in command configuration 2025-01-21 15:24:01 -06:00
s1d3sw1ped 80cc75f372 docs: update README with garbage collection details and Windows hosts file override instructions 2025-01-21 15:23:24 -06:00
s1d3sw1ped 53f5d1577c refactor: simplify tag release workflow by removing unnecessary steps
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 17s
CI / Test (push) Successful in 59s
2025-01-21 13:38:23 -06:00
s1d3sw1ped 1186e14c56 fix: update Gitea URLs in goreleaser configuration forehead
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 1m1s
Release versioned tag / Build versioned release (push) Successful in 1m11s
2025-01-21 13:37:12 -06:00
s1d3sw1ped 3bb57b7741 refactor: update goreleaser configuration for improved build and release process
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 1m0s
Release versioned tag / Build versioned release (push) Failing after 1m7s
2025-01-21 13:34:51 -06:00
s1d3sw1ped 8f2d1eff9b refactor: remove commented build steps and add checksum script for binaries
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 14s
CI / Test (push) Successful in 59s
Release versioned tag / Build versioned release (push) Failing after 1m5s
2025-01-21 13:18:21 -06:00
s1d3sw1ped 27242fdf05 test
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 14s
CI / Test (push) Successful in 1m0s
Release versioned tag / Build versioned release (push) Failing after 1m25s
2025-01-21 13:07:42 -06:00
s1d3sw1ped 55330ebd3c get the entire repo depth for changelogs
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 58s
Release versioned tag / Build versioned release (push) Failing after 1m6s
2025-01-21 13:03:23 -06:00
s1d3sw1ped e37dc930dd test goreleaser
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 59s
Release versioned tag / Build versioned release (push) Failing after 1m3s
2025-01-21 12:51:17 -06:00
s1d3sw1ped 79f0814745 refactor CI workflows to streamline linting and release processes
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 14s
CI / Test (push) Successful in 57s
Release versioned tag / Build versioned release (push) Failing after 1m5s
2025-01-21 12:36:31 -06:00
s1d3sw1ped 1feee2758c fixed bad character
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 14s
CI / Test (push) Successful in 59s
Release versioned tag / Lint (push) Failing after 6s
Release versioned tag / Build versioned release (push) Failing after 20s
Release versioned tag / Test (push) Successful in 58s
2025-01-21 12:32:20 -06:00
s1d3sw1ped 22f9196960 enable parallel execution for cache tests
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 15s
CI / Test (push) Successful in 58s
2025-01-21 12:27:49 -06:00
s1d3sw1ped 9d0a09d8be minor fixes to gc_test to lower the log spam
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 16s
CI / Test (push) Successful in 58s
2025-01-21 12:25:44 -06:00
s1d3sw1ped f6f007bf47 d for disable
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 15s
CI / Test (push) Successful in 59s
2025-01-21 12:22:20 -06:00
s1d3sw1ped 232b290a44 disable errcheck
CI / Lint (push) Failing after 7s
CI / Build (push) Successful in 20s
CI / Test (push) Successful in 59s
2025-01-21 12:21:05 -06:00
s1d3sw1ped b55b95f5d8 fixed a invisible character in yaml
CI / Lint (push) Failing after 27s
CI / Build (push) Successful in 27s
CI / Test (push) Successful in 1m11s
2025-01-21 12:07:31 -06:00
s1d3sw1ped ca6c9a5861 added CI
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
2025-01-21 12:06:25 -06:00
s1d3sw1ped 16dce1f0c2 it works 2025-01-21 11:52:04 -06:00
s1d3sw1ped 2be7b117ea lottsa stuff 2025-01-19 20:17:57 -06:00
s1d3sw1ped 88ecb1bc24 initial commit 2025-01-18 20:03:40 -06:00
19 changed files with 232 additions and 584 deletions
+2 -1
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
name: PR Check
name: CI
on:
- pull_request
-1
View File
@@ -36,7 +36,6 @@ changelog:
- "^test:"
release:
name_template: '{{.ProjectName}}-{{.Version}}'
footer: >-
---
+1 -34
View File
@@ -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
View File
@@ -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")
}
+9 -12
View File
@@ -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)
}
+41 -15
View File
@@ -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 {
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
}
-4
View File
@@ -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()
+86 -167
View File
@@ -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
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,66 +47,35 @@ 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)
logger.Logger.Info().Bool("memory", true).Bool("disk", false).Msg("configuration")
} else if disksize != 0 && memorysize == 0 {
// disk only mode
c.SetSlow(dgc)
logger.Logger.Info().Bool("memory", false).Bool("disk", true).Msg("configuration")
} else if disksize != 0 && memorysize != 0 {
// memory and disk mode
c.SetFast(mgc)
c.SetSlow(dgc)
logger.Logger.Info().Bool("memory", true).Bool("disk", true).Msg("configuration")
} 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),
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()))
}
diskgc(d, int(d.Size()-d.Capacity()))
}
return sc
@@ -124,21 +84,12 @@ func New(address string, memorySize string, memoryMultiplier int, diskSize strin
func (sc *SteamCache) Run() {
logger.Logger.Info().Str("address", sc.address).Str("version", version.Version).Msg("listening")
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("upstream", sc.upstream).Msg("connected")
}
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()
@@ -160,71 +111,16 @@ func (sc *SteamCache) LogStats() {
sc.mu.Lock()
defer sc.mu.Unlock()
if sc.dirty {
logger.Logger.Info().Msg("") // empty line to separate log entries for better readability
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("go_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")
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
@@ -237,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
htt := "http://"
if r.Header.Get("X-Sls-Https") == "enable" {
host = "https://" + host
} else {
host = "http://" + host
htt = "https://"
}
ur, err := url.JoinPath(host, r.URL.String())
base := htt + r.Host
hosturl, err := url.JoinPath(base, cacheKey)
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")
}
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)
resp, err := http.Get(hosturl)
if err != nil {
http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError)
return
@@ -321,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)
}
-71
View File
@@ -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")
}
}
+12 -13
View File
@@ -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 {
+8 -23
View File
@@ -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")
+21 -73
View File
@@ -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 {
if !ok {
fii, err := os.Stat(filepath.Join(d.root, key))
if err != nil {
return nil, vfserror.ErrNotFound
} else {
return fi, nil
}
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 {
-59
View File
@@ -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
View File
@@ -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(),
+2 -44
View File
@@ -4,8 +4,6 @@ import (
"fmt"
"s1d3sw1ped/SteamCache2/vfs"
"s1d3sw1ped/SteamCache2/vfs/vfserror"
"sync"
"time"
)
// Ensure GCFS implements VFS.
@@ -15,22 +13,13 @@ var _ vfs.VFS = (*GCFS)(nil)
type GCFS struct {
vfs.VFS
multiplier int
// protected by mu
gcHanderFunc GCHandlerFunc
lifetimeBytes, lifetimeFiles uint
reclaimedBytes, deletedFiles uint
gcTime time.Duration
mu sync.Mutex
}
// 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,43 +27,12 @@ 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
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
}
+18 -12
View File
@@ -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++ {
+2 -5
View File
@@ -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
View File
@@ -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")